Listar estrutura de árvore de diretório em Python?
Normalmente preferimos usar apenas a árvore GNU, mas nem sempre temos tree
em todos os sistemas e, às vezes, o Python 3 está disponível. Uma boa resposta aqui poderia ser facilmente copiada e colada e não tornar o GNU tree
um requisito.
tree
A saída de é parecida com esta:
$ 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
Criei a estrutura de diretório acima em meu diretório inicial em um diretório que chamo pyscratch
.
Também vejo outras respostas aqui que abordam esse tipo de saída, mas acho que podemos fazer melhor, com um código mais simples e moderno e abordagens de avaliação preguiçosas.
Árvore em Python
Para começar, vamos usar um exemplo que
- usa o
Path
objeto Python 3
- usa as expressões
yield
e yield from
(que criam uma função geradora)
- usa recursão para simplicidade elegante
- usa comentários e algumas anotações de tipo para maior clareza
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)
e agora:
for line in tree(Path.home() / 'pyscratch'):
print(line)
estampas:
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
Precisamos materializar cada diretório em uma lista porque precisamos saber quanto tempo ele é, mas depois jogamos a lista fora. Para uma recursão ampla e profunda, isso deve ser lento o suficiente.
O código acima, com os comentários, deve ser suficiente para entender completamente o que estamos fazendo aqui, mas sinta-se à vontade para percorrê-lo com um depurador para melhor controlá-lo se precisar.
Mais recursos
Agora, o GNU tree
nos oferece alguns recursos úteis que eu gostaria de ter com esta função:
- imprime o nome do diretório de assunto primeiro (faz isso automaticamente, o nosso não)
- imprime a contagem de
n directories, m files
- opção para limitar a recursão,
-L level
- opção de limitar apenas a diretórios,
-d
Além disso, quando há uma árvore enorme, é útil limitar a iteração (por exemplo, com islice
) para evitar travar seu interpretador com texto, pois em algum ponto a saída se torna muito prolixa para ser útil. Podemos tornar isso arbitrariamente alto por padrão - digamos 1000
.
Portanto, vamos remover os comentários anteriores e preencher esta funcionalidade:
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 ''))
E agora podemos obter o mesmo tipo de saída que tree
:
tree(Path.home() / 'pyscratch')
estampas:
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
E podemos restringir a níveis:
tree(Path.home() / 'pyscratch', level=2)
estampas:
pyscratch
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ └── subpackage2
└── package2
└── __init__.py
4 directories, 3 files
E podemos limitar a saída aos diretórios:
tree(Path.home() / 'pyscratch', level=2, limit_to_directories=True)
estampas:
pyscratch
├── package
│ ├── subpackage
│ └── subpackage2
└── package2
4 directories
Retrospectivo
Em retrospecto, poderíamos ter usado path.glob
para correspondência. Também poderíamos usar path.rglob
para globbing recursivo, mas isso exigiria uma reescrita. Também poderíamos usar em itertools.tee
vez de materializar uma lista de conteúdo de diretório, mas isso poderia ter compensações negativas e provavelmente tornaria o código ainda mais complexo.
Comentários são bem vindos!