Automatización de Archivos y Directorios
pathlib y los módulos shutil/tempfile reemplazan a los frágiles comandos de shell para copiar, rotar y generar archivos. Las escrituras seguras y la codificación explícita evitan la pérdida de datos cuando los scripts se ejecutan sin supervisión bajo cron.
Receta
from pathlib import Path
import tempfile
import os
def atomic_write(path: Path, data: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=path.parent, text=True)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(data)
os.replace(tmp, path)
except Exception:
os.unlink(tmp)
raiseCuándo usar esto:
- Transformaciones de archivos por lotes (renombrar, comprimir, validar)
- Generación de informes a directorios conocidos
- Tareas de rotación y limpieza de registros
- Sincronización de artefactos entre etapas de compilación en un mismo host
Ejemplo de Trabajo
Encuentra archivos CSV, valida que no estén vacíos, escribe atómicamente un manifiesto resumen.
from __future__ import annotations
import json
import os
import tempfile
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class FileRecord:
path: Path
size: int
def discover_csv(root: Path) -> list[FileRecord]:
records: list[FileRecord] = []
for path in root.glob("**/*.csv"):
if path.is_file():
size = path.stat().st_size
if size == 0:
raise ValueError(f"csv vacío: {path}")
records.append(FileRecord(path, size))
return records
def atomic_write_json(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2)
os.replace(tmp, path)
except Exception:
os.unlink(tmp)
raise
def main(root: Path, manifest: Path) -> int:
files = discover_csv(root)
atomic_write_json(
manifest,
{"count": len(files), "files": [{"path": str(r.path), "size": r.size} for r in files]},
)
return 0
if __name__ == "__main__":
raise SystemExit(main(Path("data/incoming"), Path("data/manifest.json")))Lo que esto demuestra:
- Descubrimiento recursivo
glob("**/*.csv")con pathlib - La validación de archivo vacío falla rápidamente antes de publicar el manifiesto
os.replacerenombra atómicamente en el mismo sistema de archivos, lo que evita lecturas parciales
Inmersión Profunda
Esenciales de pathlib
| Operación | API |
|---|---|
| Unir | base / "child" |
| Existe | path.exists(), is_file() |
| Leer | read_text(encoding="utf-8") |
| Recorrer árbol | path.rglob("*") |
Patrón de Escritura Segura
- Escribir en un archivo temporal en el directorio de destino (mismo montaje)
fsyncopcional para durabilidad críticaos.replaceintercambia atómicamente en su lugar
Notas de Python
import shutil
from pathlib import Path
shutil.copy2(Path("src.txt"), Path("backup/src.txt")) # conserva metadatos
shutil.rmtree(Path("tmp/build"), ignore_errors=False)Errores Comunes
- Escribir directamente en la ruta final: los lectores ven archivos parcialmente escritos. Solución: archivo temporal atómico + reemplazo.
- Codificación predeterminada en Windows: mojibake en CSV UTF-8. Solución: siempre
encoding="utf-8"explícitamente. - Seguir enlaces simbólicos en eliminaciones: elimina destinos inesperados. Solución:
is_symlink()comprueba antes deunlink. - Carrera en
mkdir: scripts paralelos colisionan. Solución:exist_ok=Trueo capturarFileExistsError. - Árboles enormes en memoria:
list(rglob)en millones de archivos. Solución: iterar el generador sin materializar todas las rutas.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
os.walk | Mantenimiento de código heredado | Código nuevo: preferir pathlib |
Shell find | Comandos rápidos en la máquina de operaciones | Necesidad de automatización Python multiplataforma |
watchdog | Reaccionar a eventos del sistema de archivos | Las tareas nocturnas son suficientes |
Preguntas Frecuentes
¿pathlib vs os.path?
pathlib es la opción moderna predeterminada en código Python 3.14: más claro y encadenable.
¿Cómo copio árboles de directorios?
shutil.copytree(src, dst) con dirs_exist_ok=True en 3.14 para fusiones.
¿Cómo elijo la codificación?
UTF-8 para texto; open(path, "rb") para binario; nunca asumas la codificación local.
¿Escritura atómica entre sistemas de archivos?
os.replace requiere el mismo montaje; usa un nombre único + versionado a nivel de aplicación si es entre dispositivos.
¿Cómo excluyo directorios?
Filtrar path.parts o usar fnmatch en la cadena de ruta relativa durante el recorrido.
¿Patrones de eliminación segura?
Mover a un directorio de preparación primero; --dry-run lista los destinos antes de unlink.
¿Cómo funcionan los permisos?
path.chmod(0o640) después de escribir para archivos secretos; respeta umask en directorios compartidos.
¿Escritores concurrentes?
Bloqueo de archivo (fcntl/portalocker) o delegar a un trabajador de cola para garantizar un único escritor.
¿Cómo hago pruebas?
La fixture tmp_path de pytest para un directorio aislado por prueba.
¿Qué pasa con S3 en lugar de disco local?
Usa boto3 para almacenamiento de objetos; pathlib local para agentes de compilación y procesamiento de registros en disco.
Relacionados
- Conceptos Básicos de Scripting - estructura de CLI
- Scripts Robustos - dry-run e idempotencia
- Análisis de Registros y Texto - leer archivos de registro grandes
- subprocess e Interoperabilidad con Shell - tar/rsync externos
- Mejores Prácticas de Scripting de Sistema - reglas para scripts desatendidos
Versiones de la Pila: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.