Рекурсивный поиск подпапок и возврат файлов в списке python

119

Я работаю над сценарием, чтобы рекурсивно просматривать подпапки в основной папке и создавать список из определенного типа файлов. У меня проблема со сценарием. В настоящее время он установлен следующим образом

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

проблема в том, что переменная subFolder извлекает список вложенных папок, а не папку, в которой расположен файл ITEM. Раньше я думал запустить цикл for для подпапки и присоединиться к первой части пути, но я подумал, что Id дважды проверит, есть ли у кого-нибудь предложения до этого. Спасибо за вашу помощь!

user2709514
источник

Ответы:

157

Вы должны использовать тот, dirpathкоторый вы вызываете root. dirnamesПоставляется , так что вы можете обрезать его , если есть папки , которые вы не хотите , os.walkчтобы рекурсия в.

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

Редактировать:

После последнего отрицательного голоса мне пришло в голову, что globэто лучший инструмент для выбора по расширению.

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

Также версия генератора

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

Edit2 для Python 3.4+

from pathlib import Path
result = list(Path(".").rglob("*.[tT][xX][tT]"))
Джон Ла Рой
источник
1
Шаблон глобуса '*. [Tt] [Xx] [Tt] »сделает поиск нечувствительным к регистру.
Сергей Колесников
@SergiyKolesnikov, спасибо, я использовал это в правке внизу. Обратите внимание, что rglobэто нечувствительно на платформах Windows, но не является нечувствительным к переносимости.
Джон Ла Рой
1
@JohnLaRooy Он globтоже работает (здесь Python 3.6):glob.iglob(os.path.join(real_source_path, '**', '*.[xX][mM][lL]')
Сергей Колесников
@Sergiy: Ваш iglobне работает для файлов в подпапках или ниже. Вам нужно добавить recursive=True.
user136036
1
@ user136036, «лучше» не всегда означает самый быстрый. Иногда также важны удобочитаемость и ремонтопригодность.
Джон Ла Рой
114

Изменено в Python 3.5 : поддержка рекурсивных глобусов с использованием «**».

glob.glob()получил новый рекурсивный параметр .

Если вы хотите получить каждый .txtфайл my_path(рекурсивно включая подкаталоги):

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'

Если вам нужен итератор, вы можете использовать iglob в качестве альтернативы:

for file in glob.iglob(my_path, recursive=False):
    # ...
Ротарети
источник
1
TypeError: glob () получил неожиданный аргумент ключевого слова 'рекурсивный'
CyberJacob
1
Он должен работать. Убедитесь, что вы используете версию> = 3.5. Я добавил ссылку на документацию в своем ответе для более подробной информации.
Rotareti 06
Вот почему я на 2.7
CyberJacob
1
Почему понимание списка, а не просто files = glob.glob(PATH + '/*/**/*.txt', recursive=True)?
tobltobs
Упс! :) Это совершенно избыточно. Понятия не имею, что заставило меня написать это так. Спасибо, что упомянули об этом! Я это исправлю.
Rotareti
20

Я переведу понимание списка Джона Ла Роя на вложенный for, на всякий случай, если у кого-то еще возникнут проблемы с его пониманием.

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

Должен быть эквивалентен:

import glob

result = []

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

Вот документация по пониманию списка и функциям os.walk и glob.glob .

Джефферсон Лима
источник
1
Этот ответ работал у меня в Python 3.7.3. glob.glob(..., recursive=True)и list(Path(dir).glob(...'))не сделал.
miguelmorin 04
11

Это кажется самым быстрым решением, которое я мог придумать, и оно быстрее os.walkи намного быстрее любого globдругого решения .

  • Он также предоставит вам список всех вложенных подпапок практически бесплатно.
  • Вы можете искать несколько разных расширений.
  • Вы также можете выбрать возврат либо полных путей, либо только имен файлов, изменив f.pathна f.name(не меняйте его для вложенных папок!).

Args: dir: str, ext: list.
Функция возвращает два списка: subfolders, files.

См. Ниже подробный анализ скорости.

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


Анализ скорости

для различных методов, чтобы получить все файлы с определенным расширением во всех подпапках и в основной папке.

tl; dr:
- fast_scandirявно выигрывает и в два раза быстрее всех остальных решений, кроме os.walk.
- os.walkзанимает второе место чуть медленнее.
- использование globсильно замедлит процесс.
- Ни в одном из результатов не используется естественная сортировка . Это означает, что результаты будут отсортированы следующим образом: 1, 10, 2. Чтобы получить естественную сортировку (1, 2, 10), посетите https://stackoverflow.com/a/48030307/2441026


Полученные результаты:

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

Тесты проводились с W7x64, Python 3.8.1, 20 прогонов. 16596 файлов в 439 (частично вложенных) подпапках.
find_filesнаходится с https://stackoverflow.com/a/45646357/2441026 и позволяет искать несколько расширений.
fast_scandirбыл написан мной и также вернет список вложенных папок. Вы можете предоставить ему список расширений для поиска (я тестировал список с одной записью для простого, if ... == ".jpg"и не было существенной разницы).


# -*- 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
источник
10

Новая pathlibбиблиотека упрощает это до одной строки:

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

Вы также можете использовать версию генератора:

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

Это возвращает Pathобъекты, которые можно использовать практически для чего угодно или получить имя файла в виде строки с помощью file.name.

Эмре
источник
6

Это не самый питонический ответ, но я поставлю его здесь для удовольствия, потому что это аккуратный урок рекурсии.

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

На моей машине есть две папки rootиroot2

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

Допустим , я хочу , чтобы найти все , .txtи все .midфайлы в любом из этих каталогов, то я могу просто сделать

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']
дермен
источник
4

Рекурсивный является новым в Python 3.5, поэтому он не будет работать в Python 2.7. Вот пример, в котором используются rстроки, поэтому вам просто нужно указать путь, как в 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

Примечание: в нем будут перечислены все файлы, независимо от того, насколько глубоко они должны быть.

прости
источник
3

Вы можете сделать это таким образом, чтобы вернуть вам список файлов с абсолютными путями.

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)
УильямКанин
источник
3

Если вы не против установить дополнительную библиотеку света, вы можете сделать это:

pip install plazy

Использование:

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)

Результат должен выглядеть примерно так:

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

Он работает как на Python 2.7, так и на Python 3.

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

Отказ от ответственности: я автор plazy.

Мин Нгуен
источник
1

Эта функция будет рекурсивно помещать в список только файлы. Надеюсь, это будет у вас.

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
Йоссариан42
источник
0

Ваше исходное решение было почти правильным, но переменная «root» динамически обновляется, поскольку она рекурсивно перемещается. os.walk () - рекурсивный генератор. Каждый набор кортежей (корень, вложенная папка, файлы) предназначен для определенного корня, как вы его настроили.

т.е.

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

...

Я немного изменил ваш код, чтобы напечатать полный список.

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)

Надеюсь это поможет!

LastTigerEyes
источник