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 cabecera y un valor de confianza, mediante ExtractTables para una página y ExtractDocumentTables para un documento entero. Cada tabla se convierte a CSV o JSON con una sola llamada, y las tablas que continúan a través de un salto de página pueden enlazarse en una cadena de continuación
PDF no tiene un objeto tabla. Una tabla en un PDF es un conjunto de trazos 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 te dice su confianza es más útil que una que no lo hace
Dos modos de detección para dos tipos de tabla
La detección con 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, lo cual es lo que evita que dos tablas separadas en la misma página se combinen en una única cuadrícula sin sentido
La detección por espacios en blanco maneja tablas dibujadas mediante alineación en lugar de líneas. Las cajas de palabras se agrupan en filas visuales, los huecos 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 hueco entre filas es 3 por defecto, lo cual cubre el espaciado de línea base de aproximadamente 30 puntos típico de un texto de 12 puntos, sin dejar que una sola línea con varios trazos de texto se haga pasar por una tabla
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;
¿Cómo se recuperan las celdas combinadas?
Esta es la parte que los extractores ingenuos hacen mal. Una celda combinada no puede identificarse a partir de la cuadrícula global por sí sola, 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 usada aquí es local: dos celdas base adyacentes están combinadas cuando ninguna línea de límite cubre el intervalo entre ellas. Union-find las une, los componentes rectangulares resultantes se convierten en 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 coste lineal en palabras más celdas, en lugar del escaneo cuadrático que resultaría de comprobar cada palabra contra cada celda
El efecto práctico es que una tabla financiera con una cabecera «Total» combinada que abarca tres columnas sale con una celda de span tres, en lugar de una celda con contenido y dos misteriosamente 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 de la página anterior, la siguiente debe ser la tabla más alta de 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 sería encadenar cada tabla de cuatro columnas de un documento en una única mega-tabla imaginaria solo porque comparten un número 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, obtienes tablas separadas y puedes decidir por ti mismo
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 en un visor deja al usuario mirando la página en la que estaba
Exportar sin corromper los datos
Ambos exportadores son deliberados con el escapado. CSV siempre entrecomilla los campos y duplica las comillas internas, lo cual evita el fallo clásico donde una celda que contiene una coma se convierte en silencio en dos columnas. Para las celdas combinadas, el contenido se emite únicamente en el ancla superior izquierda, de modo que un ciclo de ida y vuelta por CSV no duplica una cabecera de span a lo largo de las columnas que cubre
JSON conserva 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, marcadores de cabecera e información de continuación. Si estás alimentando tablas extraídas a un sistema posterior, prefiere JSON, porque una fila de CSV no puede decirte que la tabla de la que provino obtuvo una confianza de 0,51:
// Extracción a nivel de documento, conservando solo las tablas dignas de confianza
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 parar
Tres ajustes importan más que el resto. MinConfidence es la puerta de calidad, y 0,5 es deliberadamente permisivo; súbelo para la ingesta automatizada y bájalo para una interfaz de revisión donde un humano confirma cada resultado. MinColumnGap decide qué cuenta como límite de columna en modo de espacios en blanco, y las tablas ajustadas 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
Sé honesto sobre los límites. Las tablas con líneas se extraen de forma fiable. Las tablas alineadas por espacios en blanco de forma limpia se extraen bien. Las tablas con texto rotado, tablas anidadas, o celdas cuyo contenido se ajusta en lo que parece otra fila necesitarán revisión sea cual sea la configuración de los parámetros. Para esos casos, el modelo de texto estructurado te da la materia prima para construir un lector específico de dominio, descrito en bloques de texto estructurado y orden de lectura
Un emparejamiento ú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. Añade una primero, como se describe en añadir una capa de texto con capacidad de búsqueda a PDF escaneados, y luego extrae. Las cajas de palabras 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 reflujo 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