Python: lista de archivos en un directorio

Prefiero trabajar con Python porque es un lenguaje de programación muy flexible y me permite interactuar fácilmente con el sistema operativo. Esto también incluye...

Prefiero trabajar con Python porque es un lenguaje de programación muy flexible y me permite interactuar fácilmente con el sistema operativo. Esto también incluye funciones del sistema de archivos. Para simplemente enumerar archivos en un directorio, entran en juego los módulos os, subprocess, fnmatch y pathlib. Las siguientes soluciones demuestran cómo usar estos métodos de manera efectiva.

Usar os.walk()

El módulo os contiene una larga lista de métodos que se ocupan del sistema de archivos y del sistema operativo. Uno de ellos es walk(), que genera los nombres de archivo en un árbol de directorios recorriendo el árbol de arriba hacia abajo o de abajo hacia arriba (con la configuración predeterminada de arriba hacia abajo).

os.walk() devuelve una lista de tres elementos. Contiene el nombre del directorio raíz, una lista de los nombres de los subdirectorios y una lista de los nombres de archivo en el directorio actual. Listado 1 muestra cómo escribir esto con solo tres líneas de código. Esto funciona con los intérpretes de Python 2 y 3.

Listado 1: Recorriendo el directorio actual usando os.walk()

1
2
3
4
5
import os

for root, dirs, files in os.walk("."):
    for filename in files:
        print(filename)

Uso de la línea de comandos a través de un subproceso {#uso de la línea de comandos a través de un subproceso}

Nota: si bien esta es una forma válida de listar archivos en un directorio, no se recomienda ya que presenta la oportunidad de ataques de inyección de comandos.

Como ya se describió en el artículo Procesamiento paralelo en Python, el módulo subprocess permite ejecutar un comando del sistema y recopilar su resultado. El comando de sistema que llamamos en este caso es el siguiente:

Ejemplo 1: Listado de archivos en el directorio actual

1
$ ls -p . | grep -v /$

El comando ls -p . enumera los archivos de directorio para el directorio actual y agrega el delimitador / al final del nombre de cada subdirectorio, que necesitaremos en el siguiente paso. La salida de esta llamada se canaliza al comando grep que filtra los datos a medida que los necesitamos.

Los parámetros -v /$ excluyen todos los nombres de las entradas que terminan con el delimitador /. En realidad, /$ es una expresión regular que coincide con todas las cadenas que contienen el carácter / como el último carácter antes del final de la cadena, que está representado por $.

El módulo subprocess permite construir conductos reales y conectar los flujos de entrada y salida como lo hace en una línea de comando. Llamar al método subprocess.Popen() abre un proceso correspondiente y define los dos parámetros llamados stdin y stdout.

Listado 2 muestra cómo programar eso. La primera variable ls se define como un proceso que ejecuta ls -p. que sale a una tubería. Es por eso que el canal de salida estándar se define como subprocess.PIPE. La segunda variable grep también se define como un proceso, pero en su lugar ejecuta el comando grep -v /$.

Para leer la salida del comando ls desde la tubería, el canal estándar de grep se define como ls.stdout. Finalmente, la variable endOfPipe lee la salida de grep de grep.stdout que se imprime en el elemento estándar en el bucle for a continuación. La salida se ve en Ejemplo 2.

Listado 2: Definición de dos procesos conectados con una tubería

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import subprocess

# define the ls command
ls = subprocess.Popen(["ls", "-p", "."],
                      stdout=subprocess.PIPE,
                     )

# define the grep command
grep = subprocess.Popen(["grep", "-v", "/$"],
                        stdin=ls.stdout,
                        stdout=subprocess.PIPE,
                        )

# read from the end of the pipe (stdout)
endOfPipe = grep.stdout

# output the files line by line
for line in endOfPipe:
    print (line)

Ejemplo 2: Ejecutando el programa

1
2
3
4
5
$ python find-files3.py
find-files2.py
find-files3.py
find-files4.py
...

Esta solución funciona bastante bien con Python 2 y 3, pero ¿podemos mejorarla de alguna manera? Echemos un vistazo a las otras variantes, entonces.

Combinando os y fnmatch

Como ha visto antes, la solución que usa subprocesos es elegante pero requiere mucho código. En su lugar, combinemos los métodos de los dos módulos os y fnmatch. Esta variante también funciona con Python 2 y 3.

Como primer paso, importamos los dos módulos os y fnmatch. A continuación, definimos el directorio en el que nos gustaría listar los archivos usando os.listdir(), así como el patrón para filtrar los archivos. En un bucle for iteramos sobre la lista de entradas almacenadas en la variable listOfFiles.

Finalmente, con la ayuda de fnmatch, filtramos las entradas que estamos buscando e imprimimos las entradas coincidentes en la salida estándar. El Listado 3 contiene el script de Python y el Ejemplo 3 la salida correspondiente.

Listado 3: Listado de archivos usando el módulo os y fnmatch

1
2
3
4
5
6
7
import os, fnmatch

listOfFiles = os.listdir('.')
pattern = "*.py"
for entry in listOfFiles:
    if fnmatch.fnmatch(entry, pattern):
            print (entry)

Ejemplo 3: La salida del Listado 3

1
2
3
4
5
$ python2 find-files.py
find-files.py
find-files2.py
find-files3.py
...

Uso de os.listdir() y generadores

En términos simples, un generador es un poderoso iterador que mantiene su estado. Para obtener más información sobre los generadores, consulta uno de nuestros artículos anteriores, Generadores de Python.

La siguiente variante combina el método listdir() del módulo os con una función generadora. El código funciona con las versiones 2 y 3 de Python.

Como habrás notado antes, el método listdir() devuelve la lista de entradas para el directorio dado. El método os.path.isfile() devuelve True si la entrada dada es un archivo. El operador yield sale de la función pero mantiene el estado actual y devuelve solo el nombre de la entrada detectada como un archivo. Esto nos permite recorrer la función del generador (ver Listado 4). El resultado es idéntico al del Ejemplo 3.

Listado 4: Combinando os.listdir() y una función generadora

1
2
3
4
5
6
7
8
9
import os

def files(path):
    for file in os.listdir(path):
        if os.path.isfile(os.path.join(path, file)):
            yield file

for file in files("."):
    print (file)

Usar pathlib

El módulo pathlib se describe a sí mismo como una forma de "Analizar, compilar, probar y trabajar en nombres de archivo y rutas utilizando una API orientada a objetos en lugar de operaciones de cadena de bajo nivel". Esto suena genial, hagámoslo. A partir de Python 3, el módulo pertenece a la distribución estándar.

En Listado 5, primero definimos el directorio. El punto (".") define el directorio actual. A continuación, el método iterdir() devuelve un iterador que proporciona los nombres de todos los archivos. En un bucle for imprimimos el nombre de los archivos uno tras otro.

Listado 5: Lectura de contenidos de directorios con pathlib

1
2
3
4
5
6
7
import pathlib

# define the path
currentDirectory = pathlib.Path('.')

for currentFile in currentDirectory.iterdir():
    print(currentFile)

De nuevo, el resultado es idéntico al del Ejemplo 3.

Como alternativa, podemos recuperar archivos haciendo coincidir sus nombres de archivo usando algo llamado [globo] (https://en.wikipedia.org/wiki/Glob_(programming)). De esta forma solo podremos recuperar los archivos que queramos. Por ejemplo, en el código a continuación, solo queremos enumerar los archivos de Python en nuestro directorio, lo que hacemos especificando "*.py" en el globo.

Listado 6: Usar pathlib con el método glob

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import pathlib

# define the path
currentDirectory = pathlib.Path('.')

# define the pattern
currentPattern = "*.py"

for currentFile in currentDirectory.glob(currentPattern):
    print(currentFile)

Usando os.scandir()

En Python 3.6, un nuevo método está disponible en el módulo os. Se llama scandir() y simplifica significativamente la llamada para listar archivos en un directorio.

Habiendo importado primero el módulo os, use el método getcwd() para detectar el directorio de trabajo actual y guarde este valor en la variable path. A continuación, scandir() devuelve una lista de entradas para esta ruta, que probamos para ver si es un archivo usando el método is_file().

Listado 7: Lectura de contenidos de directorios con scandir()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import os

# detect the current working directory
path = os.getcwd()

# read the entries
with os.scandir(path) as listOfEntries:
    for entry in listOfEntries:
        # print all entries that are files
        if entry.is_file():
            print(entry.name)

De nuevo, el resultado del Listado 7 es idéntico al del Ejemplo 3.

Conclusión

Hay desacuerdo sobre qué versión es la mejor, cuál es la más elegante y cuál es la más "pythonica". Me gusta la simplicidad del método os.walk() así como el uso de los módulos fnmatch y pathlib.

Las dos versiones con los procesos/tuberías y el iterador requieren una comprensión más profunda de los procesos de UNIX y el conocimiento de Python, por lo que es posible que no sean las mejores para todos los programadores debido a su complejidad adicional (e innecesaria).

Para encontrar una respuesta a qué versión es la más rápida, el módulo timeit es muy útil. Este módulo cuenta el tiempo que ha transcurrido entre dos eventos.

Para comparar todas nuestras soluciones sin modificarlas, usamos una funcionalidad de Python: llame al intérprete de Python con el nombre del módulo y el código de Python apropiado para ejecutar. Para hacer eso para todos los scripts de Python a la vez, ayuda un script de shell (Listado 8).

Listado 8: Evaluando el tiempo de ejecución usando el módulo timeit

1
2
3
4
5
6
7
#! /bin/bash

for filename in *.py; do
    echo "$filename:"
    cat $filename | python3 -m timeit
    echo " "
done

Las pruebas se realizaron utilizando Python 3.5.3. El resultado es el siguiente, mientras que os.walk() da el mejor resultado. Ejecutar las pruebas con Python 2 devuelve diferentes valores pero no cambia el orden: os.walk() todavía está en la parte superior de la lista.

Método Resultado para 100.000.000 bucles


os.walk 0.0085 usec por ciclo subproceso/tubería 0.00859 usec por ciclo os.listdir/fnmatch 0.00912 usec por ciclo os.listdir/generator 0.00867 usec por ciclo pathlib 0.00854 usec por ciclo pathlib/glob 0.00858 usec por ciclo os.scandir 0.00856 usec por ciclo

Agradecimientos

El autor desea agradecer a Geroldo Rupprecht por su apoyo y comentarios durante la preparación de este artículo. o.

Licensed under CC BY-NC-SA 4.0