Технічна стаття

Вилучення таблиць зі сторінок PDF у Delphi з PDFium

PDFium Component виявляє таблиці на сторінці PDF і повертає їх як сітку клітинок з охопленнями рядків і стовпців, рядками заголовків та значенням достовірності, через ExtractTables для однієї сторінки та ExtractDocumentTables для всього документа. Кожна таблиця перетворюється на CSV або JSON одним викликом, а таблиці, що продовжуються через розрив сторінки, можна пов'язати в ланцюжок продовження

У PDF немає об'єкта таблиці. Таблиця в PDF — це набір текстових фрагментів, розташованих так, щоб людина читала їх як сітку, іноді з намальованими навколо лініями, а часто й без них. Відновлення сітки означає реконструкцію наміру, який файл ніколи не записував, тому кожен інструмент вилучення дає трохи інший результат і тому інструмент, що повідомляє свою достовірність, кориснiший за той, що цього не робить

Два режими виявлення для двох типів таблиць

Виявлення за лініями використовує намальовані лінії. Кожен сегмент обведеного (stroked) шляху перетворюється в координати сторінки через матрицю об'єкта сторінки, горизонтальні та вертикальні лінії перетинаються, а перетини утворюють зв'язні компоненти. Кожен компонент стає власною впорядкованою сіткою позицій X та Y — саме це не дає двом окремим таблицям на одній сторінці злитися в одну беззмістовну сітку

Виявлення за пробілами обробляє таблиці, намальовані вирівнюванням замість ліній. Прямокутники слів групуються у візуальні рядки, проміжки всередині рядка розбивають його на кандидатів у стовпці, і таблиця приймається лише тоді, коли принаймні MinRows рядків повторюють принаймні MinColumns прив'язок, вирівняних по лівому краю, у межах AlignmentTolerance. Коефіцієнт проміжку між рядками за замовчуванням дорівнює 3, що покриває приблизно 30-пунктовий інтервал базових ліній, типовий для 12-пунктового тексту, не дозволяючи одному рядку з кількома текстовими фрагментами видавати себе за таблицю

Схема конвеєра виявлення таблиць PDFium Component у Delphi, де перетини намальованих ліній і вирівняні рядки слів надходять в один оцінений запис таблиці з експортом у CSV та JSON
Виявлення за лініями перетинає намальовані штрихи, тоді як виявлення за пробілами підраховує вирівняні рядки прямокутників слів; кандидати, що долають 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

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // за замовчуванням 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;

Як відновлюються об'єднані клітинки?

Це та частина, у якій наївні екстрактори помиляються. Об'єднану клітинку неможливо виявити лише за загальною сіткою, адже сітка виводиться з усіх ліній на сторінці, а об'єднана область просто не має внутрішньої лінії, яка б її розділяла

Правило, що застосовується тут, локальне: дві сусідні базові клітинки об'єднуються, коли жодна межова лінія не покриває інтервал між ними. Union-find об'єднує їх, отримані прямокутні компоненти стають значеннями RowSpan та ColumnSpan, а текст призначається базовій клітинці за її центральною точкою й потім слідує за цією клітинкою до її кореня об'єднання. Такий підхід також тримає вартість лінійною відносно слів плюс клітинок, замість квадратичного сканування, яке дало б перевірка кожного слова проти кожної клітинки

Схема відновлення об'єднаних клітинок при вилученні таблиць PDFium для Delphi, де union-find об'єднує сусідні базові клітинки щоразу, коли жодна межова лінія не покриває інтервал між ними, даючи RowSpan і ColumnSpan
Union-find об'єднує сусідні базові клітинки, чий спільний інтервал не має намальованої межі, тож об'єднаний заголовок повертається як одна клітинка зі встановленим ColumnSpan замість однієї заповненої клітинки, оточеної порожніми

Практичний ефект полягає в тому, що фінансова таблиця з об'єднаним заголовком «Total», що охоплює три стовпці, виходить як одна клітинка з охопленням три, а не як одна заповнена клітинка й дві загадково порожні

Продовження між сторінками

Довгі таблиці розриваються між сторінками, і трактування фрагмента кожної сторінки як незалежної таблиці змушує викликача зшивати їх самостійно. ExtractDocumentTables натомість може пов'язати їх, але лише за суворих умов: фрагмент має бути найнижчою таблицею на попередній сторінці, наступний — найвищою таблицею на наступній сторінці, номери сторінок мають бути суміжними, а межі стовпців мають збігатися

Саме всі чотири умови разом запобігають очевидній помилці — зв'язуванню кожної чотиристовпцевої таблиці в документі в одну уявну мегатаблицю лише тому, що вони випадково мають однакову кількість стовпців. Коли умови виконуються, таблиці отримують спільний ідентифікатор групи продовження й несуть метадані продовження; коли ні — ви отримуєте окремі таблиці й можете вирішувати самі

Схема продовження таблиці між сторінками PDF у Delphi, де чотири суворі умови вирішують, чи приєднується найнижчий фрагмент однієї сторінки до найвищого фрагмента наступної
Вилучення на рівні документа зв'язує фрагменти лише тоді, коли виконуються всі чотири умови, що не дає непов'язаним чотиристовпцевим таблицям злитися в одну уявну мегатаблицю

Вилучення на рівні документа розподіляє бюджети MaxCells та MaxTables між сторінками, а не скидає їх для кожної сторінки, і відновлює активну сторінку в блоці finally, тож запуск вилучення в переглядачі залишає користувача на тій сторінці, на якій він був

Експорт без пошкодження даних

Обидва експортери свідомо підходять до екранування. CSV завжди бере поля в лапки й подвоює внутрішні лапки, що уникає класичного збою, коли клітинка з комою мовчки перетворюється на два стовпці. Для об'єднаних клітинок вміст видається лише у верхній лівій прив'язці, тож круговий прохід через CSV не дублює охоплюючий заголовок по всіх стовпцях, які він покриває

JSON зберігає Unicode замість екранування в ASCII, екранує керівні символи та включає метадані, потрібні споживачу для оцінки якості: режим виявлення, достовірність, межі, значення охоплень, прапорці заголовків та інформацію про продовження. Якщо ви передаєте вилучені таблиці в подальшу систему, надавайте перевагу JSON, адже рядок CSV не може повідомити вам, що таблиця, з якої він походить, отримала достовірність 0,51:

// Вилучення для всього документа, зберігаються лише таблиці, вартi довіри
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;

Налаштування — і знання, коли зупинитися

Три налаштування важливіші за решту. MinConfidence — це поріг якості, і значення 0,5 навмисно поблажливе; підвищуйте його для автоматизованого прийому даних і знижуйте для інтерфейсу перевірки, де людина підтверджує кожен результат. MinColumnGap визначає, що вважається межею стовпця в режимі пробілів, і щільно набрані таблиці в насичених звітах можуть потребувати зменшення цього значення з типових 12 пунктів. MaxRowGapFactor визначає, коли вертикальна відстань завершує таблицю, що важливо для таблиць з випадковими порожніми рядками

Варто чесно визнати межі можливостей. Таблиці з лініями вилучаються надійно. Охайно вирівняні таблиці з пробілами вилучаються добре. Таблиці з поверненим текстом, вкладені таблиці або клітинки, чий вміст переноситься так, що виглядає як ще один рядок, потребуватимуть перевірки незалежно від налаштувань параметрів. Для них модель структурованого тексту дає вихідний матеріал для побудови читача, специфічного для предметної області, описаного в статті блоки структурованого тексту та порядок читання

Одне корисне поєднання: коли скановий документ узагалі не має тексту, виявленню таблиць немає з чим працювати, доки не з'явиться текстовий шар. Спочатку додайте його, як описано в статті додавання шару тексту з можливістю пошуку до сканованих PDF, а потім вилучайте. Прямокутники слів, які повертає постачальник OCR, — саме той вхід, який потрібен виявленню за пробілами

Вилучення таблиць, структурований текст та зміна потоку тексту (reflow) читають з тієї самої моделі сторінки в Delphi, C++Builder та Lazarus; повний опис API наведено на сторінці PDFium Component для Delphi