Skip to content

Извлечение таблиц из PDF в Python

Извлечение таблиц из PDF — одна из самых частых задач в конвейерах обработки документов. Нужно достать финансовые показатели из годового отчёта, собрать товарный каталог или передать структурированные данные в LLM — во всех случаях без надёжного распознавания таблиц не обойтись. В этом руководстве разобрано всё, что пригодится в Python: от однострочников до готовых к продакшену сценариев для многостраничных таблиц.

Движок распознавания

PDF Oxide использует универсальный конвейер распознавания таблиц рёбра → snap/merge → пересечения → ячейки → группы — тот же подход, что и у Tabula, pdfplumber и PyMuPDF, но реализованный на чистом Rust.

Возможности распознавания:

  • На основе пересечений — находит пересечения горизонтальных и вертикальных линий, собирает ячейки по прямоугольникам в четыре угла и объединяет их в таблицы через union-find.
  • Расширенная сетка — если горизонтальные и вертикальные линии находятся в разных областях страницы, строится виртуальная сетка из декартова произведения всех координат.
  • Распознавание текста с учётом колонок — двухколоночные раскладки делятся по X-проекции гистограммы, после чего для каждой колонки запускается распознавание таблиц по тексту.
  • Текстовые таблицы с горизонтальными линейками — распознаются таблицы, ограниченные только горизонтальными линиями без вертикальных (часто встречаются в научных статьях).
  • Гибридное распознавание строк — границы строк выводятся из Y-координат текста, когда заданы только вертикальные границы (строки счёта-фактуры).
  • Восстановление пунктирных и штриховых линий — короткие отрезки сливаются в сплошные рёбра.
  • Разбиение по разделителям разделов — многосекционные формы делятся на полную ширину страницы по горизонтальным разделителям.
  • Фильтрация по покрытию рёбрами — отбрасываются одиночные рёбра, не участвующие ни в одной возможной сетке.

Настройка

TableDetectionConfig даёт несколько настраиваемых параметров:

Поле По умолчанию Описание
horizontal_strategy "lines_strict" "lines_strict", "lines", "text" или "explicit"
vertical_strategy "lines_strict" Тот же набор значений
v_split_gap 20.0 pt Разрыв между вертикальными линиями, при котором начинается разделение на разные таблицы (до v0.3.20 значение было жёстко задано как 4 pt)
snap_tolerance 3.0 pt Допуск объединения близких рёбер
text_tolerance 3.0 pt Допуск объединения текстовых строк

Изменение поведения

Начиная с v0.3.20, стратегия по умолчанию у Python-метода extract_tables()Both (распознавание и по линиям, и по тексту). Страницам, которые полагались на прежнее текстовое поведение, нужно явно передать horizontal_strategy="text" и vertical_strategy="text".

Python-привязка теперь корректно читает vertical_strategy из словаря table_settings — раньше значение молча игнорировалось.

Рендеринг

Извлечённые таблицы печатаются с выравниванием столбцов пробелами (вместо ASCII-псевдографики из прежних версий). Колонки с валютами и числами автоматически выравниваются по правому краю. Префиксы формы ("1 Apr 11""Apr 11") и декоративные ячейки из дефисов или подчёркиваний ("------") при рендеринге удаляются.

Извлеките данные таблицы из PDF через преобразование в Markdown:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)
# Вывод содержит таблицы в формате GFM:
# | Item | Qty | Price |
# |------|-----|-------|
# | Widget | 10 | $9.99 |

WASM

import { WasmPdfDocument } from "pdf-oxide-wasm";

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
console.log(md);
// Вывод содержит таблицы в формате GFM:
// | Item | Qty | Price |
// |------|-----|-------|
// | Widget | 10 | $9.99 |
doc.free();

Rust

use pdf_oxide::PdfDocument;

let mut doc = PdfDocument::open("invoice.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("invoice.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("invoice.pdf");
Console.WriteLine(doc.ToMarkdown(0));

PDF Oxide распознаёт табличные раскладки через пространственный анализ выровненных текстовых блоков и выдаёт таблицы в формате GitHub Flavored Markdown.

Почему извлечение таблиц из PDF — сложная задача

Если вы когда-нибудь копировали таблицу из PDF и вставляли в электронную таблицу, то знаете: результат почти всегда получается кашей. Это не ошибка просмотрщика, а фундаментальное ограничение самого формата PDF.

В PDF нет понятия «таблица». В отличие от HTML, где таблица описывается тегами <table>, <tr> и <td>, PDF хранит только инструкции рисования: поместить глиф в координаты (x, y), провести линию из точки A в точку B. Нет семантического слоя, который бы сообщил, что «эти символы относятся к ячейке строки 3, колонки 2». Любая библиотека извлечения таблиц вынуждена восстанавливать эту структуру сама, анализируя положение текста и линий на странице.

Такая реконструкция сложна по нескольким причинам:

  • Таблицы с рамкой и без неё. Если есть видимые линии сетки, инструмент использует их как границы ячеек. У таблиц без рамок — часто встречающихся в финансовой отчётности, государственных отчётах и научных статьях — линий нет вовсе. Библиотеке приходится определять границы колонок исключительно по промежуткам между текстовыми блоками, и результат легко сорвать при переменной ширине колонок или числах, выровненных по правому краю.

  • Объединённые ячейки и заголовки, охватывающие несколько колонок. Ячейка-заголовок на три колонки выглядит как один широкий текстовый блок. Без линий сетки парсер не может надёжно определить, какие именно колонки покрывает заголовок. Некоторые библиотеки справляются хорошо, но многие молча выдают искажённый результат.

  • Многострочное содержимое ячейки. Если в ячейке — абзац с переносами, наивный построчный разбор воспримет каждую строку как отдельную строку таблицы. Чтобы снова собрать их в одну ячейку, нужно понимать вертикальные границы каждой строки.

  • Многостраничные таблицы. Крупные таблицы часто раскиданы по двум и более страницам. Заголовок может повторяться на каждой странице, а может и нет; между строками таблицы встречаются колонтитулы, водяные знаки и номера страниц. Чтобы сшить фрагменты в единую таблицу, требуется логика, учитывающая разбиение на страницы.

  • Повёрнутый текст и нестандартные раскладки. Некоторые PDF используют повёрнутый текст для заголовков колонок или размещают таблицы в многоколоночном макете страницы. Такие крайние случаи ломают привычные парсерам допущения про порядок чтения слева направо и сверху вниз.

Если держать эти сложности в голове, проще подобрать инструмент под конкретный набор документов. Для чётко выровненных таблиц — большинство счетов, подтверждений заказов и простых отчётов — быстрого пространственного анализа, как в PDF Oxide, вполне достаточно. Для документов со сложными объединениями, таблицами без рамок или нестандартным оформлением может понадобиться библиотека с более развитыми эвристиками.

Извлечение таблиц: PDF Oxide и другие библиотеки

Выбор библиотеки для извлечения таблиц из PDF в Python зависит от ваших документов, требований к скорости и нужного формата вывода. Вот как соотносятся основные варианты:

Библиотека Распознавание таблиц Таблицы с рамкой Таблицы без рамки Формат вывода Скорость
PDF Oxide Встроено Да Базово Markdown/HTML 0,8 мс
pdfplumber Встроено Да Продвинуто Python-списки 23,2 мс
Camelot Встроено Да Да (lattice/stream) DataFrame ~50 мс+
PyMuPDF Базово (v1.23+) Да Ограниченно DataFrame 4,6 мс
pypdf Нет Нет Нет
tabula-py Встроено Да Да DataFrame ~100 мс+ (Java)

PDF Oxide — с большим отрывом самый быстрый вариант. Он распознаёт таблицы пространственным анализом выровненных текстовых блоков и сразу выдаёт их в аккуратном GitHub Flavored Markdown. Среднее время 0,8 мс делает его в 29 раз быстрее pdfplumber и более чем в 100 раз быстрее tabula-py. Хорошо справляется с таблицами в рамке и простыми выровненными таблицами без рамок. Для LLM-пайплайнов, где Markdown всё равно нужен, выбор очевиден.

pdfplumber имеет самое зрелое распознавание таблиц без рамок. Метод find_tables() предлагает настраиваемые стратегии определения строк и колонок по выравниванию текста и заметно лучше справляется с объединёнными ячейками и многострочным содержимым, чем большинство альтернатив. Компромисс — скорость: 23,2 мс на страницу заметно замедляют пакетную обработку.

Camelot предлагает два режима — lattice (для таблиц с рамкой) и stream (для таблиц без рамки). Он сразу возвращает pandas DataFrame, что удобно в аналитических сценариях. Однако зависит от Ghostscript и OpenCV — установка тяжёлая, а скорость самая низкая среди вариантов на чистом Python.

PyMuPDF (fitz) добавил базовое извлечение таблиц в версии 1.23. Он быстрый (4,6 мс) и неплохо работает с простыми таблицами в рамках, но поддержка таблиц без рамок уступает pdfplumber и Camelot.

pypdf вообще не умеет распознавать таблицы. Он выдаёт сырой текст, и восстанавливать структуру таблицы придётся собственным парсером.

tabula-py — это Python-обёртка над Tabula на Java. Распознавание и для таблиц с рамкой, и без неё достойное, но требуется Java-среда, и из-за старта JVM это самый медленный вариант. Подходит скорее для разовых задач, чем для высоконагруженных конвейеров.

Для большинства продакшен-задач обычно советуют брать PDF Oxide как основной экстрактор ради скорости и простоты, а pdfplumber оставлять как резерв для того подмножества документов со сложными таблицами, где нужны продвинутые эвристики.

Установка

pip install pdf_oxide

Базовое извлечение таблиц

Как Markdown-таблицы

Самый простой путь — преобразовать страницу в Markdown, где таблицы уже записаны в синтаксисе GFM:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
for i in range(doc.page_count()):
    md = doc.to_markdown(i, detect_headings=True)
    if "|" in md:  # На странице есть таблица
        print(f"--- Страница {i + 1} ---")
        print(md)

WASM

const doc = new WasmPdfDocument(bytes);
for (let i = 0; i < doc.pageCount(); i++) {
    const md = doc.toMarkdown(i);
    if (md.includes("|")) { // На странице есть таблица
        console.log(`--- Страница ${i + 1} ---`);
        console.log(md);
    }
}
doc.free();

Rust

let mut doc = PdfDocument::open("report.pdf")?;
for i in 0..doc.page_count()? {
    let md = doc.to_markdown(i, true)?;
    if md.contains("|") {
        println!("--- Страница {} ---", i + 1);
        println!("{}", md);
    }
}

Go

doc, _ := pdfoxide.Open("report.pdf")
defer doc.Close()

n, _ := doc.PageCount()
for i := 0; i < n; i++ {
    md, _ := doc.ToMarkdown(i)
    if strings.Contains(md, "|") {
        fmt.Printf("--- Страница %d ---\n%s\n", i+1, md)
    }
}

C#

using var doc = PdfDocument.Open("report.pdf");
for (int i = 0; i < doc.PageCount; i++)
{
    var md = doc.ToMarkdown(i);
    if (md.Contains("|"))
        Console.WriteLine($"--- Страница {i + 1} ---\n{md}");
}

Структурированное извлечение таблиц (v0.3.34)

Чтобы работать со строками и ограничивающими прямоугольниками типизированно, без парсинга Markdown, вызывайте ExtractTables(pageIndex) (Go, C#) или extract_tables(page) (Python, Rust). У каждой таблицы доступны структурированные ячейки, так что результат можно отправлять прямо в базу данных или DataFrame — без регулярных выражений.

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
for table in doc.extract_tables(0):
    for row in table.rows:
        print(row)

Rust

let mut doc = PdfDocument::open("invoice.pdf")?;
for table in doc.extract_tables(0)? {
    for row in &table.rows {
        println!("{:?}", row);
    }
}

Go

doc, _ := pdfoxide.Open("invoice.pdf")
defer doc.Close()

tables, _ := doc.ExtractTables(0)
for _, t := range tables {
    for _, row := range t.Rows {
        fmt.Println(row)
    }
}

C#

using var doc = PdfDocument.Open("invoice.pdf");
foreach (var table in doc.ExtractTables(0))
    foreach (var row in table.Rows)
        Console.WriteLine(string.Join(" | ", row));

Парсинг Markdown-таблиц в строки

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0)

# Извлечь строки таблицы из Markdown
rows = []
for line in md.split("\n"):
    line = line.strip()
    if line.startswith("|") and not line.startswith("|--"):
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        rows.append(cells)

header = rows[0] if rows else []
data = rows[1:] if len(rows) > 1 else []
print(f"Колонки: {header}")
for row in data:
    print(row)

WASM

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);

const rows = [];
for (const line of md.split("\n")) {
    const trimmed = line.trim();
    if (trimmed.startsWith("|") && !trimmed.startsWith("|--")) {
        const cells = trimmed.split("|").slice(1, -1).map(c => c.trim());
        rows.push(cells);
    }
}

const header = rows[0] || [];
const data = rows.slice(1);
console.log("Колонки:", header);
data.forEach(row => console.log(row));
doc.free();

Rust

let mut doc = PdfDocument::open("invoice.pdf")?;
let md = doc.to_markdown(0, false)?;

let rows: Vec<Vec<String>> = md.lines()
    .map(|l| l.trim())
    .filter(|l| l.starts_with('|') && !l.starts_with("|--"))
    .map(|l| l.split('|').skip(1).map(|c| c.trim().to_string())
        .take_while(|c| !c.is_empty()).collect())
    .collect();

if let Some(header) = rows.first() {
    println!("Колонки: {:?}", header);
    for row in &rows[1..] {
        println!("{:?}", row);
    }
}

Экспорт в CSV

import csv
from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0)

rows = []
for line in md.split("\n"):
    line = line.strip()
    if line.startswith("|") and not line.startswith("|--"):
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        rows.append(cells)

with open("table.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(rows)

Экспорт в DataFrame pandas

import pandas as pd
from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
md = doc.to_markdown(0)

rows = []
for line in md.split("\n"):
    line = line.strip()
    if line.startswith("|") and not line.startswith("|--"):
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        rows.append(cells)

if rows:
    df = pd.DataFrame(rows[1:], columns=rows[0])
    print(df)

Позиции символов для собственного парсинга таблиц

Когда нужен максимальный контроль, пригодится посимвольное извлечение в сочетании с пространственным анализом:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("financial.pdf")
chars = doc.extract_chars(0)

# Сгруппировать символы по Y-координате (строкам)
rows = {}
for ch in chars:
    row_key = round(ch.y / 2) * 2  # Прижать к сетке 2 pt
    rows.setdefault(row_key, []).append(ch)

# Строки — сверху вниз, символы — слева направо
for y in sorted(rows.keys(), reverse=True):
    line_chars = sorted(rows[y], key=lambda c: c.x)
    text = "".join(c.char for c in line_chars)
    print(text)

WASM

const doc = new WasmPdfDocument(bytes);
const chars = doc.extractChars(0);

// Сгруппировать символы по Y-координате (строкам)
const rows = new Map();
for (const ch of chars) {
    const rowKey = Math.round(ch.y / 2) * 2; // Прижать к сетке 2 pt
    if (!rows.has(rowKey)) rows.set(rowKey, []);
    rows.get(rowKey).push(ch);
}

// Строки — сверху вниз, символы — слева направо
const sortedKeys = [...rows.keys()].sort((a, b) => b - a);
for (const y of sortedKeys) {
    const lineChars = rows.get(y).sort((a, b) => a.x - b.x);
    const text = lineChars.map(c => c.char).join("");
    console.log(text);
}
doc.free();

Rust

use std::collections::BTreeMap;

let mut doc = PdfDocument::open("financial.pdf")?;
let chars = doc.extract_chars(0)?;

let mut rows: BTreeMap<i32, Vec<_>> = BTreeMap::new();
for ch in &chars {
    let row_key = ((ch.y / 2.0).round() * 2.0) as i32;
    rows.entry(row_key).or_default().push(ch);
}

for (_, line_chars) in rows.iter().rev() {
    let mut sorted = line_chars.clone();
    sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
    let text: String = sorted.iter().map(|c| c.char).collect();
    println!("{}", text);
}

Go

doc, _ := pdfoxide.Open("financial.pdf")
defer doc.Close()

chars, _ := doc.ExtractChars(0)
rows := map[int][]pdfoxide.Char{}
for _, ch := range chars {
    key := int(math.Round(float64(ch.Y)/2) * 2)
    rows[key] = append(rows[key], ch)
}

keys := make([]int, 0, len(rows))
for k := range rows { keys = append(keys, k) }
sort.Sort(sort.Reverse(sort.IntSlice(keys)))

for _, y := range keys {
    line := rows[y]
    sort.Slice(line, func(i, j int) bool { return line[i].X < line[j].X })
    var b strings.Builder
    for _, c := range line { b.WriteString(c.Char) }
    fmt.Println(b.String())
}

C#

using var doc = PdfDocument.Open("financial.pdf");
var chars = doc.ExtractChars(0);

var rows = chars
    .GroupBy(c => (int)(Math.Round(c.Y / 2) * 2))
    .OrderByDescending(g => g.Key);

foreach (var row in rows)
{
    var line = string.Concat(row.OrderBy(c => c.X).Select(c => c.Char));
    Console.WriteLine(line);
}

Экспорт таблиц в Markdown

Когда содержимое PDF уходит в большую языковую модель, строится RAG-пайплайн или данные нужно сохранить в формате, удобном и человеку, и машине, Markdown — идеальный вариант. PDF Oxide печатает таблицы сразу в GitHub Flavored Markdown (GFM), так что лишнего шага конвертации не требуется.

from pdf_oxide import PdfDocument

doc = PdfDocument("quarterly-report.pdf")

# Собрать все таблицы со всех страниц в Markdown
all_tables = []
for i in range(doc.page_count()):
    md = doc.to_markdown(i, detect_headings=True)
    # Разрезать markdown на фрагменты и выделить блоки таблиц
    in_table = False
    current_table = []
    for line in md.split("\n"):
        if line.strip().startswith("|"):
            in_table = True
            current_table.append(line)
        else:
            if in_table and current_table:
                all_tables.append("\n".join(current_table))
                current_table = []
            in_table = False

    if current_table:
        all_tables.append("\n".join(current_table))

print(f"Найдено таблиц: {len(all_tables)}")
for idx, table in enumerate(all_tables):
    print(f"\n--- Таблица {idx + 1} ---")
    print(table)

GFM-вывод можно напрямую подставлять в промпты для LLM. Передавайте его без правок в вызов API OpenAI или Anthropic — модель разберёт табличную структуру без дополнительного форматирования:

# Передать извлечённую таблицу модели для анализа
prompt = f"""Проанализируй финансовую таблицу ниже и выдели ключевые тенденции:

{all_tables[0]}
"""

Этот путь заметно быстрее, чем извлекать таблицы pdfplumber, а затем самостоятельно переводить их в Markdown.

Многостраничные таблицы

Таблицы, разбитые по нескольким страницам, — классический вызов при работе с PDF. Финансовые отчёты, инвентарные ведомости и регулятивные подачи нередко содержат таблицы на две, пять или десятки страниц. Ключевая идея — извлекать таблицу с каждой страницы по отдельности, а затем сшивать строки, аккуратно обходя повторяющиеся заголовки и служебные элементы страницы.

from pdf_oxide import PdfDocument

doc = PdfDocument("long-report.pdf")

def extract_table_rows(md_text):
    """Извлечь строки таблицы из markdown и вернуть заголовок и данные отдельно."""
    header = None
    data_rows = []
    for line in md_text.split("\n"):
        line = line.strip()
        if not line.startswith("|") or line.startswith("|--"):
            continue
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        if header is None:
            header = cells
        else:
            data_rows.append(cells)
    return header, data_rows

# Собрать строки со всех страниц
combined_header = None
combined_rows = []

for i in range(doc.page_count()):
    md = doc.to_markdown(i)
    header, rows = extract_table_rows(md)

    if header is None:
        continue  # На этой странице таблицы нет

    if combined_header is None:
        combined_header = header
    elif header == combined_header:
        pass  # Пропустить повторяющийся заголовок на следующих страницах
    else:
        # Другая таблица — сохранить текущую и начать новую
        print(f"Найдена таблица на {len(combined_rows)} строк")
        combined_header = header
        combined_rows = []

    combined_rows.extend(rows)

if combined_header and combined_rows:
    print(f"Колонки: {combined_header}")
    print(f"Всего строк: {len(combined_rows)}")
    for row in combined_rows[:5]:
        print(row)
    if len(combined_rows) > 5:
        print(f"... ещё {len(combined_rows) - 5} строк")

Паттерн надёжно работает для таблиц, у которых заголовок повторяется на каждой странице (самый частый случай). Если заголовок появляется только на первой странице, логику можно упростить: взять заголовок с первой страницы с таблицей, а все последующие строки считать данными.

Экспорт таблиц в CSV или DataFrame

Когда таблица извлечена, обычно нужен структурированный формат для дальнейшего анализа. Примеры ниже показывают, как всего за пару строк дойти от PDF до DataFrame pandas или CSV-файла.

Пакетный экспорт: каждую таблицу — в отдельный CSV

import csv
from pdf_oxide import PdfDocument

doc = PdfDocument("catalog.pdf")
table_count = 0

for i in range(doc.page_count()):
    md = doc.to_markdown(i)
    rows = []
    for line in md.split("\n"):
        line = line.strip()
        if line.startswith("|") and not line.startswith("|--"):
            cells = [cell.strip() for cell in line.split("|")[1:-1]]
            rows.append(cells)

    if len(rows) > 1:  # Хотя бы заголовок и одна строка данных
        table_count += 1
        filename = f"table_page{i + 1}_{table_count}.csv"
        with open(filename, "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerows(rows)
        print(f"Сохранён {filename} (строк данных: {len(rows) - 1})")

print(f"Всего экспортировано таблиц: {table_count}")

Многостраничная таблица в DataFrame

Для таблиц через несколько страниц объедините шаблон сшивания с pandas:

import pandas as pd
from pdf_oxide import PdfDocument

doc = PdfDocument("financial-statement.pdf")

header = None
all_rows = []

for i in range(doc.page_count()):
    md = doc.to_markdown(i)
    for line in md.split("\n"):
        line = line.strip()
        if not line.startswith("|") or line.startswith("|--"):
            continue
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        if header is None:
            header = cells
        elif cells == header:
            continue  # Пропустить повторяющийся заголовок
        else:
            all_rows.append(cells)

if header and all_rows:
    df = pd.DataFrame(all_rows, columns=header)
    # Привести числовые колонки к нужному виду
    for col in df.columns:
        # Попытаться преобразовать колонки, выглядящие числовыми
        cleaned = df[col].str.replace(r"[$,%]", "", regex=True).str.strip()
        try:
            df[col] = pd.to_numeric(cleaned)
        except (ValueError, TypeError):
            pass  # Оставить как строку

    print(df.dtypes)
    print(df.head(10))
    df.to_csv("financial_data.csv", index=False)

На выходе — аккуратный DataFrame с правильными числовыми типами, готовый для анализа в pandas, построения графиков в matplotlib или загрузки в базу данных.

Сложные таблицы: когда пригодится pdfplumber

Распознавание таблиц в PDF Oxide хорошо справляется со стандартными выровненными таблицами. В сложных случаях — объединённые ячейки, заголовки на несколько колонок, таблицы без рамок, многострочное содержимое — специализированные алгоритмы pdfplumber оказываются устойчивее:

import pdfplumber

with pdfplumber.open("complex-report.pdf") as pdf:
    page = pdf.pages[0]
    tables = page.extract_tables()
    for table in tables:
        for row in table:
            print(row)

Когда что применять

Сценарий Рекомендуется
Простые выровненные таблицы PDF Oxide (в 29 раз быстрее)
Таблицы внутри Markdown всей страницы PDF Oxide
Сложные объединённые ячейки / заголовки на несколько колонок pdfplumber
Таблицы без рамок pdfplumber
Пакетная обработка, где важна скорость PDF Oxide

Использовать оба сразу

Быстрое извлечение текста — PDF Oxide, сложные таблицы — pdfplumber:

from pdf_oxide import PdfDocument
import pdfplumber

# Быстрое извлечение полного текста
doc = PdfDocument("report.pdf")
text = doc.extract_text(0)

# Прицельное извлечение таблиц на сложных страницах
with pdfplumber.open("report.pdf") as pdf:
    tables = pdf.pages[0].extract_tables()

Связанные страницы