PDF в Markdown на Python
Конвертация PDF в Markdown — один из важнейших шагов в современной обработке документов. Независимо от того, собираете ли вы LLM-приложение, RAG-конвейер или просто складируете документы в читаемом виде, перевод PDF в Markdown на Python даёт структурированный и переносимый результат, который подойдёт где угодно.
Зачем конвертировать PDF в Markdown
Markdown стал стандартом обмена для AI- и документных процессов. Вот почему конвертация имеет смысл:
С LLM структурированный текст работает заметно лучше. Крупные языковые модели вроде GPT-4, Claude и Llama выдают существенно более качественный результат, когда получают чистый Markdown вместо сырого текста из PDF. Заголовки дают модели «карту» документа, а форматирование вроде жирного и курсива несёт семантическую нагрузку, которую обычный текст теряет.
RAG-конвейерам нужен аккуратный, поделённый на чанки текст с сохранёнными заголовками. Системы retrieval-augmented generation режут документы на чанки, строят эмбеддинги и достают нужные фрагменты на запрос. Markdown-заголовки — естественные границы чанков: деление по ## сразу даёт связные разделы, каждый со встроенным заголовком. Извлечение «плоского» текста эти границы теряет, и приходится опираться на эвристики вроде длины абзаца или числа предложений.
Markdown сохраняет структуру документа, оставаясь обычным текстом. Заголовки, маркированные и нумерованные списки, таблицы, жирный и курсив — всё это переживает конвертацию в формате, одинаково удобном и человеку, и парсеру. Файл Markdown — просто текст, он дружит с системами контроля версий, текстовым поиском и любым языком программирования.
Альтернативы — хуже. Чистый текст полностью теряет структуру: заголовки сливаются с основным текстом, таблицы рассыпаются в мешанину строк, списки теряют иерархию. Перевод в HTML структуру хранит, но сильно раздувает объём: файл на 2 КБ Markdown легко превращается в 15 КБ HTML с вложенными <div>, CSS-классами и экранированными сущностями. Markdown попадает ровно в середину: структура есть, вес маленький, поддержка везде.
Быстрый старт
Конвертируйте страницу PDF в аккуратный Markdown в три строки:
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 определяет заголовки по кластерам размеров шрифта, сохраняет жирный и курсив, переводит таблицы в синтаксис GFM и, при необходимости, встраивает изображения. Никакая другая Python-библиотека для PDF не умеет конвертировать в Markdown «из коробки».
Установка
pip install pdf_oxide
Конвертация всего документа
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() конвертирует все страницы и склеивает их через разделитель ---.
Параметры конвертации
| Параметр | По умолчанию | Описание |
|---|---|---|
detect_headings |
True |
Сопоставлять размеры шрифта заголовкам #, ##, ### |
preserve_layout |
False |
Сохранять визуальное расположение |
include_images |
True |
Включать изображения в вывод |
embed_images |
True |
Встраивать как data URI в base64 |
image_output_dir |
None |
Сохранять изображения в этот каталог |
Только заголовки (без картинок)
doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True, include_images=False)
Сохранять изображения в каталог
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)
Интеграция с RAG- и LLM-конвейерами
Для RAG-конвейеров Markdown — идеальный формат. Заголовки задают естественные границы чанков, а структурированный вид сохраняет смысл, который теряется при извлечении обычного текста.
Чанкинг по заголовкам
Python
from pdf_oxide import PdfDocument
import re
doc = PdfDocument("paper.pdf")
md = doc.to_markdown_all(detect_headings=True)
# Делим по заголовкам ради семантического чанкинга
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();
// Делим по заголовкам ради семантического чанкинга
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}...");
}
Чанкинг по страницам
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();
Массовая конвертация для векторной БД
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"Пропущен {pdf_path.name}: {e}")
print(f"Конвертировано PDF: {len(documents)}")
При 0,8 мс на страницу конвертация тысяч PDF для векторной БД занимает секунды, а не минуты.
Как работает распознавание заголовков
PDF Oxide кластеризует размеры шрифта, встречающиеся на странице, и по ним распределяет уровни заголовков:
- Извлечь все спаны текста вместе с метаданными о размере и начертании шрифта
- Сгруппировать спаны по размеру — самый частый размер считается основным текстом
- Сопоставить более крупные или более жирные размеры заголовкам
#(крупнейший),##,### - Сохранить инлайновое форматирование жирного (
**text**) и курсива (*text*)
Схема отлично работает для научных статей, отчётов и документации. Для PDF с нестандартной типографикой распознавание заголовков можно отключить:
md = doc.to_markdown(0, detect_headings=False)
PDF в Markdown для LLM- и RAG-конвейеров
Встроенная конвертация в Markdown у PDF Oxide сделана специально под AI-сценарии. Обнаруженная иерархия заголовков напрямую отражает семантическую структуру, и обработка «вниз по течению» становится простой.
Отдать Markdown напрямую LLM
Конвертируйте PDF и передайте Markdown языковой модели для суммаризации, Q&A или анализа:
from pdf_oxide import PdfDocument
doc = PdfDocument("quarterly-report.pdf")
md = doc.to_markdown_all(detect_headings=True, include_images=False)
# Отправьте в любой LLM API — структура Markdown помогает модели
# понять устройство документа
prompt = f"""Суммируй следующий документ. Опирайся на структуру
заголовков, чтобы выделить основные разделы.
{md}
"""
# response = llm_client.generate(prompt)
Раз PDF Oxide сохраняет иерархию заголовков (#, ##, ###), LLM отличает заголовки разделов от основного текста и выдаёт осознающие структуру сводки. С обычным текстом модели приходится самой угадывать, где начинаются и заканчиваются разделы.
Чанкинг по заголовкам для RAG
Деление на Markdown-заголовках даёт осмысленные чанки, которые хорошо эмбеддятся и точно находятся по запросу:
from pdf_oxide import PdfDocument
import re
doc = PdfDocument("technical-manual.pdf")
md = doc.to_markdown_all(detect_headings=True, include_images=False)
# Режем на чанки по границам заголовков
chunks = re.split(r'\n(?=#{1,3} )', md)
chunks = [c.strip() for c in chunks if c.strip()]
# У каждого чанка заголовок в первой строке — это удобный метаданный
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")
Такой подход даёт связные чанки (каждый — законченный раздел), снабжённые заголовками (заголовок работает как метаданные при поиске) и одинакового масштаба (авторы обычно пишут разделы примерно одной длины). Распознавание заголовков в PDF Oxide делает это возможным без ручной настройки: алгоритм кластеризации по размеру шрифта сам определяет уровни.
Почему PDF Oxide удобен для AI-конвейеров
При 0,8 мс на страницу PDF Oxide достаточно быстр, чтобы конвертировать документы прямо во время запроса, а не только на этапе индексации. Это открывает сценарии, которые с медленными инструментами были бы непрактичны:
- Конвертация по запросу: как только пользователь загружает PDF, вы мгновенно отдаёте Markdown без ощутимой задержки
- Переиндексация: обновляйте RAG-индекс, заново конвертируя все PDF при смене стратегии чанкинга — тысячи страниц обрабатываются за секунды
- Потоковые конвейеры: конвертируйте PDF по мере их поступления в очередь, не копя бэклог
Пакетная обработка
Конвертируйте целый каталог PDF в 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"Готово {pdf_path.name} -> {md_path.name}")
На таких скоростях (доли миллисекунды на страницу) пакетная конвертация сотен PDF занимает секунды. Для продакшен-нагрузок на тысячи файлов смотрите руководство по пакетной обработке с паттернами параллельной обработки.
PDF в Markdown: PDF Oxide и альтернативы
| Инструмент | Скорость | Встроено | Распознавание заголовков | Сохранение таблиц |
|---|---|---|---|---|
| PDF Oxide | 0,8 мс | Да | Да | Да |
| pymupdf4llm | 55,5 мс (в 69 раз медленнее) | Нет (отдельный пакет) | Да | Да |
| marker | ~500 мс и больше | Нет (отдельная утилита) | Да | Да |
| pdfplumber + свой код | ~23 мс и больше | Нет (вручную) | Нет | Вручную |
| pypdf + свой код | ~12 мс и больше | Нет (вручную) | Нет | Нет |
PDF Oxide — единственная Python-библиотека для PDF с встроенной быстрой конвертацией в Markdown. Она выявляет заголовки кластеризацией размеров шрифта, переводит таблицы в GitHub Flavored Markdown и сохраняет инлайновое форматирование — всё одним вызовом to_markdown().
pymupdf4llm требует PyMuPDF (лицензия AGPL) и сверху ещё пакет pymupdf4llm. Он в 69 раз медленнее PDF Oxide и тянет за собой копилефт-обязательства, с которыми проприетарный продукт часто несовместим.
marker — это не библиотека, а отдельная утилита. Для распознавания вёрстки она использует модели глубокого обучения: на сложных макетах точно, но на порядок медленнее и требовательна к памяти GPU.
pdfplumber и pypdf в принципе не умеют переводить в Markdown. Пришлось бы вручную писать распознавание заголовков, восстановление таблиц и форматирование результата — значительный объём работы, чтобы воспроизвести то, что PDF Oxide даёт сразу.
Связанные страницы
- API конвертации в Markdown — полный справочник API
- PDF для RAG-конвейеров — подробное руководство по интеграции с RAG
- Извлечение текста из PDF — обычное извлечение текста
- Пакетная обработка — паттерны параллельной обработки