PDF a Markdown en Python
La conversión de PDF a Markdown es uno de los pasos más importantes del procesamiento moderno de documentos. Ya sea que estés construyendo una aplicación basada en LLM, una pipeline RAG o simplemente quieras archivar documentos en un formato legible, convertir PDF a Markdown en Python te da una salida estructurada y portable que funciona en cualquier parte.
¿Por qué convertir PDF a Markdown?
Markdown se ha convertido en el formato de intercambio estándar para los flujos de trabajo de IA y de documentos. Estas son las razones que hacen que convenga la conversión:
Los LLMs rinden mejor con texto estructurado. Modelos grandes de lenguaje como GPT-4, Claude o Llama producen resultados notablemente mejores cuando reciben Markdown limpio en lugar de texto bruto extraído. Los encabezados le dan al modelo un mapa del documento, y los formatos como negrita y cursiva cargan un significado semántico que el texto plano descarta.
Las pipelines RAG necesitan texto limpio, troceado y con los encabezados intactos. Los sistemas de generación aumentada por recuperación dividen los documentos en fragmentos, los embeben y recuperan los más relevantes al momento de la consulta. Los encabezados de Markdown son límites de chunk naturales: partir por ## te da secciones semánticamente coherentes y cada chunk viene con un título listo. La extracción de texto plano pierde esos límites por completo y te obliga a depender de heurísticas como la longitud del párrafo o el número de oraciones.
Markdown preserva la estructura del documento manteniendo texto plano. Encabezados, listas con viñetas, listas numeradas, tablas, negrita y cursiva sobreviven a la conversión en un formato legible por humanos y parseable por máquinas. Un archivo Markdown es, al final, un archivo de texto: funciona con control de versiones, con búsqueda de texto y con cualquier lenguaje de programación.
Las alternativas son peores. La extracción de texto plano pierde toda la estructura: los encabezados se confunden con el cuerpo, las tablas se derrumban en líneas mezcladas y las listas pierden su jerarquía. La conversión a HTML preserva la estructura pero suma muchísimo volumen: un archivo de 2 KB en Markdown puede convertirse en 15 KB de HTML con <div> anidados, clases CSS y entidades escapadas. Markdown encuentra el punto dulce: estructurado, ligero y con soporte universal.
Inicio rápido
Convierte una página de PDF a Markdown limpio en tres líneas:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)
WASM
import { WasmPdfDocument } from "pdf-oxide-wasm";
const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
console.log(md);
doc.free();
Rust
use pdf_oxide::PdfDocument;
let mut doc = PdfDocument::open("paper.pdf")?;
let md = doc.to_markdown(0, true)?;
println!("{}", md);
Go
package main
import (
"fmt"
"log"
pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)
func main() {
doc, err := pdfoxide.Open("paper.pdf")
if err != nil { log.Fatal(err) }
defer doc.Close()
md, err := doc.ToMarkdown(0)
if err != nil { log.Fatal(err) }
fmt.Println(md)
}
C#
using PdfOxide;
using var doc = PdfDocument.Open("paper.pdf");
Console.WriteLine(doc.ToMarkdown(0));
PDF Oxide detecta los encabezados por clusters de tamaño de fuente, conserva el formato en negrita y cursiva, convierte las tablas a sintaxis GFM y, si lo pides, incrusta las imágenes. Ninguna otra biblioteca de Python para PDFs ofrece conversión a Markdown integrada.
Instalación
pip install pdf_oxide
Convertir el documento completo
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("book.pdf")
md = doc.to_markdown_all(detect_headings=True)
with open("book.md", "w", encoding="utf-8") as f:
f.write(md)
WASM
const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdownAll();
console.log(md);
doc.free();
Rust
let mut doc = PdfDocument::open("book.pdf")?;
let md = doc.to_markdown_all(true)?;
std::fs::write("book.md", &md)?;
Go
doc, _ := pdfoxide.Open("book.pdf")
defer doc.Close()
md, _ := doc.ToMarkdownAll()
_ = os.WriteFile("book.md", []byte(md), 0644)
C#
using var doc = PdfDocument.Open("book.pdf");
File.WriteAllText("book.md", doc.ToMarkdownAll());
to_markdown_all() convierte cada página y las une con separadores ---.
Opciones de conversión
| Parámetro | Por defecto | Descripción |
|---|---|---|
detect_headings |
True |
Mapea tamaños de fuente a encabezados #, ##, ### |
preserve_layout |
False |
Preserva la posición visual |
include_images |
True |
Incluye imágenes en la salida |
embed_images |
True |
Las incrusta como data URIs en base64 |
image_output_dir |
None |
En su lugar, guarda las imágenes en este directorio |
Sólo encabezados (sin imágenes)
doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True, include_images=False)
Guardar las imágenes en un directorio
doc = PdfDocument("report.pdf")
md = doc.to_markdown(0,
detect_headings=True,
embed_images=False,
image_output_dir="output/images"
)
with open("output/report.md", "w") as f:
f.write(md)
Integración con pipelines RAG y LLM
Markdown es el formato ideal para pipelines RAG. Los encabezados marcan fronteras naturales entre chunks y la estructura preserva un significado que el texto plano pierde.
Chunking por encabezado
Python
from pdf_oxide import PdfDocument
import re
doc = PdfDocument("paper.pdf")
md = doc.to_markdown_all(detect_headings=True)
# Parte por encabezados para un chunking semantico
chunks = re.split(r'\n(?=#{1,3} )', md)
chunks = [chunk.strip() for chunk in chunks if chunk.strip()]
for i, chunk in enumerate(chunks):
print(f"Chunk {i}: {chunk[:80]}...")
WASM
const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdownAll();
// Parte por encabezados para un chunking semantico
const chunks = md.split(/\n(?=#{1,3} )/).filter(c => c.trim());
chunks.forEach((chunk, i) => {
console.log(`Chunk ${i}: ${chunk.slice(0, 80)}...`);
});
doc.free();
Rust
let mut doc = PdfDocument::open("paper.pdf")?;
let md = doc.to_markdown_all(true)?;
let chunks: Vec<&str> = md.split("\n#")
.map(|c| c.trim())
.filter(|c| !c.is_empty())
.collect();
for (i, chunk) in chunks.iter().enumerate() {
println!("Chunk {}: {}...", i, &chunk[..chunk.len().min(80)]);
}
Go
doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()
md, _ := doc.ToMarkdownAll()
re := regexp.MustCompile(`\n(?=#{1,3} )`)
for i, chunk := range re.Split(md, -1) {
chunk = strings.TrimSpace(chunk)
if chunk == "" { continue }
if len(chunk) > 80 { chunk = chunk[:80] }
fmt.Printf("Chunk %d: %s...\n", i, chunk)
}
C#
using var doc = PdfDocument.Open("paper.pdf");
var md = doc.ToMarkdownAll();
var chunks = Regex.Split(md, @"\n(?=#{1,3} )")
.Select(c => c.Trim())
.Where(c => c.Length > 0)
.ToList();
for (int i = 0; i < chunks.Count; i++)
{
var preview = chunks[i].Length > 80 ? chunks[i][..80] : chunks[i];
Console.WriteLine($"Chunk {i}: {preview}...");
}
Chunking a nivel de página
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("report.pdf")
chunks = []
for i in range(doc.page_count()):
md = doc.to_markdown(i, detect_headings=True, include_images=False)
chunks.append({
"page": i,
"content": md,
"source": "report.pdf"
})
WASM
const doc = new WasmPdfDocument(bytes);
const chunks = [];
for (let i = 0; i < doc.pageCount(); i++) {
const md = doc.toMarkdown(i);
chunks.push({ page: i, content: md, source: "report.pdf" });
}
doc.free();
Rust
let mut doc = PdfDocument::open("report.pdf")?;
let mut chunks = Vec::new();
for i in 0..doc.page_count()? {
let md = doc.to_markdown(i, true)?;
chunks.push((i, md));
}
Go
doc, _ := pdfoxide.Open("report.pdf")
defer doc.Close()
type Chunk struct {
Page int
Content string
Source string
}
n, _ := doc.PageCount()
chunks := make([]Chunk, 0, n)
for i := 0; i < n; i++ {
md, _ := doc.ToMarkdown(i)
chunks = append(chunks, Chunk{Page: i, Content: md, Source: "report.pdf"})
}
C#
using var doc = PdfDocument.Open("report.pdf");
var chunks = Enumerable.Range(0, doc.PageCount)
.Select(i => new { Page = i, Content = doc.ToMarkdown(i), Source = "report.pdf" })
.ToList();
Conversión en lote para base de datos vectorial
from pdf_oxide import PdfDocument, PdfError
from pathlib import Path
pdf_dir = Path("documents/")
documents = []
for pdf_path in pdf_dir.glob("*.pdf"):
try:
doc = PdfDocument(str(pdf_path))
md = doc.to_markdown_all(detect_headings=True, include_images=False)
documents.append({
"source": pdf_path.name,
"content": md,
"pages": doc.page_count()
})
except PdfError as e:
print(f"Omitido {pdf_path.name}: {e}")
print(f"Convertidos {len(documents)} PDFs")
A 0,8 ms por página, convertir miles de PDFs para tu base de datos vectorial lleva segundos, no minutos.
Cómo funciona la detección de encabezados
PDF Oxide agrupa los tamaños de fuente de la página para identificar los niveles de encabezado:
- Extrae todos los spans de texto con el tamaño y el peso de la fuente
- Agrupa los spans por tamaño: el tamaño más frecuente es el cuerpo del texto
- Mapea los tamaños mayores o más gruesos a
#(el mayor),##y### - Conserva el formato inline negrita (
**text**) y cursiva (*text*)
Esto funciona bien para artículos académicos, informes y documentación. Para PDFs con esquemas de fuente poco convencionales, desactiva la detección:
md = doc.to_markdown(0, detect_headings=False)
PDF a Markdown para pipelines LLM y RAG
La conversión a Markdown que trae PDF Oxide está pensada específicamente para flujos de IA. La jerarquía de encabezados que detecta se mapea directamente a la estructura semántica, lo que simplifica el procesamiento posterior.
Entregar Markdown a un LLM
Convierte un PDF y envía el Markdown directamente a un modelo de lenguaje para resumir, hacer Q&A o analizar:
from pdf_oxide import PdfDocument
doc = PdfDocument("quarterly-report.pdf")
md = doc.to_markdown_all(detect_headings=True, include_images=False)
# Envialo a cualquier API de LLM: la estructura en Markdown le ayuda al
# modelo a entender la organizacion del documento
prompt = f"""Resume el siguiente documento. Presta atencion a la
estructura de encabezados para identificar las secciones principales.
{md}
"""
# response = llm_client.generate(prompt)
Como PDF Oxide preserva la jerarquía de encabezados (#, ##, ###), el LLM distingue los títulos de sección del cuerpo y produce resúmenes conscientes de la estructura. Con una extracción de texto plano, al modelo le toca adivinar dónde empieza y termina cada sección.
Chunking por encabezados para RAG
Partir el texto por encabezados de Markdown produce chunks semánticamente significativos que se embeben bien y se recuperan con precisión:
from pdf_oxide import PdfDocument
import re
doc = PdfDocument("technical-manual.pdf")
md = doc.to_markdown_all(detect_headings=True, include_images=False)
# Parte en chunks por los limites de encabezado
chunks = re.split(r'\n(?=#{1,3} )', md)
chunks = [c.strip() for c in chunks if c.strip()]
# Cada chunk arranca con un encabezado: uselo como metadato
for chunk in chunks:
lines = chunk.split('\n', 1)
title = lines[0].lstrip('#').strip()
body = lines[1].strip() if len(lines) > 1 else ""
# embed_and_store(title=title, content=body, source="technical-manual.pdf")
Este enfoque te da chunks coherentes (cada uno es una sección completa), titulados (el encabezado funciona como metadato para la recuperación) y de tamaño razonablemente parejo (los autores suelen escribir secciones de longitud similar). La detección de encabezados de PDF Oxide lo hace posible sin configuración manual: el algoritmo de clustering por tamaño de fuente identifica los niveles de forma automática.
Por qué PDF Oxide es ideal para pipelines de IA
A 0,8 ms por página, PDF Oxide es lo suficientemente rápido para convertir documentos al vuelo en el momento de la consulta, no sólo en el de la indexación. Eso habilita flujos que son impracticables con herramientas más lentas:
- Conversión bajo demanda: convierte un PDF a Markdown cuando el usuario lo sube, sin una demora perceptible
- Reprocesamiento: actualiza tu índice RAG reconvirtiendo todos los PDFs al cambiar la estrategia de chunking; miles de páginas procesadas en segundos
- Pipelines en streaming: convierte los PDFs a medida que van llegando a una cola, sin acumular atrasos
Procesamiento por lotes
Convierte un directorio entero de PDFs a archivos Markdown:
from pdf_oxide import PdfDocument
from pathlib import Path
for pdf_path in Path("documents/").glob("*.pdf"):
doc = PdfDocument(str(pdf_path))
md_parts = []
for i in range(doc.page_count()):
md_parts.append(doc.to_markdown(i, detect_headings=True))
md_path = pdf_path.with_suffix(".md")
md_path.write_text("\n\n".join(md_parts))
print(f"Convertido {pdf_path.name} -> {md_path.name}")
Con velocidades por debajo del milisegundo por página, convertir en lote cientos de PDFs se resuelve en segundos. Para cargas de producción con miles de archivos, revisa la guía de procesamiento por lotes con patrones de paralelización.
PDF a Markdown: PDF Oxide frente a las alternativas
| Herramienta | Velocidad | Integrado | Detección de encabezados | Preservación de tablas |
|---|---|---|---|---|
| PDF Oxide | 0,8 ms | Sí | Sí | Sí |
| pymupdf4llm | 55,5 ms (69× más lento) | No (paquete aparte) | Sí | Sí |
| marker | ~500 ms o más | No (herramienta aparte) | Sí | Sí |
| pdfplumber + código propio | ~23 ms o más | No (manual) | No | Manual |
| pypdf + código propio | ~12 ms o más | No (manual) | No | No |
PDF Oxide es la única biblioteca de Python para PDFs con conversión a Markdown integrada y rápida. Detecta encabezados a partir del clustering por tamaño de fuente, convierte tablas a sintaxis GitHub Flavored Markdown y preserva el formato inline, todo en una sola llamada a to_markdown().
pymupdf4llm requiere PyMuPDF (bajo licencia AGPL) más un paquete pymupdf4llm adicional. Es 69 veces más lento que PDF Oxide y arrastra obligaciones de licencia copyleft que pueden ser incompatibles con aplicaciones propietarias.
marker es una herramienta independiente, no una biblioteca. Utiliza modelos de deep learning para la detección de layout, lo que la hace precisa en layouts complejos pero órdenes de magnitud más lenta. Además necesita memoria GPU significativa para rendir bien.
pdfplumber y pypdf no ofrecen conversión a Markdown en absoluto. Tendrías que escribir tu propio código para detectar encabezados, reconstruir tablas y formatear la salida como Markdown, un esfuerzo de ingeniería importante para replicar lo que PDF Oxide ofrece de fábrica.
Páginas relacionadas
- API de conversión a Markdown — referencia completa de la API
- PDF para pipelines RAG — guía completa de integración con RAG
- Extraer texto de PDF — extracción de texto plano
- Procesamiento por lotes — patrones de paralelización