Verzeichnisbaumstruktur in Python auflisten?
Wir bevorzugen normalerweise nur die Verwendung des GNU-Baums, haben dies jedoch nicht immer tree
auf jedem System, und manchmal ist Python 3 verfügbar. Eine gute Antwort hier könnte leicht kopiert werden und GNU nicht tree
zur Anforderung machen.
tree
Die Ausgabe sieht folgendermaßen aus:
$ 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
Ich habe die obige Verzeichnisstruktur in meinem Home-Verzeichnis unter einem von mir aufgerufenen Verzeichnis erstellt pyscratch
.
Ich sehe hier auch andere Antworten, die sich dieser Art von Ausgabe nähern, aber ich denke, wir können es besser machen, mit einfacherem, modernerem Code und trägen Bewertungen von Ansätzen.
Baum in Python
Lassen Sie uns zunächst ein Beispiel dafür verwenden
- verwendet das Python 3-
Path
Objekt
- verwendet die Ausdrücke
yield
und yield from
(die eine Generatorfunktion erstellen)
- verwendet Rekursion für elegante Einfachheit
- Verwendet Kommentare und einige Typanmerkungen für zusätzliche Klarheit
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)
und nun:
for line in tree(Path.home() / 'pyscratch'):
print(line)
Drucke:
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
Wir müssen jedes Verzeichnis in einer Liste materialisieren, weil wir wissen müssen, wie lange es dauert, aber danach werfen wir die Liste weg. Für eine tiefe und breite Rekursion sollte dies faul genug sein.
Der obige Code mit den Kommentaren sollte ausreichen, um vollständig zu verstehen, was wir hier tun. Sie können ihn jedoch auch mit einem Debugger durchgehen, um ihn bei Bedarf besser zu überprüfen.
Mehr Funktionen
Jetzt tree
bietet uns GNU einige nützliche Funktionen, die ich mit dieser Funktion haben möchte:
- druckt zuerst den Betreff-Verzeichnisnamen aus (dies geschieht automatisch, bei uns nicht)
- druckt die Anzahl von
n directories, m files
- Option zur Begrenzung der Rekursion,
-L level
- Option auf nur Verzeichnisse zu beschränken,
-d
Wenn es einen großen Baum gibt, ist es auch nützlich, die Iteration (z. B. mit islice
) zu begrenzen , um zu vermeiden, dass Ihr Interpreter mit Text blockiert wird, da die Ausgabe irgendwann zu ausführlich wird, um nützlich zu sein. Wir können dies standardmäßig beliebig hoch machen - sagen wir 1000
.
Entfernen wir also die vorherigen Kommentare und füllen Sie diese Funktion aus:
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 ''))
Und jetzt können wir die gleiche Art von Ausgabe erhalten wie tree
:
tree(Path.home() / 'pyscratch')
Drucke:
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
Und wir können uns auf Ebenen beschränken:
tree(Path.home() / 'pyscratch', level=2)
Drucke:
pyscratch
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ └── subpackage2
└── package2
└── __init__.py
4 directories, 3 files
Und wir können die Ausgabe auf Verzeichnisse beschränken:
tree(Path.home() / 'pyscratch', level=2, limit_to_directories=True)
Drucke:
pyscratch
├── package
│ ├── subpackage
│ └── subpackage2
└── package2
4 directories
Rückblick
Im Nachhinein hätten wir das path.glob
Matching verwenden können. Wir könnten es vielleicht auch path.rglob
für rekursives Globbing verwenden, aber das würde ein Umschreiben erfordern. Wir könnten auch itertools.tee
eine Liste von Verzeichnisinhalten verwenden, anstatt sie zu materialisieren, aber das könnte negative Kompromisse haben und den Code wahrscheinlich noch komplexer machen.
Kommentare sind willkommen!