Daftar struktur pohon direktori dengan Python?
Kami biasanya lebih suka menggunakan pohon GNU, tetapi kami tidak selalu memiliki tree
pada setiap sistem, dan terkadang Python 3 tersedia. Jawaban yang baik di sini dapat dengan mudah disalin-tempel dan tidak menjadikan GNU tree
sebagai persyaratan.
tree
Outputnya terlihat seperti ini:
$ 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
Saya membuat struktur direktori di atas di direktori home saya di bawah direktori yang saya sebut pyscratch
.
Saya juga melihat jawaban lain di sini yang mendekati keluaran semacam itu, tetapi saya pikir kita bisa melakukan lebih baik, dengan kode yang lebih sederhana, lebih modern dan pendekatan evaluasi malas.
Pohon dengan Python
Untuk memulai, mari gunakan contoh itu
- menggunakan Python 3
Path
objek
- menggunakan
yield
danyield from
ekspresi (yang membuat fungsi generator)
- menggunakan rekursi untuk kesederhanaan yang elegan
- menggunakan komentar dan beberapa jenis anotasi untuk kejelasan ekstra
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)
dan sekarang:
for line in tree(Path.home() / 'pyscratch'):
print(line)
cetakan:
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
Kita memang perlu mewujudkan setiap direktori menjadi sebuah daftar karena kita perlu mengetahui berapa panjangnya, tetapi setelah itu kita membuang daftarnya. Untuk rekursi yang dalam dan luas, ini harus cukup malas.
Kode di atas, dengan komentar, seharusnya cukup untuk memahami sepenuhnya apa yang kita lakukan di sini, tetapi jangan ragu untuk melangkah melewatinya dengan debugger untuk lebih baik jika Anda perlu.
Lebih banyak fitur
Sekarang GNU tree
memberi kita beberapa fitur berguna yang saya ingin miliki dengan fungsi ini:
- mencetak nama direktori subjek terlebih dahulu (melakukannya secara otomatis, milik kami tidak)
- mencetak hitungan
n directories, m files
- opsi untuk membatasi rekursi,
-L level
- pilihan untuk membatasi hanya pada direktori,
-d
Juga, ketika ada pohon besar, akan berguna untuk membatasi iterasi (misalnya dengan islice
) untuk menghindari penguncian interpreter Anda dengan teks, karena pada titik tertentu hasilnya menjadi terlalu bertele-tele untuk digunakan. Kita dapat membuat ini menjadi tinggi secara default - katakanlah1000
.
Jadi mari kita hapus komentar sebelumnya dan isi fungsi ini:
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 ''))
Dan sekarang kita bisa mendapatkan jenis keluaran yang sama seperti tree
:
tree(Path.home() / 'pyscratch')
cetakan:
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
Dan kami dapat membatasi ke level:
tree(Path.home() / 'pyscratch', level=2)
cetakan:
pyscratch
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ └── subpackage2
└── package2
└── __init__.py
4 directories, 3 files
Dan kita dapat membatasi output ke direktori:
tree(Path.home() / 'pyscratch', level=2, limit_to_directories=True)
cetakan:
pyscratch
├── package
│ ├── subpackage
│ └── subpackage2
└── package2
4 directories
Retrospektif
Dalam retrospeksi, kita bisa digunakan path.glob
untuk pencocokan. Kita mungkin juga bisa menggunakan path.rglob
untuk rekursif globbing, tapi itu akan membutuhkan penulisan ulang. Kita juga dapat menggunakan itertools.tee
alih-alih membuat daftar isi direktori, tetapi itu bisa memiliki pengorbanan negatif dan mungkin akan membuat kode menjadi lebih kompleks.
Komentar dipersilakan!