Búsqueda de subcarpetas recursivas y archivos de retorno en una lista de python

118

Estoy trabajando en un script para recorrer de forma recursiva subcarpetas en una carpeta principal y crear una lista a partir de un determinado tipo de archivo. Tengo un problema con el guión. Actualmente está configurado de la siguiente manera

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

el problema es que la variable subcarpeta extrae una lista de subcarpetas en lugar de la carpeta en la que se encuentra el archivo ITEM. Estaba pensando en ejecutar un bucle for para la subcarpeta antes y unirme a la primera parte de la ruta, pero pensé que volvería a verificar si alguien tiene alguna sugerencia antes de eso. ¡Gracias por tu ayuda!

user2709514
fuente

Respuestas:

156

Debería utilizar el dirpathque llama root. Se dirnamesproporcionan para que pueda podarlo si hay carpetas en las que no desea os.walkrecurrir.

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

Editar:

Después del último voto negativo, se me ocurrió que globes una mejor herramienta para seleccionar por extensión.

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

También una versión de generador

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

Edit2 para Python 3.4+

from pathlib import Path
result = list(Path(".").rglob("*.[tT][xX][tT]"))
John La Rooy
fuente
1
El patrón global '*. [Tt] [Xx] [Tt]' hará que la búsqueda no distinga entre mayúsculas y minúsculas.
SergiyKolesnikov
@SergiyKolesnikov, Gracias, lo he usado en la edición en la parte inferior. Tenga en cuenta que rglobes insensible en las plataformas de Windows, pero no es portátilmente insensible.
John La Rooy
1
@JohnLaRooy También funciona con glob(Python 3.6 aquí):glob.iglob(os.path.join(real_source_path, '**', '*.[xX][mM][lL]')
SergiyKolesnikov
@Sergiy: Tu iglobno funciona para archivos en sub-subcarpetas o debajo. Necesitas agregar recursive=True.
user136036
1
@ user136036, "mejor" no siempre significa más rápido. A veces, la legibilidad y la facilidad de mantenimiento también son importantes.
John La Rooy
111

Cambiado en Python 3.5 : Soporte para globos recursivos usando “**”.

glob.glob()obtuvo un nuevo parámetro recursivo .

Si desea obtener todos los .txtarchivos debajo my_path(recursivamente incluidos los subdirectorios):

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 necesita un iterador, puede usar iglob como alternativa:

for file in glob.iglob(my_path, recursive=False):
    # ...
Rotareti
fuente
1
TypeError: glob () obtuvo un argumento de palabra clave inesperado 'recursivo'
CyberJacob
1
Debería estar funcionando. Asegúrese de utilizar una versión> = 3.5. Agregué un enlace a la documentación en mi respuesta para obtener más detalles.
Rotareti
Por eso, estoy en 2.7
CyberJacob
1
¿Por qué la comprensión de la lista y no solo files = glob.glob(PATH + '/*/**/*.txt', recursive=True)?
tobltobs
¡Ups! :) Es totalmente redundante. No tengo idea de qué me hizo escribirlo así. ¡Gracias por mencionarlo! Lo arreglaré.
Rotareti
20

Traduciré la lista de comprensión de John La Rooy a los for anidados, en caso de que alguien más tenga problemas para entenderla.

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

Debería ser equivalente a:

import glob

result = []

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

Aquí está la documentación para la comprensión de listas y las funciones os.walk y glob.glob .

Jefferson Lima
fuente
1
Esta respuesta me funcionó en Python 3.7.3. glob.glob(..., recursive=True)y list(Path(dir).glob(...'))no lo hizo.
miguelmorin
11

Esta parece ser la solución más rápida que pude llegar a, y es más rápido que os.walky mucho más rápido que cualquier globsolución .

  • También le dará una lista de todas las subcarpetas anidadas básicamente sin costo alguno.
  • Puede buscar varias extensiones diferentes.
  • También puede elegir devolver las rutas completas o solo los nombres de los archivos cambiando f.patha f.name(¡no lo cambie para las subcarpetas!).

Args: dir: str, ext: list.
La función devuelve dos listas: subfolders, files.

Consulte a continuación para obtener un análisis detallado de la velocidad.

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


Análisis de velocidad

para varios métodos para obtener todos los archivos con una extensión de archivo específica dentro de todas las subcarpetas y la carpeta principal.

tl; dr:
- fast_scandirclaramente gana y es dos veces más rápido que todas las demás soluciones, excepto os.walk.
- os.walkes el segundo lugar un poco más lento.
- El uso globralentizará enormemente el proceso.
- Ninguno de los resultados utiliza clasificación natural . Esto significa que los resultados se ordenarán así: 1, 10, 2. Para obtener una clasificación natural (1, 2, 10), consulte https://stackoverflow.com/a/48030307/2441026


Resultados:

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

Las pruebas se realizaron con W7x64, Python 3.8.1, 20 ejecuciones. 16596 archivos en 439 subcarpetas (parcialmente anidadas).
find_fileses de https://stackoverflow.com/a/45646357/2441026 y le permite buscar varias extensiones.
fast_scandirfue escrito por mí mismo y también devolverá una lista de subcarpetas. Puede darle una lista de extensiones para buscar (probé una lista con una entrada a una simple if ... == ".jpg"y no hubo una diferencia significativa).


# -*- 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
fuente
10

La nueva pathlibbiblioteca simplifica esto a una línea:

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

También puede utilizar la versión del generador:

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

Esto devuelve Pathobjetos, que puede usar para casi cualquier cosa, u obtener el nombre del archivo como una cadena file.name.

Emre
fuente
6

No es la respuesta más pitónica, pero la pondré aquí por diversión porque es una buena lección de recursividad.

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

En mi máquina tengo dos carpetas rootyroot2

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

Digamos que quiero encontrar todos .txty cada uno de los .midarchivos en cualquiera de estos directorios, luego puedo hacer

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
fuente
4

El recursivo es nuevo en Python 3.5, por lo que no funcionará en Python 2.7. Aquí está el ejemplo que usa rcadenas, por lo que solo necesita proporcionar la ruta tal como está en 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

Nota: Enumerará todos los archivos, sin importar qué tan profundo sea.

prosti
fuente
3

Puede hacerlo de esta manera para devolverle una lista de archivos de ruta absoluta.

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
fuente
3

Si no le importa instalar una biblioteca de luces adicional, puede hacer esto:

pip install plazy

Uso:

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)

El resultado debería verse así:

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

Funciona tanto en Python 2.7 como en Python 3.

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

Descargo de responsabilidad: soy autor de plazy.

Minh Nguyen
fuente
1

Esta función colocará recursivamente solo archivos en una lista. Espero que esto te sirva.

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
fuente
0

Su solución original era casi correcta, pero la variable "raíz" se actualiza dinámicamente a medida que recorre de forma recursiva. os.walk () es un generador recursivo. Cada conjunto de tuplas de (raíz, subcarpeta, archivos) es para una raíz específica según la configuración.

es decir

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

...

Hice una pequeña modificación en su código para imprimir una lista completa.

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)

¡Espero que esto ayude!

LastTigerOjos
fuente