Elenca la struttura ad albero delle directory in Python?
Di solito preferiamo usare solo l'albero GNU, ma non lo abbiamo sempre tree
su tutti i sistemi e talvolta è disponibile Python 3. Una buona risposta qui potrebbe essere facilmente copiata e incollata e non rendere GNU tree
un requisito.
tree
L'output di è simile a questo:
$ 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
Ho creato la struttura di directory sopra nella mia directory home sotto una directory che chiamo pyscratch
.
Vedo anche altre risposte qui che si avvicinano a quel tipo di output, ma penso che possiamo fare di meglio, con un codice più semplice e più moderno e approcci di valutazione pigramente.
Albero in Python
Per cominciare, usiamo un esempio che
- usa l'
Path
oggetto Python 3
- usa le espressioni
yield
e yield from
(che creano una funzione generatore)
- usa la ricorsione per un'elegante semplicità
- utilizza commenti e alcune annotazioni di tipo per maggiore chiarezza
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 adesso:
for line in tree(Path.home() / 'pyscratch'):
print(line)
stampe:
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
Dobbiamo materializzare ogni directory in un elenco perché abbiamo bisogno di sapere quanto è lungo, ma in seguito buttiamo via l'elenco. Per una ricorsione ampia e profonda questo dovrebbe essere abbastanza pigro.
Il codice sopra, con i commenti, dovrebbe essere sufficiente per comprendere appieno cosa stiamo facendo qui, ma sentiti libero di esaminarlo con un debugger per elaborarlo meglio se necessario.
Più funzionalità
Ora GNU tree
ci offre un paio di funzioni utili che mi piacerebbe avere con questa funzione:
- stampa prima il nome della directory del soggetto (lo fa automaticamente, il nostro no)
- stampa il conteggio di
n directories, m files
- opzione per limitare la ricorsione,
-L level
- opzione per limitare solo le directory,
-d
Inoltre, quando c'è un albero enorme, è utile limitare l'iterazione (ad esempio con islice
) per evitare di bloccare l'interprete con il testo, poiché a un certo punto l'output diventa troppo prolisso per essere utile. Possiamo renderlo arbitrariamente alto per impostazione predefinita, diciamo 1000
.
Quindi rimuoviamo i commenti precedenti e compiliamo questa funzionalità:
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 ora possiamo ottenere lo stesso tipo di output di tree
:
tree(Path.home() / 'pyscratch')
stampe:
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 possiamo limitarci a livelli:
tree(Path.home() / 'pyscratch', level=2)
stampe:
pyscratch
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ └── subpackage2
└── package2
└── __init__.py
4 directories, 3 files
E possiamo limitare l'output alle directory:
tree(Path.home() / 'pyscratch', level=2, limit_to_directories=True)
stampe:
pyscratch
├── package
│ ├── subpackage
│ └── subpackage2
└── package2
4 directories
Retrospettiva
In retrospettiva, avremmo potuto utilizzare path.glob
per la corrispondenza. Potremmo anche usarlo path.rglob
per il globbing ricorsivo, ma ciò richiederebbe una riscrittura. Potremmo anche usare itertools.tee
invece di materializzare un elenco di contenuti di directory, ma ciò potrebbe avere compromessi negativi e probabilmente renderebbe il codice ancora più complesso.
I commenti sono ben accetti!