Rekurencyjne przeszukiwanie podfolderów i zwracanie plików na liście w języku Python

118

Pracuję nad skryptem, który będzie rekurencyjnie przechodził przez podfoldery w głównym folderze i budował listę z określonego typu plików. Mam problem ze skryptem. Obecnie jest ustawiony w następujący sposób

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

problem polega na tym, że zmienna subFolder pobiera listę podfolderów, a nie folder, w którym znajduje się plik ITEM. Myślałem o uruchomieniu pętli for dla podfolderu wcześniej i dołączeniu do pierwszej części ścieżki, ale pomyślałem, że dwukrotnie sprawdzę, czy ktoś ma wcześniej jakieś sugestie. Dzięki za pomoc!

user2709514
źródło

Odpowiedzi:

156

Powinieneś używać tego, do dirpathktórego dzwonisz root. dirnamesSą dostarczane dzięki czemu można przycinać, jeśli istnieją foldery, które nie życzą sobie, os.walkaby w recurse.

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']

Edytować:

Po ostatnim głosowaniu zdałem sobie sprawę, że globjest to lepsze narzędzie do wybierania przez rozszerzenie.

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

Również wersja generatora

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

Edit2 dla Pythona 3.4+

from pathlib import Path
result = list(Path(".").rglob("*.[tT][xX][tT]"))
John La Rooy
źródło
1
Wzorzec globalny „*. [Tt] [Xx] [Tt]” spowoduje, że w wyszukiwaniu nie będzie rozróżniana wielkość liter.
SergiyKolesnikov
@SergiyKolesnikov, Dzięki, użyłem tego w edycji na dole. Zauważ, że rglobjest niewrażliwy na platformach Windows - ale nie jest przenośny nieczuły.
John La Rooy
1
@JohnLaRooy Działa globteż z (Python 3.6 tutaj):glob.iglob(os.path.join(real_source_path, '**', '*.[xX][mM][lL]')
SergiyKolesnikov
@Sergiy: Twój iglobnie działa dla plików w podfolderach lub niższych. Musisz dodać recursive=True.
user136036
1
@ user136036, „lepiej” nie zawsze oznacza najszybszy. Czasami ważna jest również czytelność i łatwość konserwacji.
John La Rooy
111

Zmieniono w Pythonie 3.5 : Obsługa rekurencyjnych globów przy użyciu „**”.

glob.glob()otrzymał nowy parametr rekurencyjny .

Jeśli chcesz pobrać każdy .txtplik pod my_path(rekursywnie włączając podkatalogi):

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'

Jeśli potrzebujesz iteratora, możesz użyć iglob jako alternatywy:

for file in glob.iglob(my_path, recursive=False):
    # ...
Rotareti
źródło
1
TypeError: glob () otrzymał nieoczekiwany argument słowa kluczowego „recursive”
CyberJacob,
1
Powinno działać. Upewnij się, że używasz wersji> = 3.5. Dodałem link do dokumentacji w mojej odpowiedzi, aby uzyskać więcej szczegółów.
Rotareti
To dlatego jestem w 2.7
CyberJacob,
1
Dlaczego rozumienie listy, a nie tylko files = glob.glob(PATH + '/*/**/*.txt', recursive=True)?
tobltobs
Ups! :) Jest całkowicie zbędne. Nie mam pojęcia, dlaczego tak to napisałem. Dzięki za wspomnienie o tym! Naprawię to.
Rotareti
20

Przetłumaczę rozumienie list Johna La Rooya na zagnieżdżone for, na wypadek gdyby ktoś inny miał problemy ze zrozumieniem.

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

Powinien być równoważny z:

import glob

result = []

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

Oto dokumentacja dotycząca rozumienia list i funkcji os.walk i glob.glob .

Jefferson Lima
źródło
1
Ta odpowiedź działała dla mnie w Pythonie 3.7.3. glob.glob(..., recursive=True)i list(Path(dir).glob(...'))nie zrobił.
miguelmorin
11

To wydaje się być najszybszym rozwiązaniem mogłem wymyślić, i to szybciej niż os.walki dużo szybciej niż jakiekolwiek globrozwiązanie .

  • Zapewnia również listę wszystkich zagnieżdżonych podfolderów w zasadzie bez żadnych kosztów.
  • Możesz wyszukać kilka różnych rozszerzeń.
  • Możesz także zwrócić pełne ścieżki lub tylko nazwy plików, zmieniając je f.pathna f.name(nie zmieniaj tego w przypadku podfolderów!).

Args: dir: str, ext: list.
Funkcja zwraca dwie listy: subfolders, files.

Poniżej znajduje się szczegółowa analiza szybkości.

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"])


Analiza prędkości

różne metody pobierania wszystkich plików z określonym rozszerzeniem do wszystkich podfolderów i folderu głównego.

tl; dr:
- fast_scandirwyraźnie wygrywa i jest dwa razy szybszy niż wszystkie inne rozwiązania, z wyjątkiem os.walk.
- os.walkdrugie miejsce zajmuje nieco wolniej.
- użycie globznacznie spowolni proces.
- W żadnym z wyników nie zastosowano naturalnego sortowania . Oznacza to, że wyniki będą sortowane w następujący sposób: 1, 10, 2. Aby uzyskać naturalne sortowanie (1, 2, 10), spójrz na https://stackoverflow.com/a/48030307/2441026


Wyniki:

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

Testy zostały wykonane z W7x64, Python 3.8.1, 20 uruchomień. 16596 plików w 439 (częściowo zagnieżdżonych) podfolderach.
find_filespochodzi z https://stackoverflow.com/a/45646357/2441026 i umożliwia wyszukiwanie kilku rozszerzeń.
fast_scandirzostał napisany przeze mnie i zwróci również listę podfolderów. Możesz dać mu listę rozszerzeń do wyszukania (testowałem listę z jednym wpisem na prostą if ... == ".jpg"i nie było znaczącej różnicy).


# -*- 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
źródło
10

Nowa pathlibbiblioteka upraszcza to do jednej linii:

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

Możesz także skorzystać z wersji generatora:

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

Zwraca to Pathobiekty, których możesz użyć do prawie wszystkiego lub pobrać nazwę pliku jako ciąg znaków file.name.

Emre
źródło
6

To nie jest najbardziej pythonowa odpowiedź, ale umieszczę ją tutaj dla zabawy, ponieważ jest to zgrabna lekcja rekurencji

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

Na moim komputerze mam dwa foldery rootiroot2

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

Powiedzmy, że chcemy znaleźć wszystkie .txti wszystkie .midpliki w jednym z tych katalogów, to mogę po prostu zrobić

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
źródło
4

Cykliczne jest nowością w Pythonie 3.5, więc nie będzie działać w Pythonie 2.7. Oto przykład, który używa rciągów, więc wystarczy podać ścieżkę taką, jaka jest w 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

Uwaga: wyświetli listę wszystkich plików, bez względu na to, jak głęboko powinny się znaleźć.

prosti
źródło
3

Możesz to zrobić w ten sposób, aby zwrócić listę plików ze ścieżkami bezwzględnymi.

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
źródło
3

Jeśli nie masz nic przeciwko zainstalowaniu dodatkowej biblioteki świateł, możesz to zrobić:

pip install plazy

Stosowanie:

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)

Wynik powinien wyglądać mniej więcej tak:

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

Działa na Pythonie 2.7 i Pythonie 3.

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

Zastrzeżenie: jestem autorem plazy.

Minh Nguyen
źródło
1

Ta funkcja rekurencyjnie umieszcza tylko pliki na liście. Mam nadzieję, że tak będzie.

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
źródło
0

Twoje oryginalne rozwiązanie było prawie poprawne, ale zmienna „root” jest dynamicznie aktualizowana, gdy rekurencyjnie przechodzi dookoła. os.walk () jest generatorem rekurencyjnym. Każdy zestaw krotek (katalog główny, podfolder, pliki) jest przeznaczony dla określonego katalogu głównego, zgodnie z konfiguracją.

to znaczy

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', ...]

...

Dokonałem drobnych poprawek w Twoim kodzie, aby wydrukować pełną listę.

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)

Mam nadzieję że to pomoże!

LastTigerEyes
źródło