Bài viết kỹ thuật

PDFium VCL: Trích xuất văn bản PDF có cấu trúc trong Delphi

PDFiumPas trả về văn bản trang dưới dạng một cấu trúc thay vì một chuỗi. GetStructuredText tạo ra một TPdfStructuredTextPage chứa các block, mỗi block chứa các dòng, mỗi dòng chứa các span có định dạng, kèm phạm vi tọa độ trang ở mọi cấp và các chỉ số ký tự nguồn được giữ nguyên, nên bất kỳ đoạn nào cũng có thể được ánh xạ ngược lại trang văn bản gốc

Cách trích xuất chuỗi phẳng mà hầu hết mã nguồn bắt đầu với vẫn tồn tại và vẫn đúng cho mục đích của nó. Nó chỉ ngừng đủ dùng ngay khi bạn cần biết những từ nào là một tiêu đề, những từ nào thuộc cột bên trái, hoặc một kết quả khớp thực sự nằm ở đâu trên trang

Vì sao một chuỗi phẳng là đầu ra sai cho hầu hết công việc?

Vì các câu hỏi người ta đặt ra cho văn bản đã trích xuất gần như không bao giờ là "trang này có những ký tự gì". Chúng là "tiêu đề là gì", "đây có phải một bảng không", "đoạn văn này có thuộc phần 4 không", "tôi vẽ vùng tô sáng ở đâu". Một chuỗi duy nhất không trả lời được câu nào trong số đó, và mỗi câu trả lời bạn tái dựng từ nó là một suy đoán bạn nay phải tự chịu trách nhiệm

Bố cục hai cột minh họa rõ điều này. Trích xuất một bài viết hai cột thành chuỗi và, tùy vào cách trình tạo ghi luồng nội dung, bạn có thể nhận được cột một rồi tới cột hai, hoặc bạn có thể nhận được dòng một của cột một, dòng một của cột hai, dòng hai của cột một, và cứ thế xuống trang. Cả hai đều xuất ra từ một PDF hợp chuẩn. Không cái nào sai ở tầng định dạng, vì PDF mô tả các dấu vết trên một trang, không phải một đề cương tài liệu. Một mô hình dựa trên block cho phép bộ trích xuất đưa ra quyết định thứ tự một cách tường minh và cho bạn biết nó đã đưa ra quyết định nào

Thứ tự nội dung hay bố cục vật lý?

TPdfStructuredTextOptions.ReadingOrder chọn giữa roContentOrderroPhysicalLayout, và câu trả lời đúng phụ thuộc vào việc bạn tin tưởng cái nào hơn, trình tạo hay hình học

Thứ tự nội dung trả về văn bản theo trình tự mà luồng nội dung vẽ ra. Điều đó nhanh, và với các tài liệu do một trình tạo cư xử đúng mực sinh ra, thường chính là thứ tự đọc dự kiến. Bố cục vật lý bỏ qua trình tự luồng và tái dựng thứ tự từ vị trí thực tế của các ký tự, gom cụm thành dòng rồi thành cột. Đó là điều bạn muốn cho các trang đã quét rồi OCR, cho đầu ra từ các công cụ phát ra văn bản theo thứ tự phông chữ thay vì thứ tự đọc, và cho bất cứ thứ gì mà kết quả thị giác là tín hiệu duy nhất bạn có thể tin cậy

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;                     // đánh số từ 1

    Options := TPdfStructuredTextOptions.Default;
    Options.ReadingOrder := roPhysicalLayout;
    Options.IncludeFontInfo := True;
    Options.IncludeSemantics := True;
    Options.MaxCharacters := 200000;         // ngân sách 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;

Việc gắn thẻ bổ sung điều gì mà hình học không có?

Chủ đích. Khi bật IncludeSemantics, các block từ một PDF có gắn thẻ mang theo một Kind lấy từ cây cấu trúc, nên một tiêu đề là tiêu đề vì trình tạo đã nói vậy, không phải vì phông chữ của nó lớn hơn mức trung bình. Các loại này bao phủ những hình dạng quan trọng cho việc tái sử dụng: cfParagraph, cfHeading kèm một HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure và dạng dự phòng chưa gắn thẻ cfPlain

Trường Source ghi lại nguồn gốc của mỗi phân loại, rosStructure cho cây cấu trúc và rosHeuristic cho suy luận, đây là trường nên ghi log khi bạn quyết định tin tưởng đến mức nào vào một pipeline trích xuất trên cả một tập tài liệu. Hình ảnh là một trường hợp đặc biệt đáng biết: đối với một block cfFigure, văn bản đến từ mô tả thay thế thay vì từ bất kỳ glyph nào, vì một hình ảnh không có ký tự riêng của nó. Văn bản thay thế không khớp vẫn được biểu diễn thay vì bị loại bỏ, đây là điều cho phép một cuộc kiểm tra khả năng truy cập thấy rằng một mô tả tồn tại ngay cả khi không gì trên trang vẽ ra nó. Bản thân mô hình gắn thẻ được trình bày trong kiểm tra hợp lệ cây cấu trúc PDF/UA

Span mang theo định dạng và nguồn gốc

Mỗi TPdfStructuredTextSpan giữ văn bản của nó, phạm vi tọa độ trang, FontName, FontSize, FontWeightAngle, cùng SourceStartIndexSourceCharacterCount. Span ngắt tại chỗ định dạng thay đổi, nên một câu có ba từ in đậm trở thành ba span, và việc tái dựng phần nhấn mạnh trong HTML hay Markdown chỉ là việc đọc các thuộc tính thay vì đoán từ tên phông

Hai trường chỉ số nguồn là những trường biến việc trích xuất thành một tính năng thay vì một báo cáo. Chúng trỏ ngược lại chuỗi ký tự của trang, nghĩa là một block bạn tìm thấy trong một lượt tìm kiếm có thể được chuyển thành hình học chọn cấp ký tự hoặc một hình chữ nhật tô sáng mà không cần một lượt duyệt thứ hai với thứ tự khác trên văn bản; cơ chế này được mô tả trong chọn dòng văn bản trực quan bằng hộp ký tự. Trường Angle quan trọng hơn vẻ ngoài của nó: văn bản xoay trong một hình đóng dấu hoặc một watermark rơi vào cùng không gian tọa độ với văn bản thân bài, và một pipeline bỏ qua góc quay sẽ vô tư ghép một chữ "DRAFT" chéo vào giữa một đoạn văn

Ngân sách, và hai bộ đếm chất lượng

MaxCharacters là một ngân sách fail-closed, không phải một thiết lập cắt bớt: một trang vượt quá nó sẽ dừng lại thay vì âm thầm trả về một phần nội dung. Trên một đường tiếp nhận không đáng tin, đó chính là hành vi bạn muốn, vì một trang có một triệu ký tự hoặc là một con quái vật do máy tạo ra, hoặc là một nỗ lực biến bộ trích xuất của bạn thành phần chậm nhất của hệ thống

Hai bộ đếm trên trang trả về mô tả trực tiếp chất lượng trích xuất. UnmappedCharacterCount đếm các ký tự không có ánh xạ Unicode khả dụng, đây là triệu chứng kinh điển của một font tập con được nhúng mà không có CMap /ToUnicode; văn bản như vậy hiển thị hoàn hảo nhưng trích xuất ra không có gì hữu ích. GeometryFailureCount đếm các ký tự không xác định được hộp bao, điều này làm suy giảm việc sắp xếp theo bố cục vật lý. Hãy ghi log cả hai. Một tập tài liệu mà những con số đó luôn gần bằng không có thể được đánh chỉ mục với sự tin tưởng, và một tập mà chúng không như vậy đang cho bạn biết rằng một số trình tạo trong pipeline của bạn cần được lưu ý trước khi bất kỳ kết quả hạ nguồn nào đáng tin cậy

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;

Hiệu năng trên các trang thực tế

Trích xuất bố cục vật lý là chế độ tốn kém, và cách triển khai được xây dựng cho các trang thực sự lớn: việc sắp xếp ký tự chạy ở độ phức tạp O(n log n) thay vì quét lặp lại, các bộ đệm dòng và span tăng theo cấp số nhân thay vì cấp phát lại theo từng ký tự, văn bản Unicode được dựng trong các bộ đệm thay vì bằng phép nối chuỗi, và các lượt tra cứu phông cho các đối tượng văn bản liền kề được cache. Sự kết hợp đó chính là điều giữ cho một trang dày đặc 5.000 ký tự có thể dự đoán được thay vì chạy theo cấp bậc hai

Với một tác vụ nặng về số lượng trang, vẫn đáng để chọn chế độ rẻ hơn khi có thể. Hãy dùng roContentOrder kèm ngữ nghĩa bật lên cho các tài liệu có gắn thẻ mà bạn tin tưởng, và dành roPhysicalLayout cho tài liệu quét và tài liệu cũ nơi hình học là tín hiệu duy nhất. Nếu tất cả những gì bạn cần là một chuỗi thuần túy, API đơn giản hơn được mô tả trong trích xuất văn bản từ tài liệu PDF vẫn là đường nhanh hơn, và khi bạn cần lần theo văn bản trở lại các định danh nội dung đánh dấu, đọc và ghi nội dung đánh dấu BDC và MCID bao phủ lớp đó

Mô hình block cũng ánh xạ gọn gàng vào những gì các pipeline truy xuất cần: một tiêu đề cùng các đoạn văn của nó là một chunk có tiêu đề, và phạm vi tọa độ cho phép một trích dẫn trỏ tới một vị trí trên một trang thay vì trỏ tới cả một tài liệu. PDFiumPas là một thành phần Delphi và Lazarus xây quanh engine PDFium, có tài liệu kèm ví dụ tại trang thành phần PDFium cho Delphi