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

Структурирано извличане на PDF текст с PDFium VCL

PDFiumPas връща текста на страница като структура, а не като низ. GetStructuredText произвежда TPdfStructuredTextPage, съдържащ блокове, всеки от които съдържа редове, всеки от които съдържа стилизирани span-ове, с граници в пространството на страницата на всяко ниво и запазени индекси на изходните знаци, така че всеки фрагмент може да бъде картографиран обратно към базовата текстова страница

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

Защо плосък низ е грешният изход за повечето задачи?

Защото въпросите, които хората задават към извлечен текст, почти никога не са „кои знаци са на тази страница“. Те са „какво е заглавието“, „това таблица ли е“, „принадлежи ли този параграф на раздел 4“, „къде да нарисувам маркирането“. Единичен низ не отговаря на нито един от тях, а всеки отговор, който реконструирате от него, е евристика, която вече притежавате

Двуколонните оформления илюстрират идеята конкретно. Извлечете двуколонна статия като низ и, в зависимост от начина, по който производителят е написал content stream-а, може да получите колона едно, последвана от колона две, или може да получите ред едно от колона едно, ред едно от колона две, ред две от колона едно и така надолу по страницата. И двете излизат от съответстващ PDF. Никое от двете не е грешно на ниво формат, защото PDF описва знаци на страница, не структура на документ. Модел, базиран на блокове, позволява на екстрактора да вземе решението за реда изрично и да ви каже кое решение е взел

Ред на съдържанието или физическо оформление?

TPdfStructuredTextOptions.ReadingOrder избира между roContentOrder и roPhysicalLayout, а правилният отговор зависи от онова, на което имате повече доверие — производителя или геометрията

Редът на съдържанието връща текста в последователността, в която content stream-ът го изчертава. Това е бързо и за документи, генерирани от добре възпитан производител, обикновено е предвиденият ред на четене. Физическото оформление игнорира последователността в stream-а и възстановява реда от онова, където знаците реално се намират, групирайки ги в редове, а после в колони. Точно това искате за сканирани и после OCR-нати страници, за изход от инструменти, излъчващи текст по ред на шрифта, а не по ред на четене, и за всичко, при което визуалният резултат е единственото, на което можете да разчитате

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfStructuredTextOptions;
  Page: TPdfStructuredTextPage;
  B, L: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'article.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;                     // с база 1

    Options := TPdfStructuredTextOptions.Default;
    Options.ReadingOrder := roPhysicalLayout;
    Options.IncludeFontInfo := True;
    Options.IncludeSemantics := True;
    Options.MaxCharacters := 200000;         // fail-closed бюджет

    Page := Pdf.GetStructuredText(Options);

    for B := 0 to High(Page.Blocks) do
    begin
      if Page.Blocks[B].Kind = cfHeading then
        Emit(Format('H%d: %s',
          [Page.Blocks[B].HeadingLevel, Page.Blocks[B].Text]))
      else
        for L := 0 to High(Page.Blocks[B].Lines) do
          Emit(Page.Blocks[B].Lines[L].Text);
    end;
  finally
    Pdf.Free;
  end;
end;

Какво добавя таг-ирането, което геометрията не може?

Намерение. С включен IncludeSemantics блоковете от таг-иран PDF носят Kind, взет от дървото на структурата, така че заглавие е заглавие, защото производителят го е казал, не защото шрифтът му е бил по-голям от средния. Видовете покриват формите, които имат значение за преизползване: cfParagraph, cfHeading с HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure и untagged резервния вариант cfPlain

Полето Source записва откъде идва всяка класификация, rosStructure за дървото на структурата и rosHeuristic за извеждане, което е полето за логване, когато решавате колко да се доверявате на конвейер за извличане през набор от документи. Фигурите са специален случай, който си струва да се знае: за cfFigure блок текстът идва от алтернативното описание, а не от каквито и да е глифи, тъй като фигурата няма собствени знаци. Несъпоставен алтернативен текст все още се представя, вместо да бъде изхвърлен, което е онова, което позволява на одит за достъпност да види, че описание съществува дори когато нищо на страницата не го изчертава. Самият модел на таг-иране е разгледан в валидацията на дървото на структурата за PDF/UA

Span-овете носят стилизирането и произхода

Всеки TPdfStructuredTextSpan носи своя текст, границите си в пространството на страницата, FontName, FontSize, FontWeight и Angle, плюс SourceStartIndex и SourceCharacterCount. Span-овете се прекъсват там, където стилизирането се променя, така че изречение с три получерни думи се превръща в три span-а, а възстановяването на подчертаване в HTML или Markdown е въпрос на четене на свойства, а не на отгатване от имена на шрифтове

Двете полета за изходен индекс са онези, които превръщат извличането във функция, а не в отчет. Те сочат обратно към последователността от знаци на страницата, което означава, че блок, съвпаднал в търсене, може да бъде превърнат в геометрия за селекция на ниво знак или правоъгълник за маркиране без втори, различно подреден пас върху текста; механиката е описана в визуалната селекция на текстов ред с char boxes. Полето Angle има по-голямо значение, отколкото изглежда: завъртян текст в печат или воден знак попада в същото координатно пространство като основния текст, а конвейер, игнориращ ъгъла, с радост ще слее диагонално „DRAFT“ в средата на параграф

Бюджет и двата брояча за качество

MaxCharacters е fail-closed бюджет, не настройка за отрязване: страница, надхвърляща го, спира, вместо тихо да върне част от съдържанието. На ненадежден път за прием точно такова е поведението, което искате, защото страница с милион знаци е или машинно генериран монстър, или опит да превърне екстрактора ви в най-бавната част на системата

Два брояча на върнатата страница описват директно качеството на извличане. UnmappedCharacterCount брои знаци без използваемо Unicode картографиране, което е класическият симптом на subset шрифт, вграден без CMap /ToUnicode; такъв текст се рендира перфектно и се извлича като нищо полезно. GeometryFailureCount брои знаци, чийто ограждащ правоъгълник не е могъл да бъде определен, което влошава подредбата при физическо оформление. Логвайте и двата. Набор от документи, при който тези числа последователно са близо до нула, може да бъде индексиран с доверие, а такъв, при който не са, ви казва, че някои производители в конвейера ви се нуждаят от внимание, преди какъвто и да е downstream резултат да бъде надежден

var
  Page: TPdfStructuredTextPage;
  B, S, L: Integer;
  Emphasised: Boolean;
begin
  Page := Pdf.GetStructuredText(Options);

  if Page.UnmappedCharacterCount > 0 then
    Log(Format('page %d: %d characters without a Unicode mapping',
      [Page.PageNumber, Page.UnmappedCharacterCount]));
  if Page.GeometryFailureCount > 0 then
    Log(Format('page %d: %d characters without geometry',
      [Page.PageNumber, Page.GeometryFailureCount]));

  for B := 0 to High(Page.Blocks) do
    for L := 0 to High(Page.Blocks[B].Lines) do
      for S := 0 to High(Page.Blocks[B].Lines[L].Spans) do
      begin
        Emphasised := Page.Blocks[B].Lines[L].Spans[S].FontWeight >= 600;
        AppendRun(Page.Blocks[B].Lines[L].Spans[S].Text, Emphasised,
          Page.Blocks[B].Lines[L].Spans[S].SourceStartIndex);
      end;
end;

Производителност на реални страници

Извличането с физическо оформление е скъпият режим, а имплементацията е изградена за страници, които наистина са големи: подредбата на знаци работи в O(n log n), а не чрез повторно сканиране, буферите за редове и span-ове растат геометрично, вместо да се преразпределят при всеки знак, Unicode текстът се изгражда в буфери, а не чрез конкатенация на низове, а справките за шрифт при съседни текстови обекти се кешират. Точно тази комбинация пази плътна страница от 5000 знака предвидима, вместо квадратична

За задача с много страници пак си струва да изберете по-евтиния режим, където можете. Използвайте roContentOrder с включена семантика за таг-ирани документи, на които се доверявате, и запазете roPhysicalLayout за сканирания и наследен материал, при които геометрията е единственият сигнал. Ако всичко, от което се нуждаете, е обикновен низ, по-простото API, описано в извличането на текст от PDF документи, остава по-бързият път, а когато трябва да проследите текст обратно до идентификатори на маркирано съдържание, четенето и записването на маркирано съдържание BDC и MCID покрива този слой

Блоковият модел се картографира чисто и върху онова, което retrieval конвейери искат: заглавие с параграфите си е откъс със заглавие, а границите позволяват на цитат да сочи към местоположение на страница, а не към документ. PDFiumPas е Delphi и Lazarus компонент около движока PDFium, документиран с примери на страницата на PDFium компонента за Delphi