Lista struktury drzewa katalogów w Pythonie?
Zwykle wolimy po prostu używać drzewa GNU, ale nie zawsze mamy go tree
w każdym systemie, a czasami Python 3 jest dostępny. Dobra odpowiedź w tym miejscu może być łatwo skopiowana i wklejona, a GNU nie będzie tree
wymaganiem.
tree
Wynik wygląda następująco:
$ 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
Utworzyłem powyższą strukturę katalogów w moim katalogu domowym w katalogu, który wywołuję pyscratch
.
Widzę tu również inne odpowiedzi, które podchodzą do tego rodzaju wyników, ale myślę, że możemy zrobić to lepiej, stosując prostszy, nowocześniejszy kod i leniwie oceniające podejścia.
Drzewo w Pythonie
Na początek użyjmy tego przykładu
- używa
Path
obiektu Python 3
- używa wyrażeń
yield
i yield from
(które tworzą funkcję generatora)
- używa rekurencji dla eleganckiej prostoty
- używa komentarzy i niektórych adnotacji dla większej przejrzystości
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)
i teraz:
for line in tree(Path.home() / 'pyscratch'):
print(line)
wydruki:
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
Musimy zmaterializować każdy katalog w postaci listy, ponieważ musimy wiedzieć, jak długi jest, ale później wyrzucamy listę. W przypadku głębokiej i szerokiej rekurencji powinno to być wystarczająco leniwe.
Powyższy kod wraz z komentarzami powinien wystarczyć, aby w pełni zrozumieć, co tutaj robimy, ale nie krępuj się go przejść za pomocą debugera, aby lepiej go zebrać, jeśli zajdzie taka potrzeba.
Więcej funkcji
Teraz GNU tree
daje nam kilka przydatnych funkcji, które chciałbym mieć dzięki tej funkcji:
- wypisuje najpierw nazwę katalogu tematycznego (robi to automatycznie, nasz nie)
- wypisuje liczbę
n directories, m files
- możliwość ograniczenia rekursji,
-L level
- możliwość ograniczenia tylko do katalogów,
-d
Ponadto, gdy istnieje ogromne drzewo, warto ograniczyć iterację (np. Za pomocą islice
), aby uniknąć blokowania interpretera tekstem, ponieważ w pewnym momencie dane wyjściowe stają się zbyt szczegółowe, aby były przydatne. Domyślnie możemy ustawić to arbitralnie wysoko - powiedzmy 1000
.
Usuńmy więc poprzednie komentarze i wypełnijmy tę funkcjonalność:
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 ''))
Teraz możemy uzyskać takie same wyniki, jak tree
:
tree(Path.home() / 'pyscratch')
wydruki:
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
Możemy ograniczyć się do poziomów:
tree(Path.home() / 'pyscratch', level=2)
wydruki:
pyscratch
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ └── subpackage2
└── package2
└── __init__.py
4 directories, 3 files
Możemy ograniczyć dane wyjściowe do katalogów:
tree(Path.home() / 'pyscratch', level=2, limit_to_directories=True)
wydruki:
pyscratch
├── package
│ ├── subpackage
│ └── subpackage2
└── package2
4 directories
Z mocą wsteczną
Z perspektywy czasu mogliśmy użyć path.glob
do dopasowania. Moglibyśmy również użyć path.rglob
do rekurencyjnego globbingu, ale wymagałoby to przepisania. Moglibyśmy również użyć itertools.tee
zamiast materializowania listy zawartości katalogu, ale mogłoby to mieć negatywne kompromisy i prawdopodobnie uczyniłoby kod jeszcze bardziej złożonym.
Komentarze są mile widziane!