技術記事

PDFiumによるDelphiでのPDFページからのテーブル抽出

PDFium Componentは、ExtractTables(1ページ用)とExtractDocumentTables(ドキュメント全体用)を通じて、PDFページ上のテーブルを検出し、行・列のスパン、ヘッダー行、信頼度の値を持つセルのグリッドとして返す。各テーブルは1回の呼び出しでCSVまたはJSONに変換でき、改ページをまたいで続くテーブルは継続チェーンとしてリンクできる

PDFにはテーブルオブジェクトが存在しない。PDFにおけるテーブルとは、人間がグリッドとして読めるように配置されたテキストランの集合であり、線が引かれていることもあれば、多くの場合そうでないこともある。グリッドを復元することは、ファイルが一度も記録していない意図を再構築することを意味し、それこそがどの抽出ツールもわずかに異なる結果を生む理由であり、自分の信頼度を教えてくれるツールが、そうでないツールより有用な理由でもある

2種類のテーブルに対応する2つの検出モード

罫線検出は描かれた線を利用する。各ストロークされたパスセグメントはページオブジェクトの行列を通じてページ座標へ変換され、水平線と垂直線が交差させられ、その交点が連結成分を形成する。各成分はそれぞれ独自のソート済みX・Y位置のグリッドになる。これにより、同じページ上の2つの別々のテーブルが1つの意味をなさないグリッドに統合されてしまうことが防がれる

空白検出は、線ではなく整列によって描かれたテーブルを扱う。単語ボックスは視覚的な行にグループ化され、行内の隙間がその行を候補となる列に分割し、少なくともMinRows行がAlignmentTolerance以内で少なくともMinColumns個の左揃えアンカーを繰り返す場合にのみテーブルとして受理される。行の隙間係数は既定で3であり、これは12ポイントのテキストに典型的な約30ポイントのベースライン間隔をカバーしつつ、複数のテキストランを含む1行がテーブルを装うことを防ぐ

DelphiにおけるPDFium Componentのテーブル検出パイプラインを示す図:罫線の交点と空白整列された単語行の両方が、CSVおよびJSONエクスポートを備えた1つのスコア付きテーブルレコードへ供給される
罫線検出は描画されたストロークを交差させ、空白検出は整列した単語ボックスの行を数える。MinRowsとMinColumnsをクリアした候補は、信頼度スコアとDetectionModeを伴って現れる
uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfTableExtractionOptions;
  Tables: TPdfTables;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'annual-report.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 12;                    // 1-based

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // default is 0.5
    Options.HeaderRowCount := 1;

    Tables := Pdf.ExtractTables(Options);
    for I := 0 to High(Tables) do
      Writeln(Format('table %d: %dx%d cells, confidence %.2f, mode %d',
        [I, Tables[I].RowCount, Tables[I].ColumnCount,
         Tables[I].Confidence, Ord(Tables[I].DetectionMode)]));

    if Length(Tables) > 0 then
      SaveText('page12-table0.csv', Tables[0].ToCsv);
  finally
    Pdf.Free;
  end;
end;

結合セルはどのように復元されるか

これは素朴な抽出ツールが誤りやすい部分である。結合セルはグローバルなグリッドだけからは特定できない。グリッドはページ上のすべての線から導出されるものであり、結合された領域には、それを分けていたはずの内部線がそもそも存在しないからである

ここで使われる規則はローカルなものである:2つの隣接する基本セルの間を覆う境界線が存在しない場合、それらは結合されているとみなされる。Union-Findによってそれらが結合され、その結果得られる矩形の成分がRowSpanColumnSpanの値になり、テキストはその中心点によって基本セルへ割り当てられ、その後そのセルの結合ルートへ従う。この方法にはもう1つの利点があり、すべての単語をすべてのセルに対して照合する二次的なスキャンではなく、単語数とセル数の合計に対して線形なコストで済む

DelphiのPDFiumテーブル抽出における結合セルの復元を示す図:隣接する基本セル間の区間を覆う境界線が存在しない場合にUnion-Findがそれらを結合し、RowSpanとColumnSpanを生み出す
Union-Findは、共有する区間に描画された境界を持たない隣接する基本セルを結合する。そのため結合されたヘッダーは、値の入った1セルとその両脇の空セルとしてではなく、ColumnSpanが設定された1つのセルとして返される

実用上の効果として、3列にまたがる結合された「合計」ヘッダーを持つ財務テーブルは、1つの値が入ったセルと2つの謎めいた空セルとしてではなく、スパン3の1つのセルとして出てくる

ページをまたぐ継続

長いテーブルはページをまたいで分割され、各ページの断片を独立したテーブルとして扱うと、呼び出し側がそれらを縫い合わせなければならなくなる。ExtractDocumentTablesは代わりにそれらをリンクできるが、それは厳格な条件下でのみ行われる:断片が前のページの最下部のテーブルであり、次のものが後続ページの最上部のテーブルであり、ページ番号が隣接しており、そして列の境界が一致していることである

この4つの条件がすべて揃うことが、明白な誤り——ドキュメント内のすべての4列テーブルを、たまたま列数が一致するというだけで1つの架空の巨大テーブルへ連結してしまうこと——を防いでいる。条件が満たされる場合、それらのテーブルは継続グループ識別子を共有し継続メタデータを持つ。満たされない場合は別々のテーブルとなり、自分で判断できる

Delphiにおけるページをまたぐテーブル継続を示す図:4つの厳格なゲートが、あるページの最下部の断片を次のページの最上部の断片と結合するかどうかを判定する
ドキュメントレベルの抽出は、4つのゲートすべてが成立する場合にのみ断片をリンクする。これにより無関係な4列テーブルが1つの架空の巨大テーブルへ融合してしまうことを防ぐ

ドキュメントレベルの抽出は、MaxCellsMaxTablesの予算をページごとにリセットするのではなくページをまたいで共有し、アクティブなページはfinallyブロックで復元されるため、ビューア内で実行された抽出処理は、ユーザーがいたページをそのまま見せ続ける

データを壊さずにエクスポートする

両方のエクスポーターはエスケープについて慎重である。CSVは常にフィールドを引用符で囲み、内部の引用符を二重化するため、カンマを含むセルが静かに2つの列になってしまうという典型的な失敗を避けられる。結合セルについては、内容は左上のアンカーでのみ出力されるため、CSVの往復変換によってスパンするヘッダーがカバーする列全体に重複することはない

JSONはUnicodeをASCIIへエスケープするのではなくそのまま保持し、制御文字はエスケープし、消費者が品質を判断するために必要なメタデータ——検出モード、信頼度、範囲、スパンの値、ヘッダーフラグ、継続情報——を含む。抽出したテーブルを下流のシステムへ渡す場合はJSONを優先すること。CSVの1行では、その元になったテーブルが0.51の信頼度だったことを伝えられないからである

// Document-wide extraction, keeping only tables worth trusting
Tables := Pdf.ExtractDocumentTables(Options);
for I := 0 to High(Tables) do
begin
  if Tables[I].Confidence < 0.75 then
  begin
    Log(Format('page %d table needs review (%.2f)',
      [Tables[I].PageNumber, Tables[I].Confidence]));
    Continue;
  end;
  if Tables[I].ContinuationGroup > 0 then
    AppendToGroup(Tables[I].ContinuationGroup, Tables[I].ToJson)
  else
    EmitStandalone(Tables[I].ToJson);
end;

チューニング、そしてどこで止めるべきかを知る

他より重要な設定が3つある。MinConfidenceは品質ゲートであり、0.5は意図的に緩めに設定されている。自動取り込みでは引き上げ、人間が各結果を確認するレビューUIでは下げるとよい。MinColumnGapは空白モードで何が列境界とみなされるかを決め、密度の高いレポート内でぎっしり詰まったテーブルでは、12ポイントの既定値から縮める必要があるかもしれない。MaxRowGapFactorは垂直方向の距離がいつテーブルの終わりを意味するかを決め、時折空行のあるテーブルにとって重要である

限界については正直であるべきである。罫線付きのテーブルは確実に抽出できる。きれいに整列した空白テーブルもうまく抽出できる。回転したテキスト、入れ子になったテーブル、あるいは折り返されて別の行のように見えるセルを持つテーブルは、パラメータをどう設定してもレビューが必要になる。そうしたケースには、構造化テキストモデルがドメイン特化のリーダーを構築するための素材を与えてくれる。これは構造化テキストブロックと読み上げ順序で説明されている

役立つ組み合わせを1つ。スキャンされたドキュメントに全くテキストがない場合、テキストレイヤーが存在するまでテーブル検出には手がかりがない。まずスキャンされたPDFへの検索可能なテキストレイヤーの追加で説明されている方法でテキストレイヤーを追加し、それから抽出すること。OCRプロバイダーが返す単語ボックスは、まさに空白検出が必要とする入力そのものである

テーブル抽出、構造化テキスト、リフローはいずれも、Delphi、C++Builder、Lazarusにおいて同じページモデルから読み取りを行う。完全なAPIはDelphi向けPDFium Componentページに記載されている