Техническа статия

Извличане на таблици от PDF страници в Delphi с PDFium

PDFium Component открива таблици на PDF страница и ги връща като мрежа от клетки с обхвати на редове и колони, редове на заглавие и стойност на увереност, чрез ExtractTables за една страница и ExtractDocumentTables за цял документ. Всяка таблица се преобразува в CSV или JSON с едно извикване, а таблици, продължаващи през прекъсване на страница, могат да бъдат свързани във верига от продължения

PDF няма обект за таблица. Таблица в PDF е набор от текстови редове, позиционирани така, че човек ги чете като мрежа, понякога с начертани около тях линии, а често без. Възстановяването на мрежата означава реконструиране на намерение, което файлът никога не е записал, поради което всеки инструмент за извличане произвежда леко различни резултати и защо инструмент, който ви казва увереността си, е по-полезен от такъв, който не го прави

Два режима на откриване за два вида таблица

Линираното откриване използва начертаните линии. Всеки очертан сегмент от път се трансформира в координати на страницата чрез матрицата на обекта на страницата, хоризонталните и вертикалните линии се пресичат, а пресечните точки образуват свързани компоненти. Всеки компонент става своя собствена сортирана мрежа от 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, екранира контролни знаци и включва метаданните, от които консуматор се нуждае, за да прецени качеството: режим на откриване, увереност, граници, стойности на обхват, флагове за заглавие и информация за продължение. Ако подавате извлечени таблици на downstream система, предпочетете JSON, защото CSV ред не може да ви каже, че таблицата, от която идва, е получила оценка на увереността 0,51:

// Извличане за целия документ, запазвайки само таблиците, на които може да се вярва
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, са точно входът, от който откриването чрез празни пространства се нуждае

Извличането на таблици, структурираният текст и преформатирането четат от един и същ модел на страница в Delphi, C++Builder и Lazarus; пълното API е описано на страницата на PDFium Component за Delphi