Extraer una tabla de un PDF significa recuperar sus filas y columnas como datos, una lista de filas o un DataFrame de pandas, en lugar de un montón de texto. Es más difícil de lo que parece, porque un PDF guarda caracteres en posiciones de una página y no tiene ningún concepto de fila ni de columna. Una librería tiene que deducir la tabla a partir de las líneas dibujadas alrededor de las celdas o de cómo se alinean las palabras.
Normalmente se hace para cargar cifras en una hoja de cálculo o una base de datos, para alimentar un análisis con una tabla o para darle a un modelo de lenguaje una tabla que pueda leer. Esta página ejecuta las tres librerías de Python más usadas sobre los mismos cinco PDFs ficticios, muestra el código que funcionó e informa de dónde falló cada una.
Qué necesitas
- Python 3.10 o superior.
- Un PDF con una tabla, a ser posible un documento ficticio o de ejemplo, porque los ejemplos imprimen lo que encuentran y el texto de documentos reales de clientes puede acabar en la terminal y en los registros de trabajo. Usa el más difícil que tengas: sin líneas de separación, con celdas de cabecera combinadas o con una tabla que ocupe varias páginas. Una tabla ordenada con líneas hace que cualquier librería parezca buena.
- Un entorno virtual por librería, para que sus dependencias no choquen:
pip install pdfplumber # entorno 1
pip install camelot-py # entorno 2
pip install pymupdf # entorno 3Paso 1. Comprueba que el PDF tiene capa de texto
Las tres librerías leen el texto guardado en el PDF. Un escaneo es una imagen sin capa de texto, y cada una devolvió una lista vacía con nuestro archivo escaneado sin lanzar ningún error. Comprueba primero todas las páginas, porque un PDF con texto puede tener una portada corta o una página en blanco:
import pdfplumber
with pdfplumber.open("table.pdf") as pdf:
pages_without_text = [
number
for number, page in enumerate(pdf.pages, start=1)
if len((page.extract_text() or "").strip()) <= 50
]
if pages_without_text:
print("pages with little or no text (possible scans):", pages_without_text)
else:
print("every page has a text layer")Una página de esa lista merece una mirada y no es un veredicto, porque una portada o una página en blanco también tienen poco texto. Si una página con una tabla no tiene capa de texto, nada del código de abajo funcionará y necesitas antes OCR o un parser que entienda el diseño; consulta las mejores librerías OCR de Python para ver las opciones de OCR.
Paso 2. Extrae la tabla con cada librería
pdfplumber, la más ligera de las tres, lee por defecto las tablas con líneas:
import pdfplumber
with pdfplumber.open("table.pdf") as pdf:
tables = pdf.pages[0].extract_tables()
if tables:
print(tables[0]) # a list of rows
else:
print("no table found")Camelot devuelve un DataFrame de pandas, con lattice para tablas con líneas y stream para tablas sin bordes:
import camelot
tables = camelot.read_pdf("table.pdf", pages="all", flavor="lattice")
print(len(tables))
if len(tables):
print(tables[0].df)PyMuPDF encuentra tablas con find_tables, y el resultado también se convierte en un DataFrame:
import pymupdf
doc = pymupdf.open("table.pdf")
tabs = doc[0].find_tables()
if tabs.tables:
print(tabs[0].extract()) # a list of rows
df = tabs[0].to_pandas()Qué vimos
Generamos cinco PDFs ficticios con una tabla conocida: uno con líneas de separación, uno sin ninguna, uno con celdas de cabecera combinadas y una celda de varias líneas, uno que ocupa dos páginas con la cabecera repetida y uno que es un escaneo. Comparamos cada salida celda a celda con la tabla que generamos. Las versiones fueron pdfplumber 0.11.10, Camelot 2.0.0 y PyMuPDF 1.28.2, y cada librería se ejecutó en su modo por defecto y en su modo basado en texto.
| pdfplumber | Camelot | PyMuPDF | |
|---|---|---|---|
| Tabla con líneas | Correcta | Correcta | Correcta |
| Sin líneas, modo por defecto | Vacía | Vacía | Vacía |
| Sin líneas, modo de texto | Celdas correctas más filas en blanco | Celdas correctas más una fila de título | Celdas correctas más filas en blanco |
| Cabecera combinada, celda de varias líneas | Correcta en modo por defecto; rota en modo de texto | Correcta en lattice; rota en stream | Correcta en modo por defecto; rota en modo de texto |
| Dos páginas | Dos tablas, cabecera repetida | Dos tablas, cabecera repetida | Dos tablas, cabecera repetida |
| Escaneo, sin capa de texto | Lista vacía, sin aviso | Lista vacía, sin aviso | Lista vacía, sin aviso |
Son cinco archivos pequeños, limpios y generados, así que muestra cómo falla cada librería y no dice nada de con qué frecuencia fallan con tus documentos.
Las líneas de separación lo deciden todo en los modos por defecto
El modo por defecto de cada librería es la misma idea con distinto nombre: encontrar las líneas dibujadas alrededor de las celdas y leer el texto de dentro. Con la tabla con líneas las tres acertaron. Con la tabla sin líneas las tres devolvieron nada, y ahí hay que cambiar a la estrategia basada en texto, que deduce las celdas a partir de las posiciones de las palabras.
El modo de texto funciona pero hay que limpiarlo
Con vertical_strategy y horizontal_strategy en "text", pdfplumber encontró las celdas correctas pero añadió una fila en blanco entre cada fila de la tabla (11 filas donde se esperaban 6):
import pdfplumber
with pdfplumber.open("table.pdf") as pdf:
found = pdf.pages[0].extract_tables(
{"vertical_strategy": "text", "horizontal_strategy": "text"}
)
rows = [r for r in found[0] if any(r)] if found else [] # drop the blank rowsTras eliminar las filas vacías, la tabla era correcta. La estrategia de texto de PyMuPDF dio las mismas filas en blanco, y el modo stream de Camelot añadió el título de la página como primera fila. Con la tabla de celdas de cabecera combinadas y una celda de varias líneas, los modos de texto fallaron: el texto de la celda se repartió entre filas y solo una de cinco filas salió intacta en pdfplumber y PyMuPDF. PyMuPDF además cortó "reorder monthly" en "reorder mont" en esa ejecución.
Las celdas combinadas y los saltos de página no se resuelven solos
Una cabecera combinada como "Sales" sobre dos subcolumnas sale como "Sales" seguida de una celda vacía, con las subcabeceras en una segunda fila. Una tabla que ocupa dos páginas llega como dos tablas, cada una con su propia copia de la cabecera, y ninguna de las tres librerías las une. Las piezas las concatenas tú y eliminas tú la cabecera repetida.
Un escaneo devuelve una lista vacía, no un error
Con el archivo escaneado todas las librerías devolvieron un resultado vacío sin aviso, algo fácil de pasar por alto en un pipeline. Por eso el paso 1 comprueba antes si hay capa de texto.
La misma prueba con un PDF financiero real
El vídeo ejecuta las tres librerías sobre un documento financiero real del conjunto de datos público ParseBench: dos tablas con 45 empresas, nombres de varias líneas y celdas combinadas. Se comprobó cada fila contra la verdad. Los resultados que se muestran en el vídeo fueron:
| Enfoque | Filas correctas | Qué falló |
|---|---|---|
| pdfplumber | 51,1 % | Pierde nombres en celdas combinadas |
| Camelot, lattice | 0 % | No hay líneas que detectar |
| Camelot, stream | 88,9 % | Une las dos tablas |
| PyMuPDF | 82,2 % | Desordena el texto en celdas complejas |
| anyformat | 95,6 % | 43 de 45 empresas |
Es un documento y una ejecución por enfoque, así que léelo como un patrón y no como un benchmark. El patrón coincide con la prueba sintética de arriba: las líneas de separación deciden los modos por defecto, y los modos basados en texto conservan más filas pero las dañan.
Ninguna de las tres librerías te avisa de que una fila está mal. Devuelven una tabla en cualquier caso, así que una celda dañada se ve igual que una correcta, y por eso merece la pena escribir la comprobación de abajo. anyformat devuelve la misma tabla con una puntuación de confianza por campo y el texto original de donde se leyó cada valor, que es lo que permite que una persona revise solo las filas dudosas.
Compruébalo con tus propios PDFs
Anota una fila que sepas que está en tu tabla y comprueba que aparece. La función solo informa de qué comprobación falló, nunca del contenido de las celdas, así que su salida es segura para registrar.
def check_table(table: list[list[str]], expected_row: list[str]) -> list[str]:
problems = []
if not table:
problems.append("no table found")
elif expected_row not in table:
problems.append("expected row is not intact")
return problems
print(check_table(tables[0] if tables else [], ["North", "1200", "1350", "1480"]))Ejecútala con cada librería sobre el mismo archivo y tendrás una comparación para tus documentos, que vale más que la nuestra.
Reproduce la prueba
Los cinco PDFs salen de un script corto, así que puedes repetirlo todo. Necesita pip install reportlab:
import json
from reportlab.lib.pagesizes import A4
from reportlab.platypus import Table, TableStyle, SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
hdr = ["Region", "Q1", "Q2", "Q3"]
rows = [["North", "1200", "1350", "1480"], ["South", "980", "1010", "1100"],
["East", "1500", "1620", "1710"], ["West", "760", "810", "905"],
["Total", "4440", "4790", "5195"]]
def simple(filename, data, grid):
style = [("FONTSIZE", (0, 0), (-1, -1), 11), ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
("BOTTOMPADDING", (0, 0), (-1, -1), 6), ("TOPPADDING", (0, 0), (-1, -1), 6)]
if grid:
style += [("GRID", (0, 0), (-1, -1), 0.8, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey)]
table = Table(data, colWidths=[110, 90, 90, 90])
table.setStyle(TableStyle(style))
SimpleDocTemplate(filename, pagesize=A4).build(
[Paragraph("Sales by region", getSampleStyleSheet()["Heading2"]), Spacer(1, 12), table])
simple("a_ruled.pdf", [hdr] + rows, grid=True)
simple("b_borderless.pdf", [hdr] + rows, grid=False)
merged = [["Product", "Sales", "", " Notes"], ["", "H1", "H2", ""],
["Widget", "100", "120", "Best seller\nreorder monthly"],
["Gadget", "80", "95", "Discontinued\nin Q4"], ["Gizmo", "60", "70", "New"]]
table = Table(merged, colWidths=[90, 70, 70, 150])
table.setStyle(TableStyle([("GRID", (0, 0), (-1, -1), 0.8, colors.black),
("SPAN", (1, 0), (2, 0)), ("SPAN", (0, 0), (0, 1)), ("SPAN", (3, 0), (3, 1)),
("ALIGN", (0, 0), (-1, 1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE")]))
SimpleDocTemplate("c_merged.pdf", pagesize=A4).build([table])
long = [hdr] + [[f"Item{i:02d}", str(100 + i * 7), str(200 + i * 3), str(300 + i * 11)] for i in range(1, 61)]
table = Table(long, colWidths=[110, 90, 90, 90], repeatRows=1)
table.setStyle(TableStyle([("GRID", (0, 0), (-1, -1), 0.8, colors.black),
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey), ("FONTSIZE", (0, 0), (-1, -1), 11)]))
SimpleDocTemplate("d_twopage.pdf", pagesize=A4).build([table])Para el escaneo, convierte el PDF con líneas en una imagen y guárdala de nuevo como un PDF sin capa de texto:
import pymupdf
src = pymupdf.open("a_ruled.pdf")
pix = src[0].get_pixmap(dpi=150)
out = pymupdf.open()
page = out.new_page(width=pix.width, height=pix.height)
page.insert_image(page.rect, pixmap=pix)
out.save("e_scan.pdf")Notas de instalación y licencia
Camelot es la instalación más pesada de las tres, unos 157 MB con OpenCV, frente a unos 42 MB de pdfplumber y 53 MB de PyMuPDF en nuestros entornos. Camelot 2.0.0 ya no necesita Ghostscript por defecto y convierte las páginas mediante pypdfium2. Las versiones antiguas 0.x, que la mayoría de los tutoriales siguen cubriendo, fallaron en cada llamada por defecto con "Ghostscript is not installed" en nuestra ejecución, incluso con el binario gs en la ruta, así que instala la versión actual.
pdfplumber tiene licencia MIT y Camelot también. PyMuPDF tiene licencia doble, AGPL 3.0 o una licencia comercial de Artifex, lo que importa si distribuyes software de código cerrado; lee su página de licencias para tu caso. Las tres habían publicado una versión en los últimos cuatro meses a 1 de octubre de 2026.
Cuándo no bastan estas librerías: anyformat
Estas librerías encajan con PDFs que tienen capa de texto y tablas con líneas o bien alineadas. Dejan de encajar cuando las tablas no tienen líneas, tienen celdas combinadas, celdas de varias líneas o saltos de página, o cuando los archivos son escaneos. En esos casos la gente recurre a un parser que entienda el diseño, a un modelo de lenguaje con visión o a un paso de OCR que detecte la estructura de la tabla.
El parsing de anyformat es una opción. Su resultado de parsing lista los bloques de un documento, y los bloques de tabla llevan sus filas, según la referencia del SDK. No lo ejecutamos con los cinco archivos sintéticos; en el vídeo se ejecutó con el PDF financiero real de arriba y acertó 43 de 45 empresas, que es un documento y una ejecución. Para tablas largas en concreto, consulta extracción de documentos largos.
Mira la prueba
La versión en vídeo de esta prueba dura unos cinco minutos y cubre las tres librerías y los resultados con el PDF real de arriba.
Preguntas frecuentes
¿Cuál es la mejor librería de Python para extraer tablas de un PDF?
Depende de la tabla. Con tablas con líneas en PDFs con texto, las tres funcionaron en nuestra prueba, y pdfplumber fue la más ligera de instalar. Con tablas sin líneas, ningún modo por defecto funcionó y todas necesitaron su modo de texto más limpieza.
¿Puede pdfplumber extraer tablas de un PDF escaneado?
No. Lee la capa de texto, y un escaneo no tiene, así que devuelve una lista vacía. Necesitas antes OCR o un parser que entienda el diseño.
¿Cómo paso una tabla de un PDF a un DataFrame de pandas?
Camelot devuelve uno directamente (tables[0].df), PyMuPDF convierte con to_pandas(), y con pdfplumber pasas la lista de filas a pandas.DataFrame, usando la primera fila como cabecera.
¿Por qué mi tabla sale con filas en blanco?
Las estrategias basadas en texto de pdfplumber y PyMuPDF deducen los bordes de las celdas a partir de las posiciones de las palabras y pueden añadir filas vacías entre las reales. Elimina las filas en las que todas las celdas están vacías.
¿Cómo extraigo una tabla que ocupa varias páginas?
Cada librería devuelve una tabla por página. Concatena las tablas y elimina tú mismo las filas de cabecera repetidas.







