Recherche récursive de sous-dossier et retour de fichiers dans une liste python

118

Je travaille sur un script pour parcourir de manière récursive les sous-dossiers d'un dossier principal et créer une liste à partir d'un certain type de fichier. J'ai un problème avec le script. Il est actuellement défini comme suit

for root, subFolder, files in os.walk(PATH):
    for item in files:
        if item.endswith(".txt") :
            fileNamePath = str(os.path.join(root,subFolder,item))

le problème est que la variable subFolder extrait une liste de sous-dossiers plutôt que le dossier dans lequel se trouve le fichier ITEM. Je pensais exécuter une boucle for pour le sous-dossier avant et rejoindre la première partie du chemin, mais j'ai pensé que je vérifiais deux fois si quelqu'un avait des suggestions avant cela. Merci de votre aide!

user2709514
la source

Réponses:

156

Vous devriez utiliser le dirpathque vous appelez root. Le dirnamessont fournis afin que vous puissiez l'élaguer s'il y a des dossiers dans lesquels vous ne souhaitez os.walkpas rentrer.

import os
result = [os.path.join(dp, f) for dp, dn, filenames in os.walk(PATH) for f in filenames if os.path.splitext(f)[1] == '.txt']

Éditer:

Après le dernier vote défavorable, il m'est apparu que c'était globun meilleur outil de sélection par extension.

import os
from glob import glob
result = [y for x in os.walk(PATH) for y in glob(os.path.join(x[0], '*.txt'))]

Également une version générateur

from itertools import chain
result = (chain.from_iterable(glob(os.path.join(x[0], '*.txt')) for x in os.walk('.')))

Edit2 pour Python 3.4+

from pathlib import Path
result = list(Path(".").rglob("*.[tT][xX][tT]"))
John La Rooy
la source
1
Le motif global '*. [Tt] [Xx] [Tt]' rendra la recherche insensible à la casse.
SergiyKolesnikov
@SergiyKolesnikov, Merci, je l'ai utilisé dans l'édition en bas. Notez que le rglobest insensible sur les plates-formes Windows - mais il n'est pas insensible au portage.
John La Rooy
1
@JohnLaRooy Cela fonctionne globaussi avec (Python 3.6 ici):glob.iglob(os.path.join(real_source_path, '**', '*.[xX][mM][lL]')
SergiyKolesnikov
@Sergiy: Votre iglobne fonctionne pas pour les fichiers dans les sous-sous-sous-dossiers ou ci-dessous. Vous devez ajouter recursive=True.
user136036
1
@ user136036, "meilleur" ne signifie pas toujours le plus rapide. Parfois, la lisibilité et la maintenabilité sont également importantes.
John La Rooy le
111

Modifié dans Python 3.5 : Prise en charge des globs récursifs utilisant «**».

glob.glob()a un nouveau paramètre récursif .

Si vous souhaitez récupérer tous les .txtfichiers sous my_path(y compris récursivement les sous- répertoires):

import glob

files = glob.glob(my_path + '/**/*.txt', recursive=True)

# my_path/     the dir
# **/       every file and dir under my_path
# *.txt     every file that ends with '.txt'

Si vous avez besoin d'un itérateur, vous pouvez utiliser iglob comme alternative:

for file in glob.iglob(my_path, recursive=False):
    # ...
Rotareti
la source
1
TypeError: glob () a obtenu un argument de mot clé inattendu 'recursive'
CyberJacob
1
Cela devrait fonctionner. Assurez-vous d'utiliser une version> = 3.5. J'ai ajouté un lien vers la documentation dans ma réponse pour plus de détails.
Rotareti
Ce serait pourquoi, je suis sur 2.7
CyberJacob
1
Pourquoi la compréhension de la liste et pas seulement files = glob.glob(PATH + '/*/**/*.txt', recursive=True)?
tobltobs
Oups! :) C'est totalement redondant. Aucune idée de ce qui m'a poussé à l'écrire comme ça. Merci de l'avoir mentionné! Je le réparerai.
Rotareti
20

Je vais traduire la compréhension de la liste de John La Rooy en for imbriquée, juste au cas où quelqu'un d'autre aurait du mal à la comprendre.

result = [y for x in os.walk(PATH) for y in glob(os.path.join(x[0], '*.txt'))]

Doit être équivalent à:

import glob

result = []

for x in os.walk(PATH):
    for y in glob.glob(os.path.join(x[0], '*.txt')):
        result.append(y)

Voici la documentation pour la compréhension de liste et les fonctions os.walk et glob.glob .

Jefferson Lima
la source
1
Cette réponse a fonctionné pour moi dans Python 3.7.3. glob.glob(..., recursive=True)et list(Path(dir).glob(...'))non.
miguelmorin
11

Cela semble être la solution la plus rapide que je pourrais trouver, et est plus rapide os.walket beaucoup plus rapide que n'importe quelle globsolution .

  • Il vous donnera également une liste de tous les sous-dossiers imbriqués sans frais.
  • Vous pouvez rechercher plusieurs extensions différentes.
  • Vous pouvez également choisir de renvoyer les chemins complets ou simplement les noms des fichiers en changeant f.pathen f.name(ne le changez pas pour les sous-dossiers!).

Args: dir: str, ext: list.
La fonction renvoie deux listes:subfolders, files .

Voir ci-dessous pour une analyse détaillée de la vitesse.

def run_fast_scandir(dir, ext):    # dir: str, ext: list
    subfolders, files = [], []

    for f in os.scandir(dir):
        if f.is_dir():
            subfolders.append(f.path)
        if f.is_file():
            if os.path.splitext(f.name)[1].lower() in ext:
                files.append(f.path)


    for dir in list(subfolders):
        sf, f = run_fast_scandir(dir, ext)
        subfolders.extend(sf)
        files.extend(f)
    return subfolders, files


subfolders, files = run_fast_scandir(folder, [".jpg"])


Analyse de la vitesse

pour diverses méthodes pour obtenir tous les fichiers avec une extension de fichier spécifique dans tous les sous-dossiers et le dossier principal.

tl; dr:
- fast_scandirgagne clairement et est deux fois plus rapide que toutes les autres solutions, sauf os.walk.
- os.walkest la deuxième place légèrement plus lente.
- l'utilisation globralentira considérablement le processus.
- Aucun des résultats n'utilise le tri naturel . Cela signifie que les résultats seront triés comme suit: 1, 10, 2. Pour obtenir un tri naturel (1, 2, 10), veuillez consulter https://stackoverflow.com/a/48030307/2441026


Résultats:

fast_scandir    took  499 ms. Found files: 16596. Found subfolders: 439
os.walk         took  589 ms. Found files: 16596
find_files      took  919 ms. Found files: 16596
glob.iglob      took  998 ms. Found files: 16596
glob.glob       took 1002 ms. Found files: 16596
pathlib.rglob   took 1041 ms. Found files: 16596
os.walk-glob    took 1043 ms. Found files: 16596

Les tests ont été réalisés avec W7x64, Python 3.8.1, 20 exécutions. 16596 fichiers dans 439 sous-dossiers (partiellement imbriqués).
find_filesprovient de https://stackoverflow.com/a/45646357/2441026 et vous permet de rechercher plusieurs extensions.
fast_scandira été écrit par moi-même et renverra également une liste de sous-dossiers. Vous pouvez lui donner une liste d'extensions à rechercher (j'ai testé une liste avec une entrée à une simple if ... == ".jpg"et il n'y avait pas de différence significative).


# -*- coding: utf-8 -*-
# Python 3


import time
import os
from glob import glob, iglob
from pathlib import Path


directory = r"<folder>"
RUNS = 20


def run_os_walk():
    a = time.time_ns()
    for i in range(RUNS):
        fu = [os.path.join(dp, f) for dp, dn, filenames in os.walk(directory) for f in filenames if
                  os.path.splitext(f)[1].lower() == '.jpg']
    print(f"os.walk\t\t\ttook {(time.time_ns() - a) / 1000 / 1000 / RUNS:.0f} ms. Found files: {len(fu)}")


def run_os_walk_glob():
    a = time.time_ns()
    for i in range(RUNS):
        fu = [y for x in os.walk(directory) for y in glob(os.path.join(x[0], '*.jpg'))]
    print(f"os.walk-glob\ttook {(time.time_ns() - a) / 1000 / 1000 / RUNS:.0f} ms. Found files: {len(fu)}")


def run_glob():
    a = time.time_ns()
    for i in range(RUNS):
        fu = glob(os.path.join(directory, '**', '*.jpg'), recursive=True)
    print(f"glob.glob\t\ttook {(time.time_ns() - a) / 1000 / 1000 / RUNS:.0f} ms. Found files: {len(fu)}")


def run_iglob():
    a = time.time_ns()
    for i in range(RUNS):
        fu = list(iglob(os.path.join(directory, '**', '*.jpg'), recursive=True))
    print(f"glob.iglob\t\ttook {(time.time_ns() - a) / 1000 / 1000 / RUNS:.0f} ms. Found files: {len(fu)}")


def run_pathlib_rglob():
    a = time.time_ns()
    for i in range(RUNS):
        fu = list(Path(directory).rglob("*.jpg"))
    print(f"pathlib.rglob\ttook {(time.time_ns() - a) / 1000 / 1000 / RUNS:.0f} ms. Found files: {len(fu)}")


def find_files(files, dirs=[], extensions=[]):
    # https://stackoverflow.com/a/45646357/2441026

    new_dirs = []
    for d in dirs:
        try:
            new_dirs += [ os.path.join(d, f) for f in os.listdir(d) ]
        except OSError:
            if os.path.splitext(d)[1].lower() in extensions:
                files.append(d)

    if new_dirs:
        find_files(files, new_dirs, extensions )
    else:
        return


def run_fast_scandir(dir, ext):    # dir: str, ext: list
    # https://stackoverflow.com/a/59803793/2441026

    subfolders, files = [], []

    for f in os.scandir(dir):
        if f.is_dir():
            subfolders.append(f.path)
        if f.is_file():
            if os.path.splitext(f.name)[1].lower() in ext:
                files.append(f.path)


    for dir in list(subfolders):
        sf, f = run_fast_scandir(dir, ext)
        subfolders.extend(sf)
        files.extend(f)
    return subfolders, files



if __name__ == '__main__':
    run_os_walk()
    run_os_walk_glob()
    run_glob()
    run_iglob()
    run_pathlib_rglob()


    a = time.time_ns()
    for i in range(RUNS):
        files = []
        find_files(files, dirs=[directory], extensions=[".jpg"])
    print(f"find_files\t\ttook {(time.time_ns() - a) / 1000 / 1000 / RUNS:.0f} ms. Found files: {len(files)}")


    a = time.time_ns()
    for i in range(RUNS):
        subf, files = run_fast_scandir(directory, [".jpg"])
    print(f"fast_scandir\ttook {(time.time_ns() - a) / 1000 / 1000 / RUNS:.0f} ms. Found files: {len(files)}. Found subfolders: {len(subf)}")
user136036
la source
10

La nouvelle pathlibbibliothèque simplifie cela à une ligne:

from pathlib import Path
result = list(Path(PATH).glob('**/*.txt'))

Vous pouvez également utiliser la version générateur:

from pathlib import Path
for file in Path(PATH).glob('**/*.txt'):
    pass

Cela renvoie des Pathobjets, que vous pouvez utiliser pour à peu près n'importe quoi, ou obtenir le nom du fichier sous forme de chaîne par file.name.

Emre
la source
6

Ce n'est pas la réponse la plus pythonique, mais je vais la mettre ici pour le plaisir car c'est une belle leçon de récursivité

def find_files( files, dirs=[], extensions=[]):
    new_dirs = []
    for d in dirs:
        try:
            new_dirs += [ os.path.join(d, f) for f in os.listdir(d) ]
        except OSError:
            if os.path.splitext(d)[1] in extensions:
                files.append(d)

    if new_dirs:
        find_files(files, new_dirs, extensions )
    else:
        return

Sur ma machine, j'ai deux dossiers rootetroot2

mender@multivax ]ls -R root root2
root:
temp1 temp2

root/temp1:
temp1.1 temp1.2

root/temp1/temp1.1:
f1.mid

root/temp1/temp1.2:
f.mi  f.mid

root/temp2:
tmp.mid

root2:
dummie.txt temp3

root2/temp3:
song.mid

Disons que je veux trouver tous .txtles .midfichiers dans l'un ou l'autre de ces répertoires, alors je peux simplement faire

files = []
find_files( files, dirs=['root','root2'], extensions=['.mid','.txt'] )
print(files)

#['root2/dummie.txt',
# 'root/temp2/tmp.mid',
# 'root2/temp3/song.mid',
# 'root/temp1/temp1.1/f1.mid',
# 'root/temp1/temp1.2/f.mid']
dermen
la source
4

Le récursif est nouveau dans Python 3.5, il ne fonctionnera donc pas sur Python 2.7. Voici l'exemple qui utilise des rchaînes, il vous suffit donc de fournir le chemin tel quel sur Win, Lin, ...

import glob

mypath=r"C:\Users\dj\Desktop\nba"

files = glob.glob(mypath + r'\**\*.py', recursive=True)
# print(files) # as list
for f in files:
    print(f) # nice looking single line per file

Remarque: il répertorie tous les fichiers, quelle que soit leur profondeur.

prosti
la source
3

Vous pouvez le faire de cette façon pour vous renvoyer une liste de fichiers de chemin absolus.

def list_files_recursive(path):
    """
    Function that receives as a parameter a directory path
    :return list_: File List and Its Absolute Paths
    """

    import os

    files = []

    # r = root, d = directories, f = files
    for r, d, f in os.walk(path):
        for file in f:
            files.append(os.path.join(r, file))

    lst = [file for file in files]
    return lst


if __name__ == '__main__':

    result = list_files_recursive('/tmp')
    print(result)
WilliamCanin
la source
3

Si cela ne vous dérange pas d'installer une bibliothèque d'éclairage supplémentaire, vous pouvez le faire:

pip install plazy

Usage:

import plazy

txt_filter = lambda x : True if x.endswith('.txt') else False
files = plazy.list_files(root='data', filter_func=txt_filter, is_include_root=True)

Le résultat devrait ressembler à ceci:

['data/a.txt', 'data/b.txt', 'data/sub_dir/c.txt']

Cela fonctionne à la fois sur Python 2.7 et Python 3.

Github: https://github.com/kyzas/plazy#list-files

Avertissement: je suis un auteur de plazy.

Minh Nguyen
la source
1

Cette fonction mettra récursivement uniquement les fichiers dans une liste. J'espère que ce sera vous.

import os


def ls_files(dir):
    files = list()
    for item in os.listdir(dir):
        abspath = os.path.join(dir, item)
        try:
            if os.path.isdir(abspath):
                files = files + ls_files(abspath)
            else:
                files.append(abspath)
        except FileNotFoundError as err:
            print('invalid directory\n', 'Error: ', err)
    return files
Yossarian42
la source
0

Votre solution d'origine était presque correcte, mais la variable «racine» est mise à jour dynamiquement car elle se déplace de manière récursive. os.walk () est un générateur récursif. Chaque ensemble de tuples de (racine, sous-dossier, fichiers) est destiné à une racine spécifique telle que vous l'avez configurée.

c'est à dire

root = 'C:\\'
subFolder = ['Users', 'ProgramFiles', 'ProgramFiles (x86)', 'Windows', ...]
files = ['foo1.txt', 'foo2.txt', 'foo3.txt', ...]

root = 'C:\\Users\\'
subFolder = ['UserAccount1', 'UserAccount2', ...]
files = ['bar1.txt', 'bar2.txt', 'bar3.txt', ...]

...

J'ai légèrement modifié votre code pour imprimer une liste complète.

import os
for root, subFolder, files in os.walk(PATH):
    for item in files:
        if item.endswith(".txt") :
            fileNamePath = str(os.path.join(root,item))
            print(fileNamePath)

J'espère que cela t'aides!

LastTigerEyes
la source