Trabajar con PDFs en Python: pypdf, pdfplumber y ReportLab
Python ofrece varias bibliotecas para trabajar con PDFs: pypdf para leer, fusionar y dividir; pdfplumber para extraer texto y tablas con precisión; ReportLab para crear PDFs desde cero.
1. Instalación
pip install pypdf # Leer, fusionar, dividir, cifrar
pip install pdfplumber # Extracción precisa de texto y tablas
pip install reportlab # Crear PDFs desde cero
pip install fpdf2 # Alternativa sencilla a ReportLab
2. pypdf: leer y manipular PDFs
from pypdf import PdfReader, PdfWriter
# Leer un PDF
reader = PdfReader("documento.pdf")
# Información del documento
print(f"Páginas: {len(reader.pages)}")
print(f"Autor: {reader.metadata.get('/Author', 'Desconocido')}")
print(f"Título: {reader.metadata.get('/Title', 'Sin título')}")
print(f"Cifrado: {reader.is_encrypted}")
# Extraer texto de todas las páginas
texto_completo = ""
for i, pagina in enumerate(reader.pages):
texto = pagina.extract_text()
print(f"--- Página {i+1} ---")
print(texto[:200]) # Primeros 200 caracteres
texto_completo += texto + "\n"
# Extraer texto de páginas específicas
pagina1 = reader.pages[0].extract_text()
ultima = reader.pages[-1].extract_text()
3. pypdf: fusionar, dividir y reordenar
from pypdf import PdfReader, PdfWriter
# Fusionar múltiples PDFs en uno
def fusionar_pdfs(rutas_entrada, ruta_salida):
writer = PdfWriter()
for ruta in rutas_entrada:
reader = PdfReader(ruta)
for pagina in reader.pages:
writer.add_page(pagina)
with open(ruta_salida, "wb") as f:
writer.write(f)
print(f"Fusionados en: {ruta_salida}")
fusionar_pdfs(["parte1.pdf", "parte2.pdf", "parte3.pdf"], "completo.pdf")
# Extraer páginas específicas
def extraer_paginas(ruta_entrada, ruta_salida, paginas):
reader = PdfReader(ruta_entrada)
writer = PdfWriter()
for num in paginas:
writer.add_page(reader.pages[num])
with open(ruta_salida, "wb") as f:
writer.write(f)
extraer_paginas("informe.pdf", "resumen.pdf", [0, 1, 2]) # Páginas 1-3
# Dividir por página
def dividir_por_pagina(ruta_entrada, carpeta_salida):
from pathlib import Path
reader = PdfReader(ruta_entrada)
carpeta = Path(carpeta_salida)
carpeta.mkdir(exist_ok=True)
for i, pagina in enumerate(reader.pages):
writer = PdfWriter()
writer.add_page(pagina)
with open(carpeta / f"pagina_{i+1:03d}.pdf", "wb") as f:
writer.write(f)
print(f"Dividido en {len(reader.pages)} archivos")
# Rotar páginas
reader = PdfReader("horizontal.pdf")
writer = PdfWriter()
for pagina in reader.pages:
pagina.rotate(90) # 90, 180, 270 grados
writer.add_page(pagina)
with open("rotado.pdf", "wb") as f:
writer.write(f)
4. pypdf: proteger y desproteger PDFs
from pypdf import PdfReader, PdfWriter
# Cifrar PDF con contraseña
def cifrar_pdf(ruta_entrada, ruta_salida, contrasena_usuario, contrasena_propietario=None):
reader = PdfReader(ruta_entrada)
writer = PdfWriter()
for pagina in reader.pages:
writer.add_page(pagina)
writer.encrypt(
user_password=contrasena_usuario,
owner_password=contrasena_propietario or contrasena_usuario,
use_128bit=True,
)
with open(ruta_salida, "wb") as f:
writer.write(f)
cifrar_pdf("documento.pdf", "documento_protegido.pdf", "mi_contrasena")
# Leer PDF cifrado
reader = PdfReader("documento_protegido.pdf")
if reader.is_encrypted:
reader.decrypt("mi_contrasena")
print(reader.pages[0].extract_text())
5. pdfplumber: extracción precisa de texto y tablas
import pdfplumber
# Extraer texto con precisión
with pdfplumber.open("factura.pdf") as pdf:
for i, pagina in enumerate(pdf.pages):
print(f"=== Página {i+1} ===")
texto = pagina.extract_text()
if texto:
print(texto)
# Extraer tablas
with pdfplumber.open("informe_financiero.pdf") as pdf:
pagina = pdf.pages[0]
tablas = pagina.extract_tables()
for i, tabla in enumerate(tablas):
print(f"\n--- Tabla {i+1} ---")
for fila in tabla:
print(" | ".join(cell or "" for cell in fila))
# Extraer tabla específica con configuración
with pdfplumber.open("datos.pdf") as pdf:
pagina = pdf.pages[0]
tabla = pagina.extract_table({
"vertical_strategy": "lines",
"horizontal_strategy": "lines",
})
if tabla:
import csv
with open("datos.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(tabla)
print("Tabla exportada a datos.csv")
6. pdfplumber: extraer palabras y coordenadas
import pdfplumber
with pdfplumber.open("documento.pdf") as pdf:
pagina = pdf.pages[0]
# Palabras con coordenadas (x0, y0, x1, y1)
palabras = pagina.extract_words()
for palabra in palabras[:10]:
print(f"'{palabra['text']}' en ({palabra['x0']:.0f}, {palabra['top']:.0f})")
# Recortar una región de la página (bounding box)
region = pagina.crop((50, 100, 550, 400)) # (x0, y0, x1, y1)
texto_region = region.extract_text()
print(texto_region)
# Buscar texto en el PDF
coincidencias = pagina.search("Total:")
for m in coincidencias:
print(f"'Total:' encontrado en ({m['x0']:.0f}, {m['top']:.0f})")
7. ReportLab: crear PDFs desde cero
from reportlab.lib.pagesizes import A4, LETTER
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
from reportlab.lib.enums import TA_CENTER, TA_LEFT
def crear_factura(ruta_salida):
doc = SimpleDocTemplate(ruta_salida, pagesize=A4)
estilos = getSampleStyleSheet()
elementos = []
# Título
estilo_titulo = ParagraphStyle(
"Titulo",
parent=estilos["Heading1"],
alignment=TA_CENTER,
textColor=colors.HexColor("#1565C0"),
fontSize=24,
)
elementos.append(Paragraph("FACTURA #2025-001", estilo_titulo))
elementos.append(Spacer(1, 0.5*cm))
# Datos empresa
elementos.append(Paragraph("Mi Empresa S.L.", estilos["Heading2"]))
elementos.append(Paragraph("Calle Mayor 1, Madrid | CIF: B12345678", estilos["Normal"]))
elementos.append(Spacer(1, 1*cm))
# Tabla de productos
datos_tabla = [
["Concepto", "Cantidad", "Precio", "Total"],
["Desarrollo web", "1", "2.000,00 €", "2.000,00 €"],
["Diseño gráfico", "3", "500,00 €", "1.500,00 €"],
["Mantenimiento", "12", "150,00 €", "1.800,00 €"],
["", "", "TOTAL:", "5.300,00 €"],
]
tabla = Table(datos_tabla, colWidths=[8*cm, 3*cm, 4*cm, 4*cm])
tabla.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1565C0")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, 0), 12),
("BOTTOMPADDING", (0, 0), (-1, 0), 12),
("BACKGROUND", (0, 1), (-1, -2), colors.HexColor("#E3F2FD")),
("ROWBACKGROUNDS", (0, 1), (-1, -2), [colors.white, colors.HexColor("#E3F2FD")]),
("GRID", (0, 0), (-1, -2), 1, colors.lightgrey),
("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
("FONTSIZE", (0, -1), (-1, -1), 12),
]))
elementos.append(tabla)
elementos.append(Spacer(1, 1*cm))
# Nota al pie
elementos.append(Paragraph(
"Pago a 30 días. IBAN: ES12 3456 7890 1234 5678 9012",
estilos["Italic"]
))
doc.build(elementos)
print(f"Factura creada: {ruta_salida}")
crear_factura("factura.pdf")
8. fpdf2: alternativa sencilla para PDFs simples
from fpdf import FPDF
class PDF(FPDF):
def header(self):
self.set_font("Helvetica", "B", 14)
self.cell(0, 10, "Informe de Conversiones", align="C")
self.ln(5)
def footer(self):
self.set_y(-15)
self.set_font("Helvetica", "I", 8)
self.cell(0, 10, f"Página {self.page_no()}", align="C")
pdf = PDF()
pdf.add_page()
pdf.set_font("Helvetica", size=12)
# Tabla simple
pdf.set_fill_color(21, 101, 192)
pdf.set_text_color(255, 255, 255)
pdf.set_font("Helvetica", "B", 12)
for col, ancho in [("Formato", 50), ("Conversiones", 60), ("Tamaño medio", 60)]:
pdf.cell(ancho, 10, col, border=1, fill=True)
pdf.ln()
pdf.set_text_color(0, 0, 0)
pdf.set_font("Helvetica", size=11)
datos = [("JPG → WebP", "12.345", "1.2 MB"), ("PDF → DOCX", "8.901", "450 KB")]
for fila in datos:
for texto, ancho in zip(fila, [50, 60, 60]):
pdf.cell(ancho, 9, texto, border=1)
pdf.ln()
pdf.output("informe.pdf")
print("PDF creado con fpdf2")
9. Buenas prácticas
- pypdf para operaciones estructurales: fusionar, dividir, rotar, cifrar.
- pdfplumber para extracción: mejor precisión en texto y tablas que pypdf.
- ReportLab para generación: documentos complejos con estilos, tablas e imágenes.
- fpdf2 para generación simple: curva de aprendizaje menor que ReportLab.
- No todos los PDFs son iguales: PDFs con texto real son fáciles de extraer; PDFs de imágenes necesitan OCR (pytesseract).
- Cierra siempre los archivos: usa
with open(...)opdfplumber.open(...)con context manager.
Conversiones relacionadas
Conversiones de documento que siguen este tema: