|
| 1 | +--- |
| 2 | +slug: "papers_organizacion" |
| 3 | +id: "python-01" |
| 4 | +date: "2025-10-31" |
| 5 | +order: 1 |
| 6 | +title: "Estandarización de Nombres de Papers con Python" |
| 7 | +shortTitle: "Estandarización de Nombres de Papers con Python" |
| 8 | +author: "Eric Lucero" |
| 9 | +coverImage: https://res.cloudinary.com/dcvnw6hvt/image/upload/v1741389125/elCronopio/cover_test-2_dnehhh.jpg |
| 10 | +excerpt: "Script para organizar papers académicos automáticamente con Python." |
| 11 | +doctype: ["blog","python"] |
| 12 | +isPublic: true |
| 13 | +--- |
| 14 | + |
| 15 | +# De Caos a Orden: Cómo Automaticé la Organización de mis Papers con Python |
| 16 | + |
| 17 | +Como todo estudiante de doctorado, mi carpeta de "Papers por leer" era un caos. Nombres como `final_v2.pdf`, `1234.5678.pdf` o `articulo_importante.pdf` hacían imposible encontrar nada. Cansado de perder tiempo, decidí aplicar lo que mejor sé hacer: usar Python para construir una herramienta que pusiera orden de una vez por todas. |
| 18 | + |
| 19 | +En este post, te guiaré a través del proceso de creación de un script que: |
| 20 | +1. Vigila una carpeta de entrada (`_Inbox`). |
| 21 | +2. Extrae metadatos (Título, Autor, Año) de los archivos PDF. |
| 22 | +3. Renombra los archivos a un formato estándar y consistente. |
| 23 | +4. Lo hace de forma robusta y multiplataforma. |
| 24 | + |
| 25 | +¡Vamos a transformar ese desorden en una biblioteca organizada! |
| 26 | + |
| 27 | + |
| 28 | + |
| 29 | +## El Flujo de Trabajo: De `_Inbox` a la Gloria |
| 30 | + |
| 31 | +La idea es simple. Todos los papers nuevos que descargo van a una única carpeta llamada `_Inbox`. Luego, ejecuto mi script de Python, que los procesa y los mueve a mi biblioteca principal, ya renombrados con el formato: **`Año-Titulo-resumido-(Autor).pdf`**. |
| 32 | + |
| 33 | +## Paso 1: Extrayendo la Magia de los PDFs con `PyPDF2` |
| 34 | + |
| 35 | +El primer desafío es conseguir la información que está dentro del PDF. Para esto, usaremos la librería `PyPDF2`. Nuestro objetivo es crear una función que, dado un archivo, nos devuelva el título, el autor y el año. |
| 36 | + |
| 37 | +La clave es acceder a los **metadatos** del archivo, que es información guardada junto con el contenido principal. |
| 38 | + |
| 39 | +```python |
| 40 | +# En metadata_utils.py |
| 41 | +import PyPDF2 |
| 42 | +import logging |
| 43 | + |
| 44 | +def extract_pdf_metadata(pdf_file): |
| 45 | + """Devuelve (año, título, autor) a partir de los metadatos de un PDF.""" |
| 46 | + try: |
| 47 | + with open(pdf_file, "rb") as f: |
| 48 | + reader = PyPDF2.PdfReader(f) |
| 49 | + meta = reader.metadata |
| 50 | + if meta: |
| 51 | + # Lógica para extraer y limpiar el año del campo 'CreationDate' |
| 52 | + year = meta.get("/CreationDate", "")[2:6] |
| 53 | + |
| 54 | + # Lógica para obtener y limpiar el título |
| 55 | + title = meta.get("/Title", "NoTitle").strip() |
| 56 | + |
| 57 | + # Lógica para obtener el apellido del primer autor |
| 58 | + author_full = meta.get("/Author", "Unknown").strip() |
| 59 | + author = author_full.split(",")[0].split()[-1] |
| 60 | + |
| 61 | + return year, title, author |
| 62 | + except Exception as e: |
| 63 | + logging.warning(f"No se pudieron leer los metadatos de {pdf_file.name}: {e}") |
| 64 | + |
| 65 | + return None, None, None |
| 66 | +``` |
| 67 | +**Fundamento:** Usar un bloque `try-except` es crucial. Muchos PDFs de internet están corruptos o no tienen metadatos estándar. Nuestro script debe ser lo suficientemente robusto como para no fallar si se encuentra con uno de estos archivos. |
| 68 | + |
| 69 | +## Paso 2: El Corazón del Sistema con `pathlib` |
| 70 | + |
| 71 | +Para manejar las rutas de archivos, **olvídate de `os.path`**. La forma moderna, limpia y multiplataforma de hacerlo es con `pathlib`. Esta librería convierte las rutas de texto en objetos inteligentes. |
| 72 | + |
| 73 | +La ventaja más visible es la unión de rutas. Es tan intuitivo como esto: |
| 74 | +```python |
| 75 | +from pathlib import Path |
| 76 | + |
| 77 | +# La forma antigua y engorrosa |
| 78 | +# ruta = os.path.join(base, carpeta, archivo) |
| 79 | + |
| 80 | +# La forma moderna con pathlib |
| 81 | +ruta = Path(base) / carpeta / archivo |
| 82 | +``` |
| 83 | +Esta sintaxis no solo es más legible, sino que `pathlib` se encarga automáticamente de usar `/` o `\` según estemos en macOS/Linux o Windows. |
| 84 | + |
| 85 | +Nuestra clase principal, `PaperOrganizer`, usará `pathlib` para todas las operaciones de archivos, como buscar PDFs en el `_Inbox`, renombrarlos y moverlos. |
| 86 | + |
| 87 | +```python |
| 88 | +# En manage_names.py |
| 89 | +from pathlib import Path |
| 90 | +import sys |
| 91 | + |
| 92 | +class PaperOrganizer: |
| 93 | + def __init__(self, base_path: str): |
| 94 | + self.base_path = Path(base_path) |
| 95 | + self.inbox_path = self.base_path / "_Inbox" |
| 96 | + |
| 97 | + def rename_to_standard_format(self, filepath, year, title, author): |
| 98 | + path = Path(filepath) |
| 99 | + # Normalizamos el título para que sea un nombre de archivo válido |
| 100 | + title_normalized = title.replace(" ", "-")[:80] # Truncamos a 80 caracteres |
| 101 | + |
| 102 | + new_name = f"{year}-{title_normalized}-({author}).pdf" |
| 103 | + new_path = path.parent / new_name |
| 104 | + |
| 105 | + # El método .rename() también sirve para mover archivos |
| 106 | + path.rename(new_path) |
| 107 | + print(f"✅ Renombrado: {path.name} -> {new_name}") |
| 108 | + |
| 109 | + def add_macos_tag(self, pdf_file): |
| 110 | + """Añade una etiqueta 'Pending' solo si estamos en macOS.""" |
| 111 | + if sys.platform == "darwin": # 'darwin' es macOS |
| 112 | + try: |
| 113 | + subprocess.run(["tag", "-a", "Pending", str(pdf_file)], check=True) |
| 114 | + except FileNotFoundError: |
| 115 | + print("⚠️ Comando 'tag' no encontrado. Instálalo con 'brew install tag'") |
| 116 | + |
| 117 | +``` |
| 118 | +**Beneficios:** Al centralizar la lógica en una clase y usar `pathlib`, creamos un sistema cohesivo donde cada parte tiene una responsabilidad clara. El método `.rename()` de `pathlib` es tan versátil que sirve tanto para renombrar en el sitio como para mover el archivo a otra carpeta, simplificando nuestro código. |
| 119 | + |
| 120 | +## Paso 3: Creando una Herramienta de Terminal con `argparse` |
| 121 | + |
| 122 | +Para que nuestro script sea una verdadera herramienta, le añadimos una interfaz de línea de comandos (CLI) con `argparse`. Esto nos permite ejecutar diferentes acciones (`normalize`, `hyphenize`, etc.) de forma sencilla. |
| 123 | + |
| 124 | +Además, usamos `python-dotenv` para gestionar la configuración (como la ruta principal a nuestros papers) de forma segura en un archivo `.env`, en lugar de escribirla directamente en el código. |
| 125 | + |
| 126 | +```python |
| 127 | +# En main.py |
| 128 | +import os |
| 129 | +from pathlib import Path |
| 130 | +import argparse |
| 131 | +from dotenv import load_dotenv |
| 132 | + |
| 133 | +def main(): |
| 134 | + load_dotenv() # Carga las variables de .env |
| 135 | + parser = argparse.ArgumentParser(description="Utilidad para organizar papers.") |
| 136 | + parser.add_argument("command", choices=["normalize", "hyphenize"], help="Función a ejecutar") |
| 137 | + args = parser.parse_args() |
| 138 | + |
| 139 | + # Leemos la ruta desde el entorno y la convertimos a un objeto Path |
| 140 | + PHD_PATH = os.getenv("my_path") |
| 141 | + if not PHD_PATH: |
| 142 | + raise ValueError("La variable 'my_path' no está definida en .env") |
| 143 | + |
| 144 | + base_dir = Path(PHD_PATH) |
| 145 | + organizer = PaperOrganizer(str(base_dir)) |
| 146 | + |
| 147 | + if args.command == "normalize": |
| 148 | + organizer.batch_normalize_filenames(str(base_dir / "_Inbox")) |
| 149 | + # ... otras condiciones ... |
| 150 | + |
| 151 | +if __name__ == "__main__": |
| 152 | + main() |
| 153 | +``` |
| 154 | +**Fundamento:** Separar la configuración (`.env`) del código (`main.py`) es una práctica profesional estándar. Permite que otras personas (o tú mismo en otro ordenador) usen tu script simplemente creando su propio archivo `.env` sin tener que modificar la lógica del programa. |
| 155 | + |
| 156 | +## Conclusion: Más allá de un simple script |
| 157 | + |
| 158 | +Lo que empezó como una solución a una molestia personal se convirtió en una herramienta robusta y un excelente ejercicio de "Pythonic code". Este proyecto no solo me ahorra horas de trabajo manual, sino que también es una pieza de portafolio que demuestra mi dominio de: |
| 159 | + |
| 160 | +- **Programación Orientada a Objetos** en Python. |
| 161 | +- **Manipulación de archivos moderna y multiplataforma** con `pathlib`. |
| 162 | +- **Procesamiento de datos** con `PyPDF2` para extraer información de PDFs. |
| 163 | +- **Creación de herramientas de terminal** robustas con `argparse`. |
| 164 | +- **Buenas prácticas de desarrollo** como el manejo de errores, logging y gestión de la configuración. |
| 165 | + |
| 166 | +Espero que este desglose te sea útil y te inspire a automatizar tus propios flujos de trabajo. ¡El orden nunca había sido tan satisfactorio! |
| 167 | + |
| 168 | +Puedes encontrar el código completo de este proyecto en mi [GitHub](https://github.com/EricLuceroGonzalez/Reading_planner). |
0 commit comments