Порядок читання та XY-cut — видобування багатоколонкових PDF у природному порядку
Багатоколонкові PDF — наукові статті, підручники, журнальні матеріали, policy briefs — збивають більшість інструментів видобування. Наївний прохід згори донизу підхоплює слово з першої колонки, потім із другої, далі знову з першої — і на виході виходить каша на кшталт accompaally ("accompa" з першої колонки, склеєне з "ally" з другої).
PDF Oxide використовує алгоритм XY-cut, щоб виявляти колонки і автоматично формувати природний порядок читання. Починаючи з v0.3.34 він також захищений від хибних спрацьовувань на розріджених макетах (сторінки з копірайтом, титульні аркуші) і коректно обробляє змішані випадки, коли таблиця вбудована в основний текст.
Швидкий приклад
Видобування враховує колонки за замовчуванням — жодних додаткових прапорів:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("academic-paper.pdf")
text = doc.extract_text(0)
# Колонки читаються згори донизу в межах кожної колонки, без чергування.
Rust
use pdf_oxide::PdfDocument;
let mut doc = PdfDocument::open("academic-paper.pdf")?;
let text = doc.extract_text(0)?;
JavaScript / TypeScript (Node)
const { PdfDocument } = require("pdf-oxide");
const doc = new PdfDocument("academic-paper.pdf");
const text = doc.extractText(0);
doc.close();
JavaScript (WASM)
import { WasmPdfDocument } from "pdf-oxide-wasm";
const doc = new WasmPdfDocument(bytes);
console.log(doc.extractText(0));
doc.free();
Go
doc, _ := pdfoxide.Open("academic-paper.pdf")
defer doc.Close()
text, _ := doc.ExtractText(0)
fmt.Println(text)
C#
using PdfOxide;
using var doc = PdfDocument.Open("academic-paper.pdf");
Console.WriteLine(doc.ExtractText(0));
Що робить XY-cut
Алгоритм XY-cut рекурсивно ділить сторінку на прямокутні регіони, чергуючи вертикальні й горизонтальні розрізи вздовж жолобів пустого простору:
- Проєктує всі символи на вісь X. Якщо знайдено високий і широкий вертикальний проміжок (жолоб між колонками), сторінка ділиться на дві області за цією координатою X.
- Усередині кожної області проєктує на вісь Y і розрізає за горизонтальними жолобами (міжабзацні проміжки, межі розділів).
- Рекурсія триває, доки в листовій області не залишиться помітних жолобів — це і є атомарні блоки.
- Блоки серіалізуються в порядку згори донизу, зліва направо.
Це збігається з тим, як читає людина: перша колонка згори донизу, далі друга колонка згори донизу, а потім спільний «підвал» на всю ширину.
Коли спрацьовує XY-cut
XY-cut умикається автоматично, щойно extract_text бачить багатоколонковий макет. Його пропускають у таких випадках:
- Одноколонкові сторінки (вертикального жолоба немає, тож використовується звичайне сортування за рядками)
- Розріджені сторінки, де на передбачувану колонку припадає менш як приблизно 10 текстових span’ів — зазвичай це титульні аркуші або сторінки з копірайтом, де два піки по X — це артефакт, а не справжні колонки (виправлено в v0.3.34)
Для звичайних випадків налаштування не потрібне. Якщо треба явно увімкнути чи вимкнути режим, див. розділ «Вимкнення» нижче.
Що виправлено у v0.3.34
Перемішаний вивід на багатоколонкових PDF без тегів
На багатоколонкових PDF без тегів (наукових підручниках, довідниках із генетики) extract_text раніше застосовував XY-cut усередині extract_spans(), а потім пересортовував результат посортувавши за рядками в extract_text_with_options, стираючи структуру колонок. У результаті виходили уривки вигляду accompaally.
Виправлення: пересортування за рядками тепер пропускається на сторінках, які справді є багатоколонковими. Чистий вивід підтверджено на підручниках Hartwell Genetics, Murphy ML та Kandel Neural Science.
Сторінки з таблицею всередині тексту
Змішані макети (таблиця всередині основного тексту) могли ввести детектор колонок в оману: рядки таблиці, розтягнуті табуляціями, заповнювали жолоб між колонками. Виправлення:
- Широкі span’и (понад 55 % ширини регіону) виключаються з щільності проєкції — рядки з табами більше не маскують жолоб.
- Односимвольні span’и (значення клітинок виду
G,T) виключаються з проєкції, аби не «розмазуватися» впоперек жолоба. - Покриття рахується через оцінку кількості символів, а не за сирою шириною bbox, тож рядки з табами більше не видають себе за щільний основний текст.
Хибні спрацьовування на розріджених макетах
Сторінки з копірайтом, титульні аркуші та колофони можуть давати два піки по X за наявності лише 7–10 span’ів на «колонку». Тепер вони не вважаються багатоколонковими — XY-cut більше не розрізає речення, половини яких перебувають у різних X-позиціях одного рядка.
Структурований доступ за колонками
На рівень нижче за extract_text можна отримувати слова або посимвольні дані зі збереженим порядком колонок:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("paper.pdf")
for w in doc.extract_words(0):
print(f"{w.text} ({w.x0:.0f},{w.y0:.0f})")
Rust
let mut doc = PdfDocument::open("paper.pdf")?;
for w in doc.extract_words(0)? {
println!("{} ({:.0},{:.0})", w.text, w.x0, w.y0);
}
Go
doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()
words, _ := doc.ExtractWords(0)
for _, w := range words {
fmt.Printf("%s (%.0f,%.0f)\n", w.Text, w.X0, w.Y0)
}
C#
using var doc = PdfDocument.Open("paper.pdf");
// Node/C# повертають рядки формату (text, x, y, w, h):
var lines = doc.ExtractTextLines(0);
foreach (var (text, x, y, w, h) in lines)
Console.WriteLine($"{text} ({x:F0},{y:F0})");
Кожне слово / рядок містить свою обмежувальну рамку, тож їх можна згрупувати за колонками й упорядкувати самостійно, якщо потрібна власна політика (скажімо, читати спершу праву колонку для арабських макетів).
Ручне визначення багатоколонкових сторінок
Якщо перед видобуванням потрібне розгалуження за ознакою багатоколонковості:
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("mixed.pdf")
for i in range(doc.page_count()):
words = doc.extract_words(i)
# Евристика: розрізнювані кластери X-центрів
x_centers = {round((w.x0 + w.x1) / 2 / 50) * 50 for w in words}
if len(x_centers) >= 2:
print(f"Page {i}: likely multi-column ({len(x_centers)} X-centers)")
У продакшн-сценаріях краще покладатися на extract_text — нехай рішення ухвалює зв’язка «XY-cut + захист від розріджених макетів».
Вимкнення або власне впорядкування
Якщо потрібні сирі span’и у порядку позицій (наприклад, для власного рушія верстки), використовуйте extract_chars або extract_words — вони повертають записи з обмежувальними рамками, за якими можна застосувати власне сортування:
Python
chars = doc.extract_chars(0)
# Згори донизу, далі зліва направо — колонки ігноруються
chars_sorted = sorted(chars, key=lambda c: (-c.y, c.x))
Rust
let mut chars = doc.extract_chars(0)?;
chars.sort_by(|a, b| b.y.partial_cmp(&a.y).unwrap()
.then(a.x.partial_cmp(&b.x).unwrap()));
Пов’язані сторінки
- Видобування тексту — повна API видобування
- Профілі видобування — налаштування розпізнавання пробілів під тип документа
- Видобування таблиць із PDF — структурований вивід таблиць
- Changelog — виправлення багатоколонкової верстки та змішаних макетів у v0.3.34