Listar la estructura del árbol de directorios en Python?
Por lo general, preferimos usar el árbol GNU, pero no siempre lo tenemos tree
en todos los sistemas y, a veces, Python 3 está disponible. Una buena respuesta aquí podría copiarse y pegarse fácilmente y no convertir a GNU en tree
un requisito.
tree
La salida se ve así:
$ tree
.
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
4 directories, 9 files
Creé la estructura de directorio anterior en mi directorio de inicio en un directorio al que llamo pyscratch
.
También veo otras respuestas aquí que se acercan a ese tipo de resultado, pero creo que podemos hacerlo mejor, con un código más simple y moderno y enfoques de evaluación perezosa.
Árbol en Python
Para empezar, usemos un ejemplo que
- usa el
Path
objeto Python 3
- usa las expresiones
yield
y yield from
(que crean una función generadora)
- utiliza la recursividad para una simplicidad elegante
- utiliza comentarios y algunas anotaciones de tipo para mayor claridad
from pathlib import Path
# prefix components:
space = ' '
branch = '│ '
# pointers:
tee = '├── '
last = '└── '
def tree(dir_path: Path, prefix: str=''):
"""A recursive generator, given a directory Path object
will yield a visual tree structure line by line
with each line prefixed by the same characters
"""
contents = list(dir_path.iterdir())
# contents each get pointers that are ├── with a final └── :
pointers = [tee] * (len(contents) - 1) + [last]
for pointer, path in zip(pointers, contents):
yield prefix + pointer + path.name
if path.is_dir(): # extend the prefix and recurse:
extension = branch if pointer == tee else space
# i.e. space because last, └── , above so no more |
yield from tree(path, prefix=prefix+extension)
y ahora:
for line in tree(Path.home() / 'pyscratch'):
print(line)
huellas dactilares:
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
Necesitamos materializar cada directorio en una lista porque necesitamos saber cuánto tiempo tiene, pero luego tiramos la lista. Para una recursividad amplia y profunda, esto debería ser lo suficientemente lento.
El código anterior, con los comentarios, debería ser suficiente para comprender completamente lo que estamos haciendo aquí, pero siéntase libre de revisarlo con un depurador para asimilarlo mejor si es necesario.
Más características
Ahora GNU tree
nos brinda un par de características útiles que me gustaría tener con esta función:
- imprime primero el nombre del directorio del asunto (lo hace automáticamente, el nuestro no)
- imprime el recuento de
n directories, m files
- opción para limitar la recursividad,
-L level
- opción de limitar solo a directorios,
-d
Además, cuando hay un árbol enorme, es útil limitar la iteración (por ejemplo, con islice
) para evitar bloquear su intérprete con texto, ya que en algún momento la salida se vuelve demasiado detallada para ser útil. Podemos hacer esto arbitrariamente alto por defecto, digamos 1000
.
Así que eliminemos los comentarios anteriores y completemos esta funcionalidad:
from pathlib import Path
from itertools import islice
space = ' '
branch = '│ '
tee = '├── '
last = '└── '
def tree(dir_path: Path, level: int=-1, limit_to_directories: bool=False,
length_limit: int=1000):
"""Given a directory Path object print a visual tree structure"""
dir_path = Path(dir_path) # accept string coerceable to Path
files = 0
directories = 0
def inner(dir_path: Path, prefix: str='', level=-1):
nonlocal files, directories
if not level:
return # 0, stop iterating
if limit_to_directories:
contents = [d for d in dir_path.iterdir() if d.is_dir()]
else:
contents = list(dir_path.iterdir())
pointers = [tee] * (len(contents) - 1) + [last]
for pointer, path in zip(pointers, contents):
if path.is_dir():
yield prefix + pointer + path.name
directories += 1
extension = branch if pointer == tee else space
yield from inner(path, prefix=prefix+extension, level=level-1)
elif not limit_to_directories:
yield prefix + pointer + path.name
files += 1
print(dir_path.name)
iterator = inner(dir_path, level=level)
for line in islice(iterator, length_limit):
print(line)
if next(iterator, None):
print(f'... length_limit, {length_limit}, reached, counted:')
print(f'\n{directories} directories' + (f', {files} files' if files else ''))
Y ahora podemos obtener el mismo tipo de resultado que tree
:
tree(Path.home() / 'pyscratch')
huellas dactilares:
pyscratch
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
4 directories, 9 files
Y podemos restringir a niveles:
tree(Path.home() / 'pyscratch', level=2)
huellas dactilares:
pyscratch
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ └── subpackage2
└── package2
└── __init__.py
4 directories, 3 files
Y podemos limitar la salida a directorios:
tree(Path.home() / 'pyscratch', level=2, limit_to_directories=True)
huellas dactilares:
pyscratch
├── package
│ ├── subpackage
│ └── subpackage2
└── package2
4 directories
Retrospectivo
En retrospectiva, podríamos haber utilizado path.glob
para emparejar. Quizás también podríamos usarlo path.rglob
para el globbing recursivo, pero eso requeriría una reescritura. También podríamos usaritertools.tee
lugar de materializar una lista de contenidos de directorio, pero eso podría tener compensaciones negativas y probablemente haría el código aún más complejo.
¡Los comentarios son bienvenidos!