PDF para Markdown em Python
Converter PDF para Markdown é uma das etapas mais importantes no processamento moderno de documentos. Seja para construir uma aplicação com LLM, um pipeline RAG ou apenas arquivar documentos em um formato legível, converter PDF para Markdown em Python entrega uma saída estruturada e portátil que funciona em qualquer lugar.
Por que converter PDF para Markdown?
O Markdown virou o formato de intercâmbio padrão em fluxos de IA e de documentos. Aqui estão os motivos para fazer a conversão:
As janelas de contexto dos LLMs funcionam melhor com texto estruturado. Modelos grandes como GPT-4, Claude e Llama produzem resultados bem melhores quando recebem Markdown limpo em vez de texto bruto extraído. Os títulos dão ao modelo um mapa do documento, e formatações como negrito e itálico carregam um significado semântico que o texto puro descarta.
Pipelines RAG precisam de texto limpo, fatiado e com os títulos preservados. Sistemas de geração aumentada por recuperação dividem os documentos em chunks, geram embeddings e recuperam os trechos mais relevantes na hora da consulta. Os títulos Markdown são fronteiras naturais de chunk — dividir em ## já entrega seções coerentes, cada uma com um título próprio. Com extração de texto puro essas fronteiras somem e você acaba dependendo de heurísticas frágeis, como tamanho de parágrafo ou contagem de frases.
O Markdown preserva a estrutura do documento sem deixar de ser texto puro. Títulos, listas com marcadores, listas numeradas, tabelas, negrito e itálico sobrevivem à conversão num formato que humanos e máquinas entendem. Um arquivo Markdown é, no fim, um arquivo de texto — ele funciona com controle de versão, com busca textual e com qualquer linguagem de programação.
As alternativas são piores. A extração de texto puro perde toda a estrutura: os títulos ficam indistinguíveis do corpo, as tabelas viram um amontoado de linhas e as listas perdem a hierarquia. A conversão para HTML preserva a estrutura, mas acrescenta bastante volume — 2 KB de Markdown podem virar 15 KB de HTML com <div> aninhados, classes CSS e entidades escapadas. O Markdown atinge o ponto ideal: estruturado, leve e universalmente suportado.
Início rápido
Converta uma página de PDF em Markdown limpo em três linhas:
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));
O PDF Oxide detecta títulos a partir da clusterização dos tamanhos de fonte, preserva formatações em negrito e itálico, converte tabelas para a sintaxe GFM e, se quiser, incorpora as imagens. Nenhuma outra biblioteca de PDF em Python traz a conversão para Markdown embutida.
Instalação
pip install pdf_oxide
Converter o documento inteiro
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() converte todas as páginas e as une com separadores ---.
Opções de conversão
| Parâmetro | Padrão | Descrição |
|---|---|---|
detect_headings |
True |
Mapeia tamanhos de fonte para títulos #, ##, ### |
preserve_layout |
False |
Preserva a posição visual |
include_images |
True |
Inclui imagens na saída |
embed_images |
True |
Incorpora como data URIs em base64 |
image_output_dir |
None |
Salva as imagens neste diretório em vez de incorporar |
Só os títulos (sem imagens)
doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True, include_images=False)
Salvar imagens num diretório
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)
Integração com pipelines RAG e LLM
O Markdown é o formato ideal para pipelines RAG. Os títulos já entregam fronteiras naturais de chunk, e a estrutura preservada carrega significado que o texto puro perde pelo caminho.
Fatiamento por título
Python
from pdf_oxide import PdfDocument
import re
doc = PdfDocument("paper.pdf")
md = doc.to_markdown_all(detect_headings=True)
# Quebra pelos titulos para um 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();
// Quebra pelos titulos para um 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}...");
}
Fatiamento por 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();
Conversão em lote para banco vetorial
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"Ignorado {pdf_path.name}: {e}")
print(f"Convertidos {len(documents)} PDFs")
A 0,8 ms por página, converter milhares de PDFs para o seu banco vetorial leva segundos, não minutos.
Como funciona a detecção de títulos
O PDF Oxide agrupa os tamanhos de fonte presentes na página para identificar os níveis de título:
- Extrai todos os spans de texto com metadados de tamanho e peso de fonte
- Agrupa os spans por tamanho — o tamanho mais comum é o do corpo do texto
- Mapeia tamanhos maiores ou mais pesados para títulos
#(maior),##e### - Preserva a formatação inline negrito (
**text**) e itálico (*text*)
Esse esquema funciona bem em artigos acadêmicos, relatórios e documentação. Para PDFs com esquemas de fonte pouco convencionais, desative a detecção de títulos:
md = doc.to_markdown(0, detect_headings=False)
PDF para Markdown em pipelines LLM e RAG
A conversão para Markdown embutida no PDF Oxide foi pensada para fluxos de IA. A hierarquia de títulos detectada reflete diretamente a estrutura semântica, o que simplifica o processamento posterior.
Entregar Markdown a um LLM
Converta um PDF e mande o Markdown direto para um modelo de linguagem para resumo, Q&A ou análise:
from pdf_oxide import PdfDocument
doc = PdfDocument("quarterly-report.pdf")
md = doc.to_markdown_all(detect_headings=True, include_images=False)
# Envie para qualquer API de LLM — a estrutura em Markdown ajuda o
# modelo a entender a organizacao do documento
prompt = f"""Resuma o documento a seguir. Preste atencao na estrutura
de titulos para identificar as secoes principais.
{md}
"""
# response = llm_client.generate(prompt)
Como o PDF Oxide preserva a hierarquia de títulos (#, ##, ###), o LLM consegue diferenciar títulos de seção do corpo do texto e gerar resumos conscientes da estrutura. Com extração de texto puro, o modelo precisa adivinhar onde as seções começam e terminam.
Fatiamento por títulos para RAG
Dividir o texto pelos títulos Markdown gera chunks semanticamente relevantes que embedam bem e são recuperados com precisão:
from pdf_oxide import PdfDocument
import re
doc = PdfDocument("technical-manual.pdf")
md = doc.to_markdown_all(detect_headings=True, include_images=False)
# Divide em chunks pelas fronteiras de titulo
chunks = re.split(r'\n(?=#{1,3} )', md)
chunks = [c.strip() for c in chunks if c.strip()]
# Cada chunk tem um titulo na primeira linha — use como metadado
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")
Essa abordagem gera chunks coerentes (cada chunk é uma seção inteira), com título (o título serve como metadado na recuperação) e de tamanho razoavelmente parecido (os autores costumam escrever seções de tamanho similar). A detecção de títulos do PDF Oxide torna isso possível sem configuração manual — o algoritmo de clusterização por tamanho de fonte identifica os níveis automaticamente.
Por que o PDF Oxide é ideal para pipelines de IA
Com 0,8 ms por página, o PDF Oxide é rápido o bastante para converter documentos na hora da consulta, e não apenas no momento de indexação. Isso abre espaço para fluxos que ficariam inviáveis com ferramentas mais lentas:
- Conversão sob demanda: converta um PDF para Markdown quando o usuário fizer o upload, sem atraso perceptível
- Reprocessamento: atualize o índice RAG reconvertendo todos os PDFs ao mudar a estratégia de chunking — milhares de páginas processadas em segundos
- Pipelines de streaming: converta os PDFs à medida que chegam na fila, sem acumular backlog
Processamento em lote
Converta um diretório inteiro de PDFs em arquivos 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}")
Em velocidades abaixo do milissegundo por página, converter em lote centenas de PDFs termina em segundos. Para cargas de produção com milhares de arquivos, veja o guia de processamento em lote com padrões de paralelização.
PDF para Markdown: PDF Oxide vs. alternativas
| Ferramenta | Velocidade | Embutido | Detecção de títulos | Preservação de tabelas |
|---|---|---|---|---|
| PDF Oxide | 0,8 ms | Sim | Sim | Sim |
| pymupdf4llm | 55,5 ms (69× mais lento) | Não (pacote à parte) | Sim | Sim |
| marker | ~500 ms+ | Não (ferramenta à parte) | Sim | Sim |
| pdfplumber + código próprio | ~23 ms+ | Não (manual) | Não | Manual |
| pypdf + código próprio | ~12 ms+ | Não (manual) | Não | Não |
PDF Oxide é a única biblioteca de PDF em Python com conversão para Markdown embutida e rápida. Ela detecta títulos por clusterização dos tamanhos de fonte, converte tabelas para a sintaxe do GitHub Flavored Markdown e preserva a formatação inline — tudo numa única chamada a to_markdown().
pymupdf4llm exige o PyMuPDF (sob licença AGPL) mais o pacote pymupdf4llm instalado por cima. É 69 vezes mais lento que o PDF Oxide e traz obrigações de licença copyleft que podem ser incompatíveis com aplicações proprietárias.
marker é uma ferramenta independente, não uma biblioteca. Usa modelos de deep learning para detecção de layout, o que deixa o resultado preciso em layouts complexos, mas ordens de magnitude mais lento. Além disso, exige uma quantidade significativa de memória de GPU para render bem.
pdfplumber e pypdf não oferecem conversão para Markdown. Você teria de escrever código próprio para detectar títulos, reconstruir tabelas e formatar a saída como Markdown — um esforço de engenharia considerável para replicar o que o PDF Oxide entrega pronto.
Páginas relacionadas
- API de conversão em Markdown — referência completa da API
- PDF para pipelines RAG — guia completo de integração com RAG
- Extrair texto de PDF — extração de texto puro
- Processamento em lote — padrões de paralelização