Artículo técnico

Extraer tablas de páginas PDF en Delphi con PDFium

PDFium Component detecta tablas en una página PDF y las devuelve como una cuadrícula de celdas con spans de fila y columna, filas de encabezado y un valor de confianza, mediante ExtractTables para una página y ExtractDocumentTables para un documento completo. Cada tabla se convierte a CSV o JSON con una sola llamada, y las tablas que continúan después de un salto de página se pueden enlazar en una cadena de continuación

PDF no tiene un objeto tabla. Una tabla en un PDF es un conjunto de bloques de texto posicionados de modo que un humano los lea como una cuadrícula, a veces con líneas dibujadas alrededor y a menudo sin ellas. Recuperar la cuadrícula significa reconstruir una intención que el archivo nunca registró, razón por la cual cada herramienta de extracción produce resultados ligeramente distintos, y por la cual una herramienta que le dice su nivel de confianza es más útil que una que no lo hace

Dos modos de detección para dos tipos de tabla

La detección por líneas usa las líneas dibujadas. Cada segmento de trazo se transforma a coordenadas de página mediante la matriz del objeto de página, las líneas horizontales y verticales se intersecan, y las intersecciones forman componentes conectados. Cada componente se convierte en su propia cuadrícula ordenada de posiciones X e Y, que es lo que evita que dos tablas separadas en la misma página se fusionen en una cuadrícula sin sentido

La detección por espacios en blanco maneja tablas dibujadas mediante alineación en lugar de líneas. Los cuadros de palabras se agrupan en filas visuales, los espacios dentro de una fila la dividen en columnas candidatas, y una tabla se acepta solo cuando al menos MinRows filas repiten al menos MinColumns anclas alineadas a la izquierda dentro de AlignmentTolerance. El factor de espacio entre filas es 3 por defecto, lo cual cubre el espaciado de línea base de aproximadamente 30 puntos típico de texto de 12 puntos, sin dejar que una sola línea con varios bloques de texto se haga pasar por una tabla

Diagrama del pipeline de detección de tablas de PDFium Component en Delphi, donde las intersecciones de líneas dibujadas y las filas de palabras alineadas por espacios en blanco alimentan un único registro de tabla puntuado, con exportación a CSV y JSON
La detección por líneas interseca los trazos dibujados, mientras que la detección por espacios en blanco cuenta filas de cuadros de palabra alineados; los candidatos que superan MinRows y MinColumns emergen con un puntaje de confianza y el DetectionMode adjunto
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;                    // basado en 1

    Options := TPdfTableExtractionOptions.Default;
    Options.DetectRuledTables := True;
    Options.DetectWhitespaceTables := True;
    Options.MinConfidence := 0.6;            // el valor por defecto es 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;

El proyecto completo viene incluido con el componente como Demo/Delphi/TableExtractionLab/TableExtractionLab.dpr, con sus equivalentes para C++Builder y Lazarus bajo Demo/CBuilder/TableExtractionLab y Demo/Lazarus/TableExtractionLab. El laboratorio arma un documento de ejemplo que contiene una tabla con líneas y celdas de encabezado combinadas, una tabla alineada por espacios en blanco sin líneas de ningún tipo, y una tabla larga que se corta en un salto de página; luego extrae cada una y muestra las celdas, los spans, la confianza, los grupos de continuación y la salida CSV / JSON, lo que lo convierte en la forma más rápida de ver cómo interactúan estas opciones. La detección de tablas llegó en la versión 3.36.0, así que si el compilador marca TPdfTableExtractionOptions o TPdfTables como identificador desconocido, la copia de la librería en su proyecto es anterior a ese release y hay que actualizarla antes de que este código compile

¿Cómo se recuperan las celdas combinadas?

Esta es la parte que los extractores ingenuos hacen mal. Una celda combinada no se puede identificar solo a partir de la cuadrícula global, porque la cuadrícula se deriva de todas las líneas de la página, y una región combinada simplemente carece de la línea interior que la habría separado

La regla que se usa aquí es local: dos celdas base adyacentes se combinan cuando ninguna línea de límite cubre el intervalo entre ellas. Union-find las une, los componentes rectangulares resultantes se convierten en los valores RowSpan y ColumnSpan, y el texto se asigna a una celda base por su punto central y luego sigue a esa celda hasta su raíz de combinación. Hacerlo así también mantiene el costo lineal en palabras más celdas, en lugar del escaneo cuadrático que se obtiene al probar cada palabra contra cada celda

El efecto práctico es que una tabla financiera con un encabezado "Total" combinado que abarca tres columnas sale como una celda con span tres, en lugar de una celda con contenido y dos celdas vacías misteriosas

Diagrama de la recuperación de celdas combinadas en la extracción de tablas de PDFium para Delphi, donde union-find une celdas base adyacentes siempre que ninguna línea de límite cubra el intervalo entre ellas, generando RowSpan y ColumnSpan
Union-find combina celdas base vecinas cuyo intervalo compartido no lleva ningún límite dibujado, de modo que un encabezado combinado se devuelve como una sola celda con ColumnSpan definido, en lugar de una celda con contenido flanqueada por celdas vacías

Continuación entre páginas

Las tablas largas se cortan entre páginas, y tratar el fragmento de cada página como una tabla independiente obliga al llamador a coserlas. ExtractDocumentTables puede enlazarlas en su lugar, pero solo bajo condiciones estrictas: el fragmento debe ser la tabla más baja en la página anterior, la siguiente debe ser la tabla más alta en la página siguiente, los números de página deben ser adyacentes, y los límites de columna deben coincidir

Las cuatro condiciones juntas son lo que evita el error obvio, que es encadenar toda tabla de cuatro columnas en un documento en una imaginaria mega-tabla solo porque comparten la cantidad de columnas. Cuando las condiciones se cumplen, las tablas comparten un identificador de grupo de continuación y llevan metadatos de continuación; cuando no se cumplen, usted obtiene tablas separadas y puede decidir por su cuenta

Diagrama de la continuación de tablas entre páginas PDF en Delphi, donde cuatro condiciones estrictas deciden si el fragmento más bajo de una página se une con el fragmento más alto de la siguiente
La extracción a nivel de documento enlaza fragmentos solo cuando se cumplen las cuatro condiciones, evitando que tablas de cuatro columnas sin relación se fusionen en una imaginaria mega-tabla

La extracción a nivel de documento comparte los presupuestos MaxCells y MaxTables entre páginas en lugar de reiniciarlos por página, y restaura la página activa dentro de un bloque finally, de modo que una extracción ejecutada dentro de un visor deja al usuario mirando la página en la que estaba

Exportar sin corromper los datos

Ambos exportadores son deliberados respecto al escapado. CSV siempre entrecomilla los campos y duplica las comillas internas, lo cual evita el fallo clásico en el que una celda que contiene una coma se convierte en silencio en dos columnas. Para las celdas combinadas, el contenido se emite solo en el ancla superior izquierda, de modo que un ciclo de ida y vuelta por CSV no duplica un encabezado que abarca varias columnas a lo largo de las columnas que cubre

JSON preserva Unicode en lugar de escaparlo a ASCII, escapa los caracteres de control, e incluye los metadatos que un consumidor necesita para juzgar la calidad: modo de detección, confianza, límites, valores de span, banderas de encabezado e información de continuación. Si usted está alimentando tablas extraídas a un sistema posterior, prefiera JSON, porque una fila de CSV no le puede decir que la tabla de la que proviene obtuvo una confianza de 0.51:

// Extracción de todo el documento, conservando solo las tablas confiables
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;

Ajustar, y saber cuándo detenerse

Tres ajustes importan más que el resto. MinConfidence es la barrera de calidad, y 0.5 es deliberadamente permisivo; súbalo para una ingesta automatizada y bájelo para una interfaz de revisión donde un humano confirma cada resultado. MinColumnGap decide qué cuenta como límite de columna en el modo de espacios en blanco, y las tablas con ajuste apretado en informes densos pueden necesitar reducirlo desde el valor por defecto de 12 puntos. MaxRowGapFactor decide cuándo la distancia vertical termina una tabla, algo que importa para tablas con filas en blanco ocasionales

Sea honesto sobre los límites. Las tablas con líneas se extraen de forma confiable. Las tablas de espacios en blanco alineadas con limpieza se extraen bien. Las tablas con texto rotado, tablas anidadas, o celdas cuyo contenido se envuelve hasta parecer otra fila, van a necesitar revisión sin importar cómo se configuren los parámetros. Para esos casos, el modelo de texto estructurado le da la materia prima para construir un lector específico de su dominio, descrito en bloques de texto estructurado y orden de lectura

Una combinación útil: cuando un documento escaneado no tiene texto en absoluto, la detección de tablas no tiene con qué trabajar hasta que exista una capa de texto. Agregue una primero, como se describe en agregar una capa de texto con capacidad de búsqueda a PDF escaneados, y luego extraiga. Los cuadros de palabra que devuelve un proveedor de OCR son exactamente la entrada que necesita la detección por espacios en blanco

La extracción de tablas, el texto estructurado y el reflow leen todos del mismo modelo de página en Delphi, C++Builder y Lazarus; la API completa se describe en la página de PDFium Component para Delphi