O PDFium Component detecta tabelas em uma página PDF e as retorna como uma grade de células com spans de linha e coluna, linhas de cabeçalho e um valor de confiança, por meio de ExtractTables para uma página e ExtractDocumentTables para um documento inteiro. Cada tabela se converte para CSV ou JSON com uma única chamada, e tabelas que continuam através de uma quebra de página podem ser vinculadas em uma cadeia de continuação
O PDF não tem objeto de tabela. Uma tabela em um PDF é um conjunto de sequências de texto posicionadas de modo que um humano as leia como uma grade, às vezes com linhas desenhadas ao redor delas e frequentemente sem. Recuperar a grade significa reconstruir uma intenção que o arquivo nunca registrou, razão pela qual toda ferramenta de extração produz resultados ligeiramente diferentes e pela qual uma ferramenta que informa sua confiança é mais útil do que uma que não informa
Dois modos de detecção para dois tipos de tabela
A detecção por linhas usa as linhas desenhadas. Cada segmento de caminho traçado é transformado em coordenadas de página por meio da matriz do objeto de página, linhas horizontais e verticais são interseccionadas, e as interseções formam componentes conectados. Cada componente se torna sua própria grade ordenada de posições X e Y, o que é o que impede que duas tabelas separadas na mesma página sejam mescladas em uma única grade sem sentido
A detecção por espaço em branco trata tabelas desenhadas com alinhamento em vez de linhas. Caixas de palavras são agrupadas em linhas visuais, lacunas dentro de uma linha a dividem em colunas candidatas, e uma tabela só é aceita quando pelo menos MinRows linhas repetem pelo menos MinColumns âncoras alinhadas à esquerda dentro de AlignmentTolerance. O fator de lacuna de linha tem padrão 3, o que cobre o espaçamento de linha de base de aproximadamente 30 pontos típico de texto de 12 pontos, sem deixar que uma única linha contendo várias sequências de texto se disfarce de tabela
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; // baseado em 1
Options := TPdfTableExtractionOptions.Default;
Options.DetectRuledTables := True;
Options.DetectWhitespaceTables := True;
Options.MinConfidence := 0.6; // o padrão é 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;
Como as células mescladas são recuperadas?
Essa é a parte que extratores ingênuos fazem errado. Uma célula mesclada não pode ser identificada a partir da grade global sozinha, porque a grade é derivada de todas as linhas da página, e uma região mesclada simplesmente carece da linha interna que a teria separado
A regra usada aqui é local: duas células-base adjacentes são mescladas quando nenhuma linha de fronteira cobre o intervalo entre elas. Union-find as une, os componentes retangulares resultantes se tornam valores de RowSpan e ColumnSpan, e o texto é atribuído a uma célula-base pelo seu ponto central e depois segue essa célula até sua raiz de mesclagem. Fazer dessa forma também mantém o custo linear em palavras mais células, em vez do escaneamento quadrático que você obtém testando cada palavra contra cada célula
O efeito prático é que uma tabela financeira com um cabeçalho "Total" mesclado abrangendo três colunas sai como uma célula com span três, em vez de uma célula preenchida e duas misteriosamente vazias
Continuação entre páginas
Tabelas longas quebram entre páginas, e tratar o fragmento de cada página como uma tabela independente força o chamador a costurá-las. ExtractDocumentTables pode vinculá-las em vez disso, mas apenas sob condições estritas: o fragmento precisa ser a tabela mais baixa na página anterior, a próxima precisa ser a tabela mais alta na página seguinte, os números de página precisam ser adjacentes, e as fronteiras de coluna precisam coincidir
As quatro condições juntas são o que impede o erro óbvio, que é encadear toda tabela de quatro colunas em um documento em uma única mega-tabela imaginária só porque elas acontecem de compartilhar uma contagem de colunas. Quando as condições se sustentam, as tabelas compartilham um identificador de grupo de continuação e carregam metadados de continuação; quando não se sustentam, você obtém tabelas separadas e pode decidir por si mesmo
A extração em nível de documento compartilha os orçamentos MaxCells e MaxTables entre páginas, em vez de reiniciá-los por página, e restaura a página ativa em um bloco finally, de modo que uma execução de extração em um visualizador deixa o usuário olhando para a página em que estava
Exportando sem corromper os dados
Ambos os exportadores são deliberados quanto ao escape. O CSV sempre coloca campos entre aspas e duplica aspas internas, o que evita a falha clássica em que uma célula contendo uma vírgula silenciosamente se torna duas colunas. Para células mescladas, o conteúdo é emitido apenas na âncora superior esquerda, de modo que um round-trip em CSV não duplica um cabeçalho abrangente pelas colunas que ele cobre
O JSON preserva o Unicode em vez de convertê-lo para ASCII, escapa caracteres de controle, e inclui os metadados de que um consumidor precisa para avaliar a qualidade: modo de detecção, confiança, limites, valores de span, flags de cabeçalho e informações de continuação. Se você estiver alimentando tabelas extraídas em um sistema posterior, prefira JSON, porque uma linha CSV não consegue dizer a você que a tabela de onde veio pontuou 0,51 de confiança:
// Extração em todo o documento, mantendo apenas tabelas que valem confiança
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;
Ajuste fino, e saber quando parar
Três configurações importam mais do que o resto. MinConfidence é o portão de qualidade, e 0.5 é deliberadamente permissivo; aumente-o para ingestão automatizada e diminua-o para uma interface de revisão onde um humano confirma cada resultado. MinColumnGap decide o que conta como uma fronteira de coluna no modo de espaço em branco, e tabelas com espaçamento apertado em relatórios densos podem precisar que ele seja reduzido a partir do padrão de 12 pontos. MaxRowGapFactor decide quando a distância vertical encerra uma tabela, o que importa para tabelas com linhas em branco ocasionais
Seja honesto sobre os limites. Tabelas com linhas extraem de forma confiável. Tabelas de espaço em branco bem alinhadas extraem bem. Tabelas com texto rotacionado, tabelas aninhadas, ou células cujo conteúdo quebra em algo que parece outra linha vão precisar de revisão não importa como os parâmetros sejam definidos. Para essas, o modelo de texto estruturado dá a você a matéria-prima para construir um leitor específico do domínio, descrito em blocos de texto estruturado e ordem de leitura
Uma combinação útil: quando um documento digitalizado não tem texto nenhum, a detecção de tabelas não tem com o que trabalhar até que exista uma camada de texto. Adicione uma primeiro, como descrito em adicionando uma camada de texto pesquisável a PDFs digitalizados, depois extraia. As caixas de palavras que um provedor de OCR retorna são exatamente a entrada de que a detecção por espaço em branco precisa
A extração de tabelas, o texto estruturado e o refluxo, todos leem do mesmo modelo de página no Delphi, no C++Builder e no Lazarus; a API completa está descrita na página do PDFium Component para Delphi