Saltar al contenido

Listar la estructura del árbol de directorios en Python?

Al fin luego de mucho trabajar ya encontramos el resultado de esta duda que muchos de nuestros lectores de este sitio web han presentado. Si deseas aportar algo puedes aportar tu conocimiento.

Solución:

Aquí hay una función para hacer eso con el formato:

import os

def list_files(startpath):
    for root, dirs, files in os.walk(startpath):
        level = root.replace(startpath, '').count(os.sep)
        indent = ' ' * 4 * (level)
        print('/'.format(indent, os.path.basename(root)))
        subindent = ' ' * 4 * (level + 1)
        for f in files:
            print(''.format(subindent, f))

Similar a las respuestas anteriores, pero para python3, posiblemente legible y posiblemente extensible:

from pathlib import Path

class DisplayablePath(object):
    display_filename_prefix_middle = '├──'
    display_filename_prefix_last = '└──'
    display_parent_prefix_middle = '    '
    display_parent_prefix_last = '│   '

    def __init__(self, path, parent_path, is_last):
        self.path = Path(str(path))
        self.parent = parent_path
        self.is_last = is_last
        if self.parent:
            self.depth = self.parent.depth + 1
        else:
            self.depth = 0

    @property
    def displayname(self):
        if self.path.is_dir():
            return self.path.name + '/'
        return self.path.name

    @classmethod
    def make_tree(cls, root, parent=None, is_last=False, criteria=None):
        root = Path(str(root))
        criteria = criteria or cls._default_criteria

        displayable_root = cls(root, parent, is_last)
        yield displayable_root

        children = sorted(list(path
                               for path in root.iterdir()
                               if criteria(path)),
                          key=lambda s: str(s).lower())
        count = 1
        for path in children:
            is_last = count == len(children)
            if path.is_dir():
                yield from cls.make_tree(path,
                                         parent=displayable_root,
                                         is_last=is_last,
                                         criteria=criteria)
            else:
                yield cls(path, displayable_root, is_last)
            count += 1

    @classmethod
    def _default_criteria(cls, path):
        return True

    @property
    def displayname(self):
        if self.path.is_dir():
            return self.path.name + '/'
        return self.path.name

    def displayable(self):
        if self.parent is None:
            return self.displayname

        _filename_prefix = (self.display_filename_prefix_last
                            if self.is_last
                            else self.display_filename_prefix_middle)

        parts = ['!s !s'.format(_filename_prefix,
                                    self.displayname)]

        parent = self.parent
        while parent and parent.parent is not None:
            parts.append(self.display_parent_prefix_middle
                         if parent.is_last
                         else self.display_parent_prefix_last)
            parent = parent.parent

        return ''.join(reversed(parts))

Uso de ejemplo:

paths = DisplayablePath.make_tree(Path('doc'))
for path in paths:
    print(path.displayable())

Salida de ejemplo:

doc/
├── _static/
│   ├── embedded/
│   │   ├── deep_file
│   │   └── very/
│   │       └── deep/
│   │           └── folder/
│   │               └── very_deep_file
│   └── less_deep_file
├── about.rst
├── conf.py
└── index.rst

Notas

  • Esto usa la recursividad. Generará un RecursionError en realmente profundo árboles de carpetas
  • El árbol se evalúa perezosamente. Debería comportarse bien en realidad amplio árboles de carpetas. Sin embargo, los hijos inmediatos de una carpeta determinada no se evalúan de forma perezosa.

Editar:

  • ¡Bonificación adicional! devolución de llamada de criterios para filtrar rutas.

Listar la estructura del árbol de directorios en Python?

Por lo general, preferimos usar el árbol GNU, pero no siempre tenemos tree en todos los sistemas y, a veces, Python 3 está disponible. Una buena respuesta aquí podría copiarse y pegarse fácilmente y no hacer GNU tree un requerimiento.

treeLa salida se ve así:

$ 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

Creé la estructura de directorio anterior en mi directorio de inicio en un directorio al que llamo pyscratch.

También veo otras respuestas aquí que se acercan a ese tipo de resultado, pero creo que podemos hacerlo mejor, con un código más simple y moderno y enfoques de evaluación perezosa.

Árbol en Python

Para empezar, usemos un ejemplo que

  • usa Python 3 Path objeto
  • usa el yield y yield from expresiones (que crean una función generadora)
  • utiliza la recursividad para una simplicidad elegante
  • utiliza comentarios y algunos tipos de anotaciones para mayor claridad
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)

y ahora:

for line in tree(Path.home() / 'pyscratch'):
    print(line)

huellas dactilares:

├── package
│   ├── __init__.py
│   ├── __main__.py
│   ├── subpackage
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   └── module.py
│   └── subpackage2
│       ├── __init__.py
│       ├── __main__.py
│       └── module2.py
└── package2
    └── __init__.py

Necesitamos materializar cada directorio en una lista porque necesitamos saber cuánto tiempo tiene, pero luego tiramos la lista. Para una recursividad amplia y profunda, esto debería ser lo suficientemente perezoso.

El código anterior, con los comentarios, debería ser suficiente para comprender completamente lo que estamos haciendo aquí, pero siéntase libre de revisarlo con un depurador para asimilarlo mejor si es necesario.

Más características

Ahora GNU tree nos da un par de características útiles que me gustaría tener con esta función:

  • imprime primero el nombre del directorio del asunto (lo hace automáticamente, el nuestro no)
  • imprime el recuento de n directories, m files
  • opción para limitar la recursividad, -L level
  • opción de limitar solo a directorios, -d

Además, cuando hay un árbol enorme, es útil limitar la iteración (por ejemplo, con islice) para evitar bloquear su intérprete con texto, ya que en algún momento la salida se vuelve demasiado detallada para ser útil. Podemos hacer esto arbitrariamente alto por defecto, digamos 1000.

Así que eliminemos los comentarios anteriores y completemos esta funcionalidad:

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'ndirectories directories' + (f', files files' if files else ''))

Y ahora podemos obtener el mismo tipo de salida que tree:

tree(Path.home() / 'pyscratch')

huellas dactilares:

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

Y podemos restringir a niveles:

tree(Path.home() / 'pyscratch', level=2)

huellas dactilares:

pyscratch
├── package
│   ├── __init__.py
│   ├── __main__.py
│   ├── subpackage
│   └── subpackage2
└── package2
    └── __init__.py

4 directories, 3 files

Y podemos limitar la salida a directorios:

tree(Path.home() / 'pyscratch', level=2, limit_to_directories=True)

huellas dactilares:

pyscratch
├── package
│   ├── subpackage
│   └── subpackage2
└── package2

4 directories

Retrospectivo

En retrospectiva, podríamos haber usado path.glob para emparejar. Quizás también podríamos usar path.rglob para el globbing recursivo, pero eso requeriría una reescritura. También podríamos usar itertools.tee en lugar de materializar una lista de contenidos de directorio, pero eso podría tener compensaciones negativas y probablemente haría que el código fuera aún más complejo.

¡Los comentarios son bienvenidos!

Sección de Reseñas y Valoraciones

Eres capaz de sostener nuestro trabajo ejecutando un comentario y dejando una puntuación te damos las gracias.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *