Skip to content

Python PDF テーブル抽出

PDF からのテーブル抽出は、ドキュメント処理パイプラインで最も頻出する作業の一つです。年次報告書から財務データを取り出す場合でも、製品カタログをスクレイピングする場合でも、構造化データを LLM に流し込む場合でも、信頼できるテーブル抽出は欠かせません。本ガイドでは、ワンライナーから複数ページにまたがる本番品質のワークフローまで、Python で PDF テーブルを抽出するために必要なことを一通り押さえます。

検出エンジン

PDF Oxide は、エッジ → スナップ/マージ → 交点 → セル → グループという汎用テーブル検出パイプラインを採用しています。Tabula、pdfplumber、PyMuPDF と同じ考え方を、純粋な Rust で実装したものです。

検出機能:

  • 交点ベース — 水平×垂直の線の交差を見つけ、四隅の矩形からセルを構築し、Union-Find でテーブルにグループ化します。
  • 拡張グリッド — 水平線と垂直線がページ上の異なる領域に存在する場合、全座標のデカルト積から仮想グリッドを構築します。
  • カラム対応のテキスト検出 — X 射影ヒストグラムで 2 カラムのレイアウトを分割し、カラムごとにテキストのみのテーブル検出を実行します。
  • H 罫で囲まれたテキストテーブル — 水平罫線のみで囲まれ垂直線を持たないテーブルを検出します(学術論文で多く見られます)。
  • 行のハイブリッド検出 — 垂直境界しかない場合に、テキストの Y 位置から行の境界を推定します(請求書の明細行など)。
  • 点線・破線の再構成 — 短い線分をつないで連続したエッジに復元します。
  • セクション区切りでの分割 — ページ幅いっぱいの水平区切り線で、複数セクションのフォームを分割します。
  • エッジ被覆フィルタ — どのグリッドにも参加しない孤立エッジを取り除きます。

設定

TableDetectionConfig は次の調整可能なパラメータを公開します。

フィールド 既定値 説明
horizontal_strategy "lines_strict" "lines_strict""lines""text""explicit"
vertical_strategy "lines_strict" 同じ語彙
v_split_gap 20.0 pt 別テーブルへ分割する垂直線間のギャップ(v0.3.20 以前は 4pt にハードコード)
snap_tolerance 3.0 pt 近接するエッジのマージ許容値
text_tolerance 3.0 pt テキスト行のマージ許容値

挙動の変更

v0.3.20 以降、Python の extract_tables() の既定戦略は Both(線とテキストの両方で検出)です。以前のテキスト専用挙動に依存していたページでは、horizontal_strategy="text"vertical_strategy="text" を明示的に渡してください。

Python バインディングは table_settings dict の vertical_strategy を正しく読み取るようになりました。以前は暗黙のうちに無視されていました。

レンダリング

抽出されたテーブルは、以前の ASCII 罫線の代わりに、スペースでパディングした列揃えで出力されます。通貨や数値の列は自動で右寄せになります。フォーム番号のプレフィックス("1 Apr 11""Apr 11")や装飾的なダッシュ/アンダースコアのセル("------")はレンダリング時に取り除かれます。

Markdown 変換を使って PDF からテーブルデータを抽出します。

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 形式そのものの根本的な制約に由来します。

PDF には「テーブル」という概念がありません。 HTML が <table><tr><td> タグでテーブル構造を表すのに対し、PDF ファイルには描画命令しか保存されません。座標 (x, y) にこのグリフを置く、点 A から点 B へ線を引く、といった命令です。「これらの文字は 3 行 2 列のセルに属する」と伝える意味的なレイヤーは存在しません。テーブル抽出ライブラリはすべて、ページ上のテキストや線の空間的な位置を解析して、その構造を復元しなければなりません。

この復元がいくつもの理由で難しいのです。

  • 罫線ありと罫線なしのテーブル。 見える罫線があれば、抽出ツールはそれをセル境界として使えます。財務諸表、行政レポート、学術論文に多い罫線なしのテーブルには線そのものがありません。列の境界は、テキストブロック間の空白の隙間だけから推定するしかなく、列幅が可変だったり数値が右寄せされていたりすると誤りが出やすくなります。

  • 結合セルとまたがるヘッダー。 3 列にまたがるヘッダーセルは、見た目には 1 つの広いテキストブロックにしか映りません。境界線がなければ、パーサーはヘッダーがどの列をカバーしているかを確実に判別できません。これをうまく扱うライブラリもありますが、多くは無言で壊れた出力を返します。

  • セルに複数行のコンテンツ。 セル内に折り返しのある段落が入っている場合、素朴な行ベースのパーサーは折り返し行それぞれを別の行として扱います。再び 1 つのセルにまとめるには、各行の垂直方向の広がりを理解する必要があります。

  • 複数ページにまたがるテーブル。 大きなテーブルはしばしば 2 ページ以上にまたがります。ヘッダー行はページごとに繰り返されることもあれば、そうでないこともあり、行と行の間にフッター、透かし、ページ番号が挟まることもあります。これらの断片を 1 つの整合したテーブルに縫い合わせるには、ページ単位のロジックが欠かせません。

  • 回転テキストや非標準のレイアウト。 PDF の中には列ヘッダーを回転させて表示するものや、多段組のページレイアウトにテーブルを配置するものもあります。こうしたエッジケースは、ほとんどのパーサーが前提としている「左から右、上から下」の読み順を崩します。

これらの難しさを理解すれば、手元のドキュメントに合ったツールを選びやすくなります。多くの請求書、注文確認、簡易なレポートに出てくる整った整列済みテーブルなら、PDF Oxide のような高速な空間解析方式で十分に扱えます。複雑な結合や罫線なしのレイアウト、不規則なフォーマットを含むドキュメントでは、より洗練されたヒューリスティクスを備えたライブラリが必要になることもあります。

テーブル抽出:PDF Oxide と他ライブラリの比較

Python で PDF からテーブルを抽出するライブラリを選ぶ際は、対象ドキュメント、性能要件、必要な出力形式で判断します。主な選択肢を比較します。

ライブラリ テーブル検出 罫線ありテーブル 罫線なしテーブル 出力形式 速度
PDF Oxide 組み込み はい 基本 Markdown/HTML 0.8 ms
pdfplumber 組み込み はい 高度 Python リスト 23.2 ms
Camelot 組み込み はい はい(lattice/stream) DataFrame ~50 ms+
PyMuPDF 基本(v1.23+) はい 限定的 DataFrame 4.6 ms
pypdf なし なし なし なし なし
tabula-py 組み込み はい はい DataFrame ~100 ms+(Java)

PDF Oxide は圧倒的に最速の選択肢です。整列したテキストブロックの空間解析でテーブルを検出し、クリーンな GitHub Flavored Markdown テーブルを出力します。抽出の平均時間 0.8 ms は、pdfplumber より 29 倍、tabula-py より 100 倍以上高速です。罫線ありのテーブルや、シンプルに整列した罫線なしテーブルをうまく処理できます。どのみち Markdown 出力が必要な LLM パイプラインでは自然な選択肢です。

pdfplumber は罫線なしテーブルの検出が最も成熟しています。find_tables() メソッドはテキストの整列に基づいて行と列を検出する設定可能な戦略を備え、結合セルや複数行のセル内容を他の多くの選択肢よりうまく扱います。代償は速度で、1 ページ 23.2 ms は大量処理では明らかに遅く感じられます。

Camelot は 2 つの検出モードを提供します。罫線ありには lattice、罫線なしには stream です。pandas DataFrame を直接生成するのでデータ分析のワークフローでは便利ですが、Ghostscript と OpenCV に依存するためインストールが重く、速度は純 Python の選択肢の中で最も遅い部類に入ります。

PyMuPDF (fitz) はバージョン 1.23 で基本的なテーブル抽出を追加しました。高速(4.6 ms)で、シンプルな罫線ありテーブルには十分ですが、罫線なしテーブルへの対応は pdfplumber や Camelot に比べると限定的です。

pypdf にはテーブル検出の機能はありません。生テキストしか抽出しないため、テーブル構造を復元するには自前のパーサーを書く必要があります。

tabula-py は Java 製の Tabula ライブラリを Python から包んだものです。罫線ありとなしの両方で良好な検出を提供しますが、Java ランタイムが必要で、JVM の起動コストがあるため最も遅い選択肢です。大量処理のパイプラインより、単発の抽出タスクに向きます。

多くの本番用途では、速度と手軽さを両立する PDF Oxide を主な抽出器として使い、複雑な表レイアウトで高度なヒューリスティクスを要するドキュメントのサブセットに対してのみ pdfplumber へフォールバックするのが定石です。

インストール

pip install pdf_oxide

基本的なテーブル抽出

Markdown テーブルとして

最もシンプルな方法は、ページを 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)

Pandas DataFrame へのエクスポート

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  # 2pt グリッドにスナップ
    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; // 2pt グリッドにスナップ
    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 のプロンプトに直接使えます。OpenAI や Anthropic の API 呼び出しにそのまま渡せば、モデルは追加の整形なしにテーブル構造を理解します。

# 抽出したテーブルを LLM に渡して分析する
prompt = f"""次の財務テーブルを分析し、主要なトレンドを要約してください:

{all_tables[0]}
"""

このやり方は、pdfplumber でテーブルを抽出してから自前で Markdown に変換するよりもずっと高速です。

複数ページにまたがるテーブルの扱い

複数ページにまたがるテーブルは、PDF 抽出の定番の難問です。財務諸表、在庫リスト、規制当局提出書類などには、2 ページ、5 ページ、ときには数十ページにわたるテーブルがよく登場します。ポイントは、ページごとに別々にテーブルを抽出し、ヘッダーの繰り返しやページの余計な要素を丁寧に処理しながら、行をつなぎ合わせることです。

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} 行")

このパターンは、ヘッダー行が各ページで繰り返されるテーブル(最も一般的なケース)で確実に動きます。ヘッダーが最初のページにしか現れないテーブルなら、最初にテーブルを含むページからヘッダーを 1 度だけ取得し、以降の行はすべてデータとして扱うように単純化できます。

テーブルを CSV や DataFrame にエクスポート

テーブルデータを抽出したら、追加分析のために構造化された形式で欲しくなるはずです。以下の例では、PDF から pandas DataFrame や 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:  # ヘッダー + データ行が最低 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()

関連ページ