Список дерев структури каталогів у Python?
Зазвичай ми вважаємо за краще використовувати дерево GNU, але це не завжди tree
в кожній системі, а іноді Python 3 доступний. Хороша відповідь тут може бути легко скопійована і не висуває GNU tree
вимогою.
tree
Вихідний результат виглядає приблизно так:
$ 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
Я створив вищевказану структуру каталогів у своєму домашньому каталозі під каталогом, який я викликаю pyscratch
.
Я також бачу тут інші відповіді, які підходять до такого виду, але я думаю, що ми можемо зробити краще, простішим, сучаснішим кодом та ліниво оцінюючими підходами.
Дерево в Пітоні
Для початку скористаємося прикладом
- використовує
Path
об’єкт Python 3
- використовує вирази
yield
та yield from
(які створюють функцію генератора)
- використовує рекурсію для елегантної простоти
- використовує коментарі та деякі примітки для додаткової чіткості
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)
а зараз:
for line in tree(Path.home() / 'pyscratch'):
print(line)
відбитки:
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ │ ├── __init__.py
│ │ ├── __main__.py
│ │ └── module.py
│ └── subpackage2
│ ├── __init__.py
│ ├── __main__.py
│ └── module2.py
└── package2
└── __init__.py
Нам потрібно матеріалізувати кожен каталог у список, тому що нам потрібно знати, скільки він триває, але згодом ми його викидаємо. Для глибокої та широкої рекурсії це повинно бути досить ледачим.
Вищенаведений код із коментарями повинен бути достатнім, щоб повністю зрозуміти, що ми тут робимо, але сміливо переступайте через нього налагоджувачем, щоб краще розібратися, якщо вам потрібно.
Більше можливостей
Тепер GNU tree
надає нам кілька корисних функцій, які я хотів би мати за допомогою цієї функції:
- спочатку виводить ім'я каталогу тематики (робить це автоматично, наше - ні)
- друкує кількість
n directories, m files
- можливість обмеження рекурсії,
-L level
- можливість обмежитися лише каталогами,
-d
Крім того, коли є величезне дерево, корисно обмежити ітерацію (наприклад, з islice
), щоб уникнути блокування перекладача текстом, оскільки в якийсь момент висновок стає надто багатослівним, щоб бути корисним. Ми можемо зробити це довільно високим за замовчуванням - скажімо 1000
.
Тож давайте видалимо попередні коментарі та заповніть цю функціональність:
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 ''))
І тепер ми можемо отримати такий же вихід, як tree
:
tree(Path.home() / 'pyscratch')
відбитки:
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
І ми можемо обмежитися рівнями:
tree(Path.home() / 'pyscratch', level=2)
відбитки:
pyscratch
├── package
│ ├── __init__.py
│ ├── __main__.py
│ ├── subpackage
│ └── subpackage2
└── package2
└── __init__.py
4 directories, 3 files
І ми можемо обмежити вихід на каталоги:
tree(Path.home() / 'pyscratch', level=2, limit_to_directories=True)
відбитки:
pyscratch
├── package
│ ├── subpackage
│ └── subpackage2
└── package2
4 directories
Ретроспектива
В ретроспективі ми могли б використовувати path.glob
для узгодження. Ми також можемо використовувати path.rglob
для рекурсивного глобалізації, але це вимагатиме перезапису. Ми також могли б скористатисяitertools.tee
замість того, щоб оформити список вмісту каталогів, але це може мати негативні компроміси і, ймовірно, зробить код ще складнішим.
Коментарі вітаються!