Skip to content

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:

  1. Extrae todos los spans de texto con el tamaño y el peso de la fuente
  2. Agrupa los spans por tamaño: el tamaño más frecuente es el cuerpo del texto
  3. Mapea los tamaños mayores o más gruesos a # (el mayor), ## y ###
  4. 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
pymupdf4llm 55,5 ms (69× más lento) No (paquete aparte)
marker ~500 ms o más No (herramienta aparte)
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