Un proveedor te manda cada mes su lista de precios en un PDF de 40 páginas. El equipo de finanzas recibe los extractos bancarios igual, y el informe de mercado que compraste la semana pasada tiene su mejor tabla en la página 17. Necesitas esas filas en una hoja de cálculo, y copiarlas a mano rompe las columnas, pierde los signos negativos y te quita una hora que volverás a perder el mes que viene.
Este artículo empieza por la pregunta que decide el método, si el archivo tiene capa de texto, y después recorre el conector PDF de Excel, pdfplumber, Camelot y tabula-py, el OCR con Tesseract para documentos escaneados y un script por lotes que convierte una carpeta de PDF en un solo CSV. Todos los ejemplos los ejecutamos sobre PDF de prueba que generamos con ReportLab.
¿Qué significa extraer datos de un PDF?
Extraer datos de un PDF es convertir un contenido maquetado para imprimir en filas y campos que puedes ordenar y cargar en otro sitio. Un PDF no guarda "una tabla de cuatro columnas". Guarda instrucciones como "dibuja 1,007.50 en esta posición con esta fuente" y "traza una línea de aquí hasta allí". Todas las herramientas de extracción de datos reconstruyen la tabla a partir de esas coordenadas, y por eso un mismo archivo sale limpio en una herramienta y desordenado en otra.
Es el mismo trabajo que sacar campos de una página web (Cómo extraer datos de una web), solo que con caracteres posicionados en lugar de etiquetas HTML. Recopilar PDF de sitios web a gran escala forma parte de un flujo más amplio de extracción de datos.
PDF de texto o PDF escaneado: ¿cómo distinguirlos?
- PDF de texto (digital). Exportado desde Word, un sistema contable o una herramienta de informes. Los caracteres se guardan como texto, así que la extracción lee los números exactos del archivo.
- PDF escaneado (imagen). Cada página es una foto de un papel. No hay texto hasta que el OCR (reconocimiento óptico de caracteres) deduce los caracteres a partir de los píxeles.
La prueba rápida: pulsa Ctrl+F y busca una palabra que ves en la página. Si la encuentra, hay capa de texto. En código, una lista page.chars vacía en pdfplumber indica que esa página es un escaneo. Algunos archivos mezclan ambos tipos (un informe digital con una página de firmas escaneada), así que comprueba página por página, no archivo por archivo.
¿Cómo funciona la extracción de tablas de un PDF?
Uses Excel, pdfplumber o Camelot, por detrás se ejecutan los mismos pasos:
- Se analiza la página. Se lee cada carácter con su posición, fuente y tamaño, además de las líneas y rectángulos dibujados.
- Los caracteres se agrupan en palabras y líneas. Los caracteres cercanos sobre la misma línea base forman palabras; las palabras a la misma altura forman una línea.
- Se localizan las zonas de tabla. A partir de líneas de reglado que forman una cuadrícula, o de columnas de palabras alineadas con espacio en blanco entre ellas.
- Se construyen las celdas. Los cruces de líneas o los huecos entre columnas definen los límites de cada celda, y cada palabra va a la celda que la contiene.
- Se devuelven las filas. Una lista de filas (o un DataFrame) que limpias y guardas.
El paso 3 causa la mayoría de los problemas. Una cuadrícula completa es fácil; una tabla sin bordes obliga a la herramienta a adivinar las columnas por el espacio en blanco, y un nombre de producto largo puede desplazar todas las columnas.
Métodos de extracción de PDF comparados
| Método | ¿Requiere código? | ¿Lee escaneos? | Ideal para | Dónde se atasca |
|---|---|---|---|---|
| Copiar y pegar | No | No | Una tabla pequeña, una sola vez | Las columnas se juntan en una celda, tablas de varias páginas |
| Excel, Obtener datos > De PDF | No | No | Pocos archivos al mes, análisis en Excel | Tablas sin bordes, muchos archivos con maquetaciones distintas |
| pdfplumber (Python) | Sí | No | Texto y tablas, trabajos por lotes, control fino | Hay que ajustarlo para tablas sin bordes |
| Camelot (Python) | Sí | Extra opcional | Tablas con reglado, informe de precisión por tabla | Texto libre alrededor de las tablas |
| tabula-py (Python) | Sí | No | DataFrames rápidos de archivos llenos de tablas | Necesita Java instalado en el equipo |
| Tesseract con pytesseract | Sí | Sí | Páginas escaneadas, fotos de documentos | La precisión depende de la calidad del escaneo; las tablas pierden su estructura |
Opciones sin código: copiar y Excel
Copiar. Para una tabla pequeña, selecciónala en el visor y pégala. Si cada fila cae en una sola celda, usa Data > Text to Columns (Datos > Texto en columnas). Deja de funcionar en cuanto un nombre de producto contiene espacios.
El conector PDF de Excel. Excel para Microsoft 365 en Windows puede leer archivos PDF a través de Power Query:
- Abre un libro en blanco y ve a Datos > Obtener datos > De un archivo > De PDF.
- Selecciona el PDF y haz clic en Abrir.
- El Navegador muestra lo que Power Query ha encontrado: cada tabla detectada (Table001, Table002…) y cada página completa (Page001…). Haz clic en un elemento para verlo.
- Elige Cargar para llevar la tabla a una hoja, o Transformar datos para limpiarla antes en el Editor de Power Query (quitar filas de encabezado repetidas, cambiar el tipo de la columna de precios a número).
- El mes siguiente, sustituye el archivo y haz clic en Data > Refresh All (Datos > Actualizar todo); los mismos pasos se ejecutan otra vez.
La documentación del conector PDF de Microsoft enumera las opciones que hay detrás: Pdf.Tables acepta StartPage y EndPage para archivos grandes y una opción MultiPageTables que une una tabla que continúa en varias páginas. El conector no aplica OCR, y Power Query en Excel para Mac no ofrece PDF como origen. El conector de carpeta combina PDF con la misma maquetación; cuando las maquetaciones difieren, un script es más fácil de mantener.
Extraer texto y tablas con pdfplumber
pdfplumber es una biblioteca de Python construida sobre pdfminer.six con su propio detector de tablas. La versión actual es la 0.11.10 (junio de 2026); su README dice que funciona mejor con PDF generados por máquina y que no hace OCR. Instálala en un entorno virtual:
python -m venv .venv
.venv\Scripts\activate # macOS/Linux: source .venv/bin/activate
pip install pdfplumber pandasLo básico cabe en unas pocas líneas:
import pdfplumber
with pdfplumber.open("pdfs/price-list-2026-08.pdf") as pdf:
page = pdf.pages[0]
print(page.extract_text()) # todo el texto, línea por línea
table = page.extract_table() # la tabla más grande de la página
print(table[:2])
# [['SKU', 'Product', 'Unit', 'Price'], ['B-001', 'Cable set 1', 'box', '13.40']]extract_tables() devuelve todas las tablas de la página como listas de cadenas de celda. Por defecto pdfplumber busca líneas dibujadas; para una tabla sin bordes, cambia a la estrategia de texto:
settings = {"vertical_strategy": "text", "horizontal_strategy": "text"}
rows = page.extract_tables(table_settings=settings)Ajusta la estrategia a la tabla. En nuestra lista de precios con reglado, la estrategia de texto partió "Cable set 1" en dos columnas y juntó "Unit" y "Price", porque adivinó por los huecos entre palabras lo que las líneas ya indicaban. Cuando solo una parte de la página es una tabla, recórtala primero con page.crop((x0, top, x1, bottom)).
Un script completo: una carpeta de PDF a un solo CSV
El script siguiente lee todos los PDF de una carpeta, toma el número de documento del texto de la primera página, saca las filas de tabla de cada página, se salta los encabezados repetidos, convierte los precios a números y escribe un único CSV. Las páginas sin capa de texto se registran para OCR, y un archivo dañado no detiene el lote.
import csv
import logging
import re
from pathlib import Path
import pdfplumber
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
IN_DIR = Path("pdfs")
OUT_CSV = Path("price_lists.csv")
HEADER = ["SKU", "Product", "Unit", "Price"]
DOC_NO = re.compile(r"price list (\S+)", re.IGNORECASE)
def to_number(text):
"""'1,007.50' -> 1007.5; returns None for empty or broken cells."""
if not text:
return None
try:
return float(text.replace(",", "").strip())
except ValueError:
return None
def rows_from_pdf(path):
with pdfplumber.open(path) as pdf:
first_text = pdf.pages[0].extract_text() or ""
match = DOC_NO.search(first_text)
doc_no = match.group(1) if match else path.stem
for page in pdf.pages:
if not page.chars: # sin capa de texto: probablemente un escaneo
logging.warning("%s p.%d has no text layer, needs OCR", path.name, page.page_number)
continue
for table in page.extract_tables():
for row in table:
cells = [(c or "").strip() for c in row]
if cells == HEADER: # encabezado repetido en cada página
continue
if len(cells) != len(HEADER):
logging.warning("%s p.%d skipped row %r", path.name, page.page_number, cells)
continue
sku, product, unit, price = cells
yield {
"file": path.name,
"doc_no": doc_no,
"page": page.page_number,
"sku": sku,
"product": product,
"unit": unit,
"price": to_number(price),
}
def main():
fields = ["file", "doc_no", "page", "sku", "product", "unit", "price"]
count = 0
with OUT_CSV.open("w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
for path in sorted(IN_DIR.glob("*.pdf")):
try:
for row in rows_from_pdf(path):
writer.writerow(row)
count += 1
except Exception as exc: # un archivo dañado no debe detener el lote
logging.error("%s failed: %s", path.name, exc)
logging.info("wrote %d rows to %s", count, OUT_CSV)
if __name__ == "__main__":
main()En nuestra carpeta de prueba (una lista de precios de 70 filas repartida en dos páginas, otra de 8 filas y una copia escaneada) la salida fue:
WARNING price-list-scan.pdf p.1 has no text layer, needs OCR
INFO wrote 78 rows to price_lists.csvutf-8-sig hace que Excel muestre bien los nombres con tilde, como "Café", y las columnas file, doc_no y page de cada fila permiten rastrear cada número hasta su página. Dónde guardar después las filas lo explicamos en Cómo guardar datos de scraping en CSV, JSON y SQLite; la limpieza, en Cómo limpiar datos de scraping con pandas en Python.
Camelot y tabula-py: cuándo usarlos
Otras dos bibliotecas se centran solo en tablas.
Camelot llegó a la versión 2.0.0 en junio de 2026. read_pdf() usa por defecto el modo lattice para tablas con reglado; stream y network se ocupan de las tablas sin bordes, y auto elige por página. Cada tabla tiene un parsing_report con una puntuación de precisión para marcar las páginas problemáticas. Consulta la documentación de Camelot.
tabula-py es un envoltorio de Python sobre tabula-java, así que necesita Java instalado. tabula.read_pdf() devuelve directamente una lista de DataFrames de pandas.
import camelot
import tabula
PDF = "pdfs/price-list-2026-09.pdf"
tables = camelot.read_pdf(PDF, pages="1-end", flavor="lattice")
for t in tables:
print(t.page, t.shape, t.parsing_report["accuracy"])
tables[0].df.to_csv("camelot_page1.csv", index=False)
dfs = tabula.read_pdf(PDF, pages="all", lattice=True)
print(len(dfs), [df.shape for df in dfs])En nuestra lista de precios de dos páginas, ambas encontraron una tabla por página. Camelot informó una precisión de 100.0 y dejó el encabezado como primera fila; tabula lo usó como nombres de columna. Las tres bibliotecas leyeron bien la tabla con reglado, así que elige según el resto del trabajo: pdfplumber si también necesitas texto libre, Camelot si quieres una puntuación por tabla y tabula-py para obtener DataFrames con el mínimo código si tienes Java instalado.
PDF escaneados: OCR con Tesseract
Cuando una página no tiene capa de texto, hay que reconocer los caracteres a partir de la imagen. Tesseract es el motor de OCR de código abierto más habitual, y pytesseract es su envoltorio para Python. El envoltorio no incluye el motor: instala antes Tesseract (si después no está en el PATH, define tesseract_cmd como se ve abajo) y luego:
pip install pytesseract pypdfium2El script renderiza cada página a 300 DPI en escala de grises, aplica OCR e imprime la confianza media por palabra, que indica qué páginas necesitan una revisión humana:
import sys
import pypdfium2 as pdfium
import pytesseract
# En Windows, indica a pytesseract dónde está el ejecutable si no está en el PATH:
# pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
def ocr_pdf(path, lang="eng", dpi=300):
pdf = pdfium.PdfDocument(path)
pages = []
for i, page in enumerate(pdf, start=1):
image = page.render(scale=dpi / 72).to_pil().convert("L") # la escala de grises ayuda
text = pytesseract.image_to_string(image, lang=lang, config="--psm 6")
pages.append(text)
# Confianza por palabra: -1 significa "no es una palabra", si no, 0-100.
data = pytesseract.image_to_data(image, lang=lang, output_type=pytesseract.Output.DICT)
confs = [float(c) for c in data["conf"] if float(c) >= 0]
if confs:
print(f"page {i}: mean confidence {sum(confs) / len(confs):.1f}")
return pages
if __name__ == "__main__":
try:
for text in ocr_pdf(sys.argv[1] if len(sys.argv) > 1 else "scans/price-list-scan.pdf"):
print(text)
except pytesseract.TesseractNotFoundError:
sys.exit("Tesseract is not installed or not on PATH")La cifra de 300 DPI procede de la guía para mejorar la calidad de Tesseract. --psm 6 trata la página como un único bloque uniforme de texto. La misma guía señala que las tablas necesitan una segmentación propia y que los bordes del escaneo pueden leerse como caracteres.
El OCR confunde 0 y O, 1 y l, y la coma y el punto de un precio; los escaneos tenues o torcidos lo empeoran. Para los números importantes, comprueba que las líneas suman el total y haz que una persona revise las páginas con baja confianza. El OCR devuelve texto, no la estructura de la tabla: reconstruye las columnas con una expresión regular por línea o prueba el extra opcional ocr de Camelot.
Dónde se usa la extracción de datos de PDF
- Listas de precios de proveedores. PDF mensuales convertidos en un historial de precios que puedes comparar en el tiempo, la base del seguimiento de precios.
- Informes de mercado y sectoriales. Tablas de informes públicos que alimentan un modelo de investigación de mercado en lugar de volver a teclearlas.
- Facturas y extractos. Totales, fechas y números de referencia que pasan a la contabilidad o a la conciliación, un trabajo habitual en los equipos de finanzas.
- Registros públicos y estadísticas. Muchos organismos siguen publicando tablas solo en PDF; un script convierte años de ellas en un único conjunto de datos, un primer paso típico antes de la minería de datos.
- Flujos ETL. Analizar PDF es la "extracción" dentro de un flujo más grande; Qué es ETL explica cómo encajan a su alrededor los pasos de transformación y carga.
Descargar muchos PDF de sitios web
Los PDF suelen estar en un portal de proveedores o en la página de publicaciones de un regulador. Descargar cientos de ellos es un trabajo de web scraping: usa la descarga oficial o la API si existe, lee el robots.txt y las condiciones de uso, y espacia las peticiones. El descargador de abajo se salta los archivos que ya tiene, no reintenta los errores 4xx, respeta Retry-After en HTTP 429 y comprueba que la respuesta sea de verdad un PDF:
import os
import time
from pathlib import Path
from urllib.parse import urlparse
import requests
PROXY = os.environ.get("PROXY_URL", "http://user:pass@pr.proxynet.io:8000")
URLS = os.environ.get("PDF_URLS", "https://example.com/reports/price-list.pdf").split()
OUT = Path("pdfs")
OUT.mkdir(exist_ok=True)
session = requests.Session()
session.proxies = {"http": PROXY, "https": PROXY}
session.headers["User-Agent"] = "price-list-archiver/1.0 (contact: data@example.com)"
def download(url, retries=3):
target = OUT / Path(urlparse(url).path).name
if target.exists(): # ya descargado en una ejecución anterior
return target
for attempt in range(1, retries + 1):
try:
r = session.get(url, timeout=30)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 30)))
continue
if 400 <= r.status_code < 500: # 403, 404...: reintentar no sirve
print(f"{url} returned {r.status_code}, skipped")
return None
r.raise_for_status()
if not r.content.startswith(b"%PDF"): # una página de error HTML, no un PDF
print(f"{url} did not return a PDF, skipped")
return None
target.write_bytes(r.content)
return target
except requests.RequestException as exc:
print(f"attempt {attempt} failed: {exc}")
time.sleep(2 ** attempt)
return None
for url in URLS:
print(url, "->", download(url))
time.sleep(2) # sé amable con el servidorA través de un proxy de prueba local con user:pass, se guardó un PDF válido, se descartaron un .pdf falso y un 404, y una segunda ejecución no volvió a descargar nada. Un proxy ayuda cuando una fuente sirve documentos distintos según el país (un Proxies residenciales con país de destino) o cuando una descarga larga de un archivo histórico choca con límites por IP (un Proxies rotativos). Aun así, lo primero es ir más despacio, como explica HTTP 429 Too Many Requests.
Problemas frecuentes y cómo resolverlos
- Las celdas combinadas vuelven como
None. pdfplumber rellena solo la primera fila de una celda combinada;ffill()de pandas copia ese valor hacia abajo (ejemplo más abajo). - La tabla sigue en la página siguiente. Extrae página por página, elimina las filas de encabezado repetidas (como hace el script de arriba) y concatena. En Excel, la opción
MultiPageTableslo hace por ti. - Los números llegan como texto.
"1,007.50","(250.00)"para negativos,"1.007,50"en un archivo alemán. Normaliza según el formato; nunca adivines el separador decimal a partir de un solo valor. - Las letras con tilde se ven rotas. Casi siempre falla el visor, no el texto: una consola de Windows, o Excel abriendo UTF-8 sin BOM (consulta errores de codificación Unicode en Python). Si el propio
extract_text()devuelve símbolos raros, la fuente no tiene mapeo Unicode y el OCR es la vía fiable. - Las columnas se desplazan en tablas sin bordes. Cambia a la estrategia de texto, recorta la zona de la tabla o pasa posiciones de columna explícitas con
"vertical_strategy": "explicit"y"explicit_vertical_lines". - Cero filas y ningún error. La página es un escaneo, la tabla es una imagen o el archivo está protegido con contraseña (
pdfplumber.open(path, password="...")para archivos que tienes derecho a abrir).
Este es el arreglo completo para celdas combinadas, probado en una tabla donde "North" ocupa dos filas:
import pandas as pd
import pdfplumber
with pdfplumber.open("merged.pdf") as pdf:
rows = pdf.pages[0].extract_table()
df = pd.DataFrame(rows[1:], columns=rows[0])
df["Region"] = df["Region"].ffill() # una celda combinada vuelve como None debajo de su primera fila
df["Price"] = pd.to_numeric(df["Price"].str.replace(",", ""), errors="coerce")
print(df)¿Qué método elegir?
| Necesidad | Recomendación |
|---|---|
| Una tabla de un archivo, una sola vez | Copiar y pegar, y después Texto en columnas |
| Pocos PDF de texto al mes, análisis en Excel | Excel, Datos > Obtener datos > De un archivo > De PDF |
| Texto y tablas de una carpeta, cada mes | Script de pdfplumber que escribe CSV |
| Tablas con reglado y puntuación de calidad por tabla | Camelot, modo lattice |
| DataFrames con el mínimo código, con Java disponible | tabula-py |
| Páginas escaneadas o fotos | Renderizar con pypdfium2 a 300 DPI + Tesseract, con revisión humana de los números |
| Cientos de PDF de un sitio web | Primero la descarga oficial o la API; si no, un descargador pausado, con proxy si la fuente es regional |
Preguntas frecuentes
¿Puedo extraer datos de un PDF gratis?
Sí. pdfplumber, Camelot, tabula-py y Tesseract son de código abierto, y el conector PDF forma parte de Excel para Microsoft 365 en Windows. Las herramientas de pago añaden sobre todo editores de plantillas y un OCR mejor para escaneos de mala calidad.
¿Cómo paso una tabla de un PDF a Excel?
En Excel para Microsoft 365 en Windows, usa Datos > Obtener datos > De un archivo > De PDF, elige la tabla en el Navegador y haz clic en Cargar. En Python, extrae las filas con pdfplumber y escríbelas con to_excel() de pandas o como un CSV que Excel abre.
¿Por qué la extracción de mi PDF devuelve texto vacío?
Lo más probable es que la página no tenga capa de texto. Revisa page.chars en pdfplumber o prueba Ctrl+F en un visor; si no encuentra nada, aplica OCR.
¿Es pdfplumber mejor que PyPDF para tablas?
Para tablas, sí. pypdf sirve para dividir, unir y leer texto plano, pero no tiene detector de tablas. pdfplumber conserva la posición de cada carácter y reconstruye las celdas a partir de líneas o alineaciones, que es lo que necesita la extracción de tablas.
¿Qué precisión tiene el OCR en PDF escaneados?
Depende del escaneo. Las páginas impresas limpias a 300 DPI suelen salir bien; los escaneos torcidos, tenues o de baja resolución, la escritura a mano y las fuentes pequeñas, no. Revisa las puntuaciones de confianza y verifica los totales.
¿Es legal extraer datos de los PDF que descargo?
Extraer datos de archivos que tienes derecho a usar (tus propias facturas, informes públicos) suele estar bien, pero el contenido puede seguir protegido por derechos de autor y los datos personales que contenga siguen sujetos a leyes como el RGPD. Lee las condiciones de la fuente antes de descargar en masa; nuestro artículo sobre la legalidad del web scraping cubre el marco general. Esto no es asesoramiento legal.
En resumen
Todo trabajo con PDF empieza con una comprobación: ¿hay capa de texto? Si la hay, el conector PDF de Excel resuelve las tablas ocasionales y un script de pdfplumber convierte una carpeta en un solo CSV, con Camelot o tabula-py para el trabajo solo con tablas. Si no la hay, renderiza a 300 DPI, aplica Tesseract y verifica los números. Guarda el archivo de origen y la página en cada fila, y trata las descargas masivas de PDF como cualquier otro trabajo de web scraping. Cuando esas descargas vienen de fuentes regionales o con límites de peticiones, nuestros planes de proxy le dan al flujo las direcciones que necesita.




