Excluyendo directorios en os.walk

148

Estoy escribiendo un script que desciende a un árbol de directorios (usando os.walk ()) y luego visita cada archivo que coincide con una determinada extensión de archivo. Sin embargo, dado que algunos de los árboles de directorios en los que se usará mi herramienta también contienen subdirectorios que a su vez contienen MUCHAS cosas inútiles (para el propósito de este script), pensé que agregaría una opción para que el usuario especifique una lista de directorios para excluir del recorrido.

Esto es bastante fácil con os.walk (). Después de todo, depende de mí decidir si realmente quiero visitar los archivos / directorios respectivos producidos por os.walk () o simplemente omitirlos. El problema es que si tengo, por ejemplo, un árbol de directorios como este:

root--
     |
     --- dirA
     |
     --- dirB
     |
     --- uselessStuff --
                       |
                       --- moreJunk
                       |
                       --- yetMoreJunk

y quiero excluir a uselessStuff y todos sus elementos secundarios, os.walk () seguirá descendiendo a todos los (potencialmente miles de) subdirectorios de uselessStuff , lo que, no hace falta decirlo, ralentiza mucho las cosas. En un mundo ideal, podría decirle a os.walk () que ni siquiera se moleste en dar más hijos de inútil Stuff , pero que yo sepa, no hay forma de hacerlo (¿existe?).

¿Alguien tiene alguna idea? ¿Tal vez hay una biblioteca de terceros que proporciona algo así?

antred
fuente

Respuestas:

243

La modificación dirs in situ eliminará los archivos y directorios (posteriores) visitados por os.walk:

# exclude = set([...])
for root, dirs, files in os.walk(top, topdown=True):
    dirs[:] = [d for d in dirs if d not in exclude]

De ayuda (os.walk):

Cuando topdown es verdadero, la persona que llama puede modificar la lista de nombres de directorio en el lugar (por ejemplo, a través de la asignación del o slice), y walk solo se repetirá en los subdirectorios cuyos nombres permanecen en los nombres de directorio; esto se puede usar para podar la búsqueda ...

unutbu
fuente
31
¿Por qué dirs[:] =?
ben
56
@ben: dirs[:] = valuemodifica dirs en el lugar . Cambia el contenido de la lista dirssin cambiar el contenedor. Como se help(os.walk)menciona, esto es necesario si desea afectar la forma en que os.walkatraviesa los subdirectorios. ( dirs = valuesimplemente reasigna (o "vincula") la variable dirsa una nueva lista, sin modificar el original dirs.)
unutbu
66
También puede usar filter():dirs[:] = list(filter(lambda x: not x in exclude, dirs))
NuclearPeon
2
@ p014k: podría escribir su propia función de generador que llame os.walky rinda root, dirs, filesdespués de excluir .git(o cualquier otra cosa que desee) dirs.
unutbu
3
@unutbu Solo para hacerle saber que en un caso, esta optimización ha reducido el tiempo de recorrido de más de 100 segundos a aproximadamente 2 segundos. Eso es lo que yo llamo una optimización que vale la pena. : D
antred
7

... una forma alternativa de la excelente respuesta de @unutbu que se lee un poco más directamente, dado que la intención es excluir directorios, a costa de O (n ** 2) vs O (n) tiempo.

( list(dirs)Se requiere hacer una copia de la lista de directorios con la ejecución correcta)

# exclude = set([...])
for root, dirs, files in os.walk(top, topdown=True):
    [dirs.remove(d) for d in list(dirs) if d in exclude]
Dmitri
fuente
55
Si quieres ser más directo a costa de algo de memoria, será mejor que escribas dirs[:] = set(dirs) - exclude. Al menos sigue siendo \ $ O (n) \ $ y no construyes una comprensión solo por sus efectos secundarios ...
301_Moved_Permanently
3
Esto no es realmente malo, pero tampoco Python idiomático en mi opinión.
Torsten Bronger
for d in list(dirs)es un poco extraño dirsYa es una lista. Y lo que tienes no es realmente una lista de comprensión. dirs.remove(d)no devuelve nada, así que terminas con una lista llena de Nones. Estoy de acuerdo con @Torsten.
seanahern