Список древовидной структуры каталогов в Python?
Обычно мы предпочитаем просто использовать дерево GNU, но мы не всегда используем его tree
в каждой системе, и иногда доступен Python 3. Хороший ответ здесь можно легко скопировать и не делать tree
требования GNU .
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
.
Я также вижу здесь другие ответы, которые подходят к такому виду вывода, но я думаю, что мы можем добиться большего, используя более простой и современный код и лениво оценивая подходы.
Дерево в Python
Для начала воспользуемся примером, который
- использует
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
вместо материализации список содержимого каталогов, но это может иметь отрицательные компромиссы и, вероятно, сделает код еще более сложным.
Комментарии приветствуются!