PDF у Markdown на Python
Конвертація PDF у Markdown — один із найважливіших кроків у сучасних процесах обробки документів. Будуєте ви застосунок на LLM, RAG-конвеєр чи просто архівуєте документи в читабельному форматі — переведення PDF у Markdown на Python дає структурований та переносний результат, який придається будь-де.
Навіщо конвертувати PDF у Markdown
Markdown став фактичним стандартом обміну в процесах ШІ та роботи з документами. Ось чому конвертація себе виправдовує:
З LLM структурований текст працює помітно краще. Великі мовні моделі на кшталт GPT-4, Claude та Llama показують суттєво кращий результат, коли отримують охайний Markdown замість сирого тексту. Заголовки дають моделі «карту» документа, а форматування як жирний і курсив несе семантичне навантаження, яке звичайний текст відкидає.
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 створена саме під ШІ-сценарії. Виявлена ієрархія заголовків напряму відбиває семантичну структуру, і подальша обробка стає простішою.
Віддати 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 добре підходить для ШІ-конвеєрів
Зі швидкістю 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 — видобування звичайного тексту
- Пакетна обробка — патерни паралельної обробки