Műszaki cikk

Strukturált PDF-szövegkinyerés Delphiben PDFium VCL-lel

A PDFiumPas az oldal szövegét struktúraként adja vissza, nem stringként. A GetStructuredText egy TPdfStructuredTextPage-et állít elő, amely blokkokat tartalmaz, mindegyik sorokat tartalmaz, mindegyik stílusos spaneket tartalmaz, oldaltér-határokkal minden szinten, és megőrzött forráskarakter-indexekkel, így bármely töredék visszaképezhető az alatta lévő szövegoldalra

A lapos-string kinyerés, amellyel a legtöbb kód kezdi, még mindig ott van, és még mindig helyes a saját céljára. Abban a pillanatban nem elég többé, amikor tudnod kell, mely szavak voltak egy címsor, melyek tartoztak a bal oszlophoz, vagy hol ül ténylegesen egy találat az oldalon

Miért rossz kimenet egy lapos string a legtöbb feladathoz?

Mert azok a kérdések, amelyeket az emberek a kinyert szöveghez intéznek, szinte soha nem az, hogy „milyen karakterek vannak ezen az oldalon”. Hanem, hogy „mi a cím”, „ez egy táblázat”, „ez a bekezdés a 4. szakaszhoz tartozik-e”, „hova rajzoljam a kiemelést”. Egyetlen string egyiket sem válaszolja meg, és minden válasz, amelyet belőle rekonstruálsz, egy heurisztika, amelyet mostantól te birtokolsz

A kéthasábos elrendezések konkréttá teszik ezt. Nyerj ki egy kéthasábos cikket stringként, és attól függően, hogyan írta az előállító a tartalmi folyamot, kaphatod az első hasábot a második hasáb után, vagy kaphatod az első hasáb első sorát, a második hasáb első sorát, az első hasáb második sorát, és így tovább lefelé az oldalon. Mindkettő egy megfelelő PDF-ből jöhet ki. Egyik sem hibás formátumszinten, mert a PDF jeleket ír le egy oldalon, nem egy dokumentumvázlatot. Egy blokkalapú modell lehetővé teszi, hogy a kinyerő explicit módon hozza meg a sorrenddöntést, és megmondja, melyik döntést hozta

Tartalmi sorrend vagy fizikai elrendezés?

A TPdfStructuredTextOptions.ReadingOrder a roContentOrder és a roPhysicalLayout között választ, és a helyes válasz attól függ, miben bízol jobban: az előállítóban vagy a geometriában

A tartalmi sorrend abban a sorrendben adja vissza a szöveget, ahogy a tartalmi folyam kirajzolja. Ez gyors, és egy jól viselkedő előállító által generált dokumentumoknál jellemzően a szándékolt olvasási sorrend. A fizikai elrendezés figyelmen kívül hagyja a folyamsorrendet, és a karakterek tényleges helyéből építi újra a sorrendet, sorokba, majd oszlopokba klaszterezve. Ez az, amit szkennelt-majd-OCR-ezett oldalaknál akarsz, olyan eszközök kimeneténél, amelyek betűtípus-sorrendben, nem olvasási sorrendben adják ki a szöveget, és bármi másnál, ahol a vizuális eredmény az egyetlen dolog, amire támaszkodhatsz

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-alapú

    Options := TPdfStructuredTextOptions.Default;
    Options.ReadingOrder := roPhysicalLayout;
    Options.IncludeFontInfo := True;
    Options.IncludeSemantics := True;
    Options.MaxCharacters := 200000;         // fail-closed keret

    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;

Mit ad hozzá a tagelés, amit a geometria nem tud?

Szándékot. Az IncludeSemantics engedélyezésével egy tagelt PDF blokkjai a struktúrafából származó Kind értéket hordoznak, így egy címsor azért címsor, mert az előállító azt mondta, nem azért, mert a betűtípusa nagyobb volt az átlagosnál. A fajták lefedik az újrafelhasználás szempontjából számító alakokat: cfParagraph, cfHeading egy HeadingLevel-lel, cfListItem, cfTableCell, cfCaption, cfFigure, és a tagelés nélküli tartalék cfPlain

A Source mező rögzíti, honnan származott az egyes osztályozás, rosStructure a struktúrafánál és rosHeuristic a következtetésnél, ez az a mező, amelyet naplózni érdemes, amikor eldöntöd, mennyire bízol meg egy kinyerési pipeline-ban egy dokumentumkészleten át. Az ábrák egy speciális eset, amelyet érdemes ismerni: egy cfFigure blokknál a szöveg az alternatív leírásból származik, nem bármilyen glyph-ből, mivel egy ábrának nincsenek saját karakterei. A nem egyeztetett alternatív szöveg továbbra is reprezentálva van ahelyett, hogy elvetnék, ami lehetővé teszi egy akadálymentességi audit számára, hogy lássa, létezik egy leírás akkor is, ha semmi az oldalon nem rajzolja ki. Magát a tagelési modellt a PDF/UA struktúrafa validálás című cikk tárgyalja

A spanek hordozzák a stílust és az eredetet

Minden TPdfStructuredTextSpan hordozza a szövegét, az oldaltér-határait, a FontName, a FontSize, a FontWeight és az Angle értékeket, plusz a SourceStartIndex-et és a SourceCharacterCount-ot. A spanek ott törnek, ahol a stílus megváltozik, így egy három félkövér szót tartalmazó mondat három spanné válik, és a kiemelés HTML-ben vagy Markdownban való újraépítése tulajdonságok kiolvasásának kérdése, nem betűtípusnevekből való találgatásé

A két forrásindex-mező az, amely egy jelentésből egy funkcióvá alakítja a kinyerést. Visszamutatnak az oldal karaktersorozatába, ami azt jelenti, hogy egy, a keresésben egyezett blokk karakterszintű kijelölési geometriává vagy egy kiemelő téglalappá alakítható át anélkül, hogy egy második, másképp rendezett átfutásra lenne szükség a szövegen; a mechanizmust a vizuális szövegsor-kijelölés karakterdobozokkal című cikk írja le. Az Angle mező jobban számít, mint amilyennek látszik: egy bélyegzőben vagy egy vízjelben elforgatott szöveg ugyanabba a koordinátatérbe kerül, mint a törzsszöveg, és egy pipeline, amely figyelmen kívül hagyja a szöget, szívesen összeolvaszt egy átlós „DRAFT” feliratot egy bekezdés közepébe

Keret, és a két minőségszámláló

A MaxCharacters egy fail-closed keret, nem egy csonkolási beállítás: egy oldal, amely túllépi, leáll, ahelyett hogy csendben a tartalom egy részét adná vissza. Egy nem megbízható befogadási útvonalon ez az a viselkedés, amit akarsz, mert egy egymillió karakteres oldal vagy egy géppel generált szörnyeteg, vagy egy kísérlet arra, hogy a kinyerődet a rendszer leglassabb részévé tegye

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;

A visszaadott oldalon két számláló írja le közvetlenül a kinyerés minőségét. Az UnmappedCharacterCount azokat a karaktereket számolja, amelyeknek nincs használható Unicode-leképezésük, ami egy /ToUnicode CMap nélkül beágyazott subset betűtípus klasszikus tünete; az ilyen szöveg tökéletesen renderelődik, és semmi hasznosat nem nyer ki. A GeometryFailureCount azokat a karaktereket számolja, amelyeknek a határolódoboza nem volt meghatározható, ami rontja a fizikai-elrendezés sorrendjét. Naplózd mindkettőt. Egy dokumentumkészlet, ahol ezek a számok következetesen nulla közelében vannak, magabiztosan indexelhető, és egy, ahol nem, azt mondja, hogy néhány előállító a pipeline-odban figyelmet igényel, mielőtt bármely lentebbi eredmény megbízható lenne

Teljesítmény valódi oldalakon

A fizikai-elrendezés kinyerés a drága mód, és az implementáció valóban nagy oldalakhoz épült: a karaktersorrend O(n log n)-ben fut, nem ismételt pásztázással, a sor- és span-pufferek geometrikusan nőnek ahelyett, hogy karakterenként újraallokálódnának, a Unicode szöveg pufferekben épül fel, nem stringkonkatenációval, és a szomszédos szövegobjektumok betűtípus-keresései gyorsítótárazva vannak. Ez a kombináció tartja kiszámíthatóvá egy sűrű, 5000 karakteres oldalt, nem kvadratikussá

Egy oldalszám-nehéz feladatnál még mindig érdemes az olcsóbb módot választani, ahol csak lehet. Használd a roContentOrder módot szemantikával engedélyezve a megbízható, tagelt dokumentumoknál, és tartsd fenn a roPhysicalLayout módot a szkennelt és örökölt anyagoknál, ahol a geometria az egyetlen jel. Ha mindössze egy egyszerű stringre van szükséged, az szöveg kinyerése PDF-dokumentumokból című cikkben leírt egyszerűbb API marad a gyorsabb út, és amikor a szöveget vissza kell vezetned marked-content azonosítókra, a BDC és MCID marked content olvasása és írása című cikk tárgyalja azt a réteget

A blokkmodell tisztán illeszkedik ahhoz is, amit a visszakeresési pipeline-ok akarnak: egy címsor a bekezdéseivel egy címmel rendelkező darab, és a határok lehetővé teszik, hogy egy idézet egy oldalon lévő helyre mutasson, ne egy dokumentumra. A PDFiumPas egy Delphi és Lazarus komponens a PDFium motor köré építve, példákkal dokumentálva a PDFium Delphi komponens oldalán