Skip to content

Extrair tabelas de PDF em Python

Extrair tabelas de documentos PDF é uma das tarefas mais comuns em pipelines de processamento de documentos. Seja para puxar dados financeiros de relatórios anuais, fazer scraping de catálogos de produtos ou alimentar dados estruturados em um LLM, a extração confiável de tabelas é essencial. Este guia cobre tudo o que você precisa saber sobre extração de tabelas de PDFs em Python, desde linhas únicas de código até fluxos de trabalho de nível de produção para tabelas que ocupam múltiplas páginas.

Motor de detecção

O PDF Oxide usa o pipeline universal de detecção de tabelas arestas → alinhamento/mesclagem → interseções → células → grupos — a mesma abordagem do Tabula, pdfplumber e PyMuPDF, implementada em Rust puro.

Capacidades de detecção:

  • Baseada em interseções — encontra cruzamentos de linhas H×V, constrói células a partir de retângulos de quatro cantos, agrupa em tabelas via union-find.
  • Grade estendida — quando linhas horizontais e verticais estão em regiões diferentes da página, uma grade virtual é construída com o produto cartesiano de todas as coordenadas.
  • Detecção de texto com reconhecimento de colunas — segmenta layouts de 2 colunas via histograma de projeção X, depois executa detecção de tabelas somente por texto em cada coluna.
  • Tabelas de texto delimitadas por linhas horizontais — detecta tabelas delimitadas por linhas horizontais sem linhas verticais (comum em artigos acadêmicos).
  • Detecção híbrida de linhas — infere limites de linhas a partir das posições Y do texto quando apenas bordas verticais existem (itens de linha em notas fiscais).
  • Reconstituição de linhas pontilhadas/tracejadas — mescla segmentos de linha curtos em arestas contínuas.
  • Separação por divisores de seção — divide formulários de múltiplas seções em divisores horizontais de largura total.
  • Filtragem de cobertura de arestas — remove arestas órfãs que não participam de nenhuma grade potencial.

Configuração

TableDetectionConfig expõe parâmetros ajustáveis:

Campo Padrão Descrição
horizontal_strategy "lines_strict" "lines_strict", "lines", "text" ou "explicit"
vertical_strategy "lines_strict" Mesmo vocabulário
v_split_gap 20.0 pt Espaço entre linhas verticais que aciona a divisão em tabelas separadas (era fixo em 4pt antes da v0.3.20)
snap_tolerance 3.0 pt Tolerância de mesclagem de alinhamento de arestas
text_tolerance 3.0 pt Tolerância de mesclagem de linhas de texto

Mudança de comportamento

A partir da v0.3.20, a estratégia padrão para o Python extract_tables() é Both (detecta via linhas e texto). Páginas que dependiam do padrão antigo de somente texto devem passar horizontal_strategy="text" e vertical_strategy="text" explicitamente.

O binding Python agora lê corretamente vertical_strategy do dicionário table_settings — anteriormente era ignorado silenciosamente.

Renderização

As tabelas extraídas são emitidas com alinhamento de coluna preenchido com espaços (substituindo os caracteres de desenho de caixa ASCII das versões anteriores). Colunas de moeda e número são alinhadas à direita automaticamente. Prefixos de número de formulário ("1 Apr 11""Apr 11") e células decorativas de traço/sublinhado ("------") são removidos durante a renderização.

Extraia dados de tabela de um PDF usando conversão para Markdown:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)
# Output includes tables in GFM format:
# | Item | Qty | Price |
# |------|-----|-------|
# | Widget | 10 | $9.99 |

WASM

import { WasmPdfDocument } from "pdf-oxide-wasm";

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);
console.log(md);
// Output includes tables in GFM format:
// | Item | Qty | Price |
// |------|-----|-------|
// | Widget | 10 | $9.99 |
doc.free();

Rust

use pdf_oxide::PdfDocument;

let mut doc = PdfDocument::open("invoice.pdf")?;
let md = doc.to_markdown(0, true)?;
println!("{}", md);

Go

package main

import (
    "fmt"
    "log"
    pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)

func main() {
    doc, err := pdfoxide.Open("invoice.pdf")
    if err != nil { log.Fatal(err) }
    defer doc.Close()

    md, err := doc.ToMarkdown(0)
    if err != nil { log.Fatal(err) }
    fmt.Println(md)
}

C#

using PdfOxide;

using var doc = PdfDocument.Open("invoice.pdf");
Console.WriteLine(doc.ToMarkdown(0));

Java

import fyi.oxide.pdf.PdfDocument;

try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("invoice.pdf"))) {
    System.out.println(doc.toMarkdown(0));
}

PHP

use PdfOxide\PdfDocument;

$doc = PdfDocument::open('invoice.pdf');
echo $doc->toMarkdown(0);
$doc->close();

Ruby

require 'pdf_oxide'

PdfOxide::PdfDocument.open('invoice.pdf') do |doc|
  puts doc.to_markdown(0)
end

C++

#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>

auto doc = pdf_oxide::Document::open("invoice.pdf");
std::cout << doc.to_markdown(0) << '\n';

Swift

import PdfOxide

let doc = try Document.open("invoice.pdf")
print(try doc.toMarkdown(0))

Kotlin

import fyi.oxide.pdf.PdfDocument

PdfDocument.open(java.nio.file.Path.of("invoice.pdf")).use { doc ->
    println(doc.toMarkdown(0))
}

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('invoice.pdf');
print(doc.toMarkdown(0));
doc.close();

R

library(pdfoxide)

doc <- pdf_open("invoice.pdf")
cat(pdf_to_markdown(doc, 0))

Julia

using PdfOxide

doc = open_document("invoice.pdf")
println(to_markdown(doc, 0))

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("invoice.pdf");
const md = try doc.toMarkdown(a, 0);
defer a.free(md);
std.debug.print("{s}\n", .{md});

Scala

import fyi.oxide.pdf.PdfDocument
import scala.util.Using

Using.resource(PdfDocument.open("invoice.pdf")) { doc =>
  println(doc.toMarkdown(0))
}

Clojure

(require '[pdf-oxide.core :as pdf])

(with-open [d (pdf/open "invoice.pdf")]
  (println (pdf/to-markdown d 0)))

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"invoice.pdf" error:&err];
NSLog(@"%@", [doc toMarkdown:0 error:&err]);

Elixir

{:ok, doc} = PdfOxide.open("invoice.pdf")
{:ok, md} = PdfOxide.to_markdown(doc, 0)
IO.puts(md)

O PDF Oxide detecta layouts tabulares a partir da análise espacial de blocos de texto alinhados e gera tabelas no formato GitHub Flavored Markdown.

Por que a extração de tabelas de PDFs é tão difícil

Se você já tentou copiar uma tabela de um PDF e colar em uma planilha, sabe que o resultado costuma ser uma bagunça. Não é um bug no seu visualizador de PDF — isso reflete uma limitação fundamental do próprio formato PDF.

PDFs não têm o conceito de “tabela”. Ao contrário do HTML, que usa as tags <table>, <tr> e <td> para definir estrutura tabular, um arquivo PDF armazena apenas instruções de desenho: colocar este glifo nas coordenadas (x, y), traçar uma linha do ponto A ao ponto B. Não há uma camada semântica que diga “esses caracteres pertencem a uma célula na linha 3, coluna 2”. Toda biblioteca de extração de tabelas precisa reconstruir essa estrutura analisando as posições espaciais do texto e das linhas na página.

Essa reconstrução é difícil por várias razões:

  • Tabelas com e sem bordas. Quando uma tabela tem linhas de grade visíveis, as ferramentas de extração podem usá-las como limites de célula. Tabelas sem bordas — comuns em demonstrações financeiras, relatórios governamentais e artigos acadêmicos — não têm linhas. A biblioteca precisa inferir os limites das colunas somente a partir dos espaços em branco entre blocos de texto, o que é propenso a erros quando as colunas têm larguras variáveis ou quando valores numéricos estão alinhados à direita.

  • Células mescladas e cabeçalhos abrangentes. Uma célula de cabeçalho que abrange três colunas parece um único bloco de texto largo. Sem as linhas de grade para delimitá-la, um analisador não tem como saber de forma confiável quais colunas o cabeçalho cobre. Algumas bibliotecas lidam bem com isso; muitas produzem saída corrompida silenciosamente.

  • Conteúdo de célula em múltiplas linhas. Quando uma célula contém um parágrafo de texto que quebra em várias linhas, o processamento ingênuo baseado em linhas trata cada linha quebrada como uma linha separada. Agrupar corretamente essas linhas de volta em uma única célula requer entender a extensão vertical de cada linha.

  • Tabelas em múltiplas páginas. Tabelas grandes frequentemente se estendem por duas ou mais páginas. A linha de cabeçalho pode ou não ser repetida em cada página, e rodapés, marcas d’água ou números de página podem aparecer entre as linhas da tabela. Unir esses fragmentos em uma única tabela coerente requer lógica com reconhecimento de página.

  • Texto rotacionado e layouts não padrão. Alguns PDFs usam texto rotacionado para cabeçalhos de coluna, ou posicionam tabelas em layouts de página com múltiplas colunas. Esses casos extremos quebram as suposições que a maioria dos analisadores faz sobre a ordem de leitura da esquerda para a direita, de cima para baixo.

Compreender esses desafios ajuda a escolher a ferramenta certa para seus documentos específicos. Para tabelas alinhadas simples — a maioria de notas fiscais, confirmações de pedidos e relatórios simples — uma abordagem de análise espacial rápida como o PDF Oxide funciona bem. Para documentos com mesclagem complexa, layouts sem bordas ou formatação incomum, pode ser necessária uma biblioteca com heurísticas mais sofisticadas.

Extração de tabelas: PDF Oxide vs outras bibliotecas

Escolher uma biblioteca para extração de tabelas de PDF em Python depende dos seus documentos, requisitos de desempenho e como você precisa da saída formatada. Veja como as principais opções se comparam:

Biblioteca Detecção de tabela Tabelas com bordas Tabelas sem bordas Formato de saída Velocidade
PDF Oxide Integrada Sim Básico Markdown/HTML 0.8ms
pdfplumber Integrada Sim Avançado Listas Python 23.2ms
Camelot Integrada Sim Sim (lattice/stream) DataFrames ~50ms+
PyMuPDF Básico (v1.23+) Sim Limitado DataFrames 4.6ms
pypdf Não Não Não N/A N/A
tabula-py Integrada Sim Sim DataFrames ~100ms+ (Java)

PDF Oxide é a opção mais rápida com grande margem. Detecta tabelas por análise espacial de blocos de texto alinhados e produz tabelas limpas no GitHub Flavored Markdown. Com tempo médio de extração de 0,8ms, é 29x mais rápido que o pdfplumber e mais de 100x mais rápido que o tabula-py. Lida bem com tabelas com bordas e tabelas sem bordas alinhadas simples. Para pipelines de LLM onde você precisa de saída Markdown de qualquer forma, é a escolha natural.

pdfplumber tem a detecção de tabelas sem bordas mais madura. Seu método find_tables() usa estratégias configuráveis para detectar linhas e colunas com base no alinhamento de texto, e lida melhor com células mescladas e conteúdo de célula em múltiplas linhas do que a maioria das alternativas. A desvantagem é a velocidade: 23,2ms por página é significativamente mais lento para processamento em lote.

Camelot oferece dois modos de detecção — lattice (para tabelas com bordas) e stream (para tabelas sem bordas). Produz DataFrames do pandas diretamente, o que é conveniente para fluxos de trabalho de análise de dados. Porém, depende do Ghostscript e OpenCV, tornando a instalação mais pesada, e sua velocidade é a mais lenta entre as opções em Python puro.

PyMuPDF (fitz) adicionou extração básica de tabelas na versão 1.23. É rápido (4,6ms) e funciona bem para tabelas simples com bordas, mas o suporte a tabelas sem bordas é limitado em comparação com pdfplumber ou Camelot.

pypdf não tem nenhuma capacidade de detecção de tabelas. Extrai texto bruto, então você precisaria escrever sua própria lógica de análise para reconstruir a estrutura da tabela.

tabula-py é um wrapper Python em torno da biblioteca Tabula baseada em Java. Fornece boa detecção de tabelas tanto com bordas quanto sem bordas, mas requer um runtime Java e é a opção mais lenta devido à sobrecarga de inicialização da JVM. É mais adequada para tarefas de extração pontuais do que para pipelines de alta taxa de transferência.

Para a maioria dos casos de uso em produção, a abordagem recomendada é usar o PDF Oxide como seu extrator principal por velocidade e simplicidade, e recorrer ao pdfplumber para o subconjunto de documentos com layouts de tabela complexos que requerem heurísticas avançadas.

Instalação

pip install pdf_oxide

Extração básica de tabelas

Como tabelas Markdown

A abordagem mais simples — converter a página para Markdown, que inclui tabelas na sintaxe GFM:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
for i in range(doc.page_count()):
    md = doc.to_markdown(i, detect_headings=True)
    if "|" in md:  # Page contains a table
        print(f"--- Page {i + 1} ---")
        print(md)

WASM

const doc = new WasmPdfDocument(bytes);
for (let i = 0; i < doc.pageCount(); i++) {
    const md = doc.toMarkdown(i);
    if (md.includes("|")) { // Page contains a table
        console.log(`--- Page ${i + 1} ---`);
        console.log(md);
    }
}
doc.free();

Rust

let mut doc = PdfDocument::open("report.pdf")?;
for i in 0..doc.page_count()? {
    let md = doc.to_markdown(i, true)?;
    if md.contains("|") {
        println!("--- Page {} ---", i + 1);
        println!("{}", md);
    }
}

Go

doc, _ := pdfoxide.Open("report.pdf")
defer doc.Close()

n, _ := doc.PageCount()
for i := 0; i < n; i++ {
    md, _ := doc.ToMarkdown(i)
    if strings.Contains(md, "|") {
        fmt.Printf("--- Page %d ---\n%s\n", i+1, md)
    }
}

C#

using var doc = PdfDocument.Open("report.pdf");
for (int i = 0; i < doc.PageCount; i++)
{
    var md = doc.ToMarkdown(i);
    if (md.Contains("|"))
        Console.WriteLine($"--- Page {i + 1} ---\n{md}");
}

Java

try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("report.pdf"))) {
    for (int i = 0; i < doc.pageCount(); i++) {
        String md = doc.toMarkdown(i);
        if (md.contains("|")) {  // Page contains a table
            System.out.println("--- Page " + (i + 1) + " ---");
            System.out.println(md);
        }
    }
}

PHP

$doc = PdfDocument::open('report.pdf');
for ($i = 0; $i < $doc->pageCount(); $i++) {
    $md = $doc->toMarkdown($i);
    if (str_contains($md, '|')) {  // Page contains a table
        echo "--- Page " . ($i + 1) . " ---\n";
        echo $md;
    }
}
$doc->close();

Ruby

PdfOxide::PdfDocument.open('report.pdf') do |doc|
  doc.page_count.times do |i|
    md = doc.to_markdown(i)
    if md.include?('|')  # Page contains a table
      puts "--- Page #{i + 1} ---"
      puts md
    end
  end
end

C++

auto doc = pdf_oxide::Document::open("report.pdf");
for (int i = 0; i < doc.page_count(); i++) {
    auto md = doc.to_markdown(i);
    if (md.find('|') != std::string::npos) {  // Page contains a table
        std::cout << "--- Page " << (i + 1) << " ---\n" << md << '\n';
    }
}

Swift

let doc = try Document.open("report.pdf")
for i in 0..<(try doc.pageCount()) {
    let md = try doc.toMarkdown(i)
    if md.contains("|") {  // Page contains a table
        print("--- Page \(i + 1) ---")
        print(md)
    }
}

Kotlin

PdfDocument.open(java.nio.file.Path.of("report.pdf")).use { doc ->
    for (i in 0 until doc.pageCount()) {
        val md = doc.toMarkdown(i)
        if (md.contains("|")) {  // Page contains a table
            println("--- Page ${i + 1} ---")
            println(md)
        }
    }
}

Dart

final doc = PdfDocument.open('report.pdf');
for (var i = 0; i < doc.pageCount; i++) {
  final md = doc.toMarkdown(i);
  if (md.contains('|')) {  // Page contains a table
    print('--- Page ${i + 1} ---');
    print(md);
  }
}
doc.close();

R

doc <- pdf_open("report.pdf")
for (i in 0:(pdf_page_count(doc) - 1)) {
  md <- pdf_to_markdown(doc, i)
  if (grepl("\\|", md)) {  # Page contains a table
    cat(sprintf("--- Page %d ---\n%s\n", i + 1, md))
  }
}

Julia

doc = open_document("report.pdf")
for i in 0:(page_count(doc) - 1)
    md = to_markdown(doc, i)
    if occursin("|", md)  # Page contains a table
        println("--- Page $(i + 1) ---")
        println(md)
    end
end

Zig

var doc = try pdf_oxide.Document.open("report.pdf");
const n = try doc.pageCount();
var i: i32 = 0;
while (i < n) : (i += 1) {
    const md = try doc.toMarkdown(a, i);
    defer a.free(md);
    if (std.mem.indexOfScalar(u8, md, '|') != null) {  // Page contains a table
        std.debug.print("--- Page {d} ---\n{s}\n", .{ i + 1, md });
    }
}

Scala

Using.resource(PdfDocument.open("report.pdf")) { doc =>
  for (i <- 0 until doc.pageCount()) {
    val md = doc.toMarkdown(i)
    if (md.contains("|")) {  // Page contains a table
      println(s"--- Page ${i + 1} ---")
      println(md)
    }
  }
}

Clojure

(with-open [d (pdf/open "report.pdf")]
  (doseq [i (range (pdf/page-count d))]
    (let [md (pdf/to-markdown d i)]
      (when (.contains md "|")  ; Page contains a table
        (println (str "--- Page " (inc i) " ---"))
        (println md)))))

Objective-C

NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"report.pdf" error:&err];
for (NSInteger i = 0; i < [doc pageCountError:&err]; i++) {
    NSString *md = [doc toMarkdown:i error:&err];
    if ([md containsString:@"|"]) {  // Page contains a table
        NSLog(@"--- Page %ld ---\n%@", (long)(i + 1), md);
    }
}

Elixir

{:ok, doc} = PdfOxide.open("report.pdf")
{:ok, n} = PdfOxide.page_count(doc)
for i <- 0..(n - 1) do
  {:ok, md} = PdfOxide.to_markdown(doc, i)
  if String.contains?(md, "|") do  # Page contains a table
    IO.puts("--- Page #{i + 1} ---")
    IO.puts(md)
  end
end

Extração estruturada de tabelas (v0.3.34)

Para acesso tipado a linhas e bounding boxes sem precisar analisar Markdown, chame ExtractTables(pageIndex) (Go, C#) / extract_tables(page) (Python, Rust). Cada tabela expõe células estruturadas para que você possa direcionar os resultados diretamente a um banco de dados ou DataFrame sem regex.

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
for table in doc.extract_tables(0):
    for row in table.rows:
        print(row)

Rust

let mut doc = PdfDocument::open("invoice.pdf")?;
for table in doc.extract_tables(0)? {
    for row in &table.rows {
        println!("{:?}", row);
    }
}

Go

doc, _ := pdfoxide.Open("invoice.pdf")
defer doc.Close()

tables, _ := doc.ExtractTables(0)
for _, t := range tables {
    for _, row := range t.Rows {
        fmt.Println(row)
    }
}

C#

using var doc = PdfDocument.Open("invoice.pdf");
foreach (var table in doc.ExtractTables(0))
    foreach (var row in table.Rows)
        Console.WriteLine(string.Join(" | ", row));

Java

import fyi.oxide.pdf.PdfDocument;
import fyi.oxide.pdf.table.Table;
import fyi.oxide.pdf.table.TableCell;

try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("invoice.pdf"))) {
    for (Table table : doc.page(0).tables()) {
        String[][] grid = new String[table.rows()][table.cols()];
        for (TableCell c : table.cells()) grid[c.row()][c.col()] = c.text();
        for (String[] row : grid) System.out.println(String.join(" | ", row));
    }
}

C++

auto doc = pdf_oxide::Document::open("invoice.pdf");
for (const auto& table : doc.extract_tables(0)) {
    for (int r = 0; r < table.row_count; r++) {
        for (int c = 0; c < table.col_count; c++) {
            std::cout << table.cell(r, c);
            if (c + 1 < table.col_count) std::cout << " | ";
        }
        std::cout << '\n';
    }
}

Swift

let doc = try Document.open("invoice.pdf")
for table in try doc.extractTables(0) {
    for r in 0..<table.rowCount {
        let row = (0..<table.colCount).map { table.cell(r, $0) }
        print(row.joined(separator: " | "))
    }
}

Kotlin

import fyi.oxide.pdf.PdfDocument

PdfDocument.open(java.nio.file.Path.of("invoice.pdf")).use { doc ->
    for (table in doc.page(0).tables()) {
        val grid = Array(table.rows()) { arrayOfNulls<String>(table.cols()) }
        table.cells().forEach { grid[it.row()][it.col()] = it.text() }
        grid.forEach { println(it.joinToString(" | ")) }
    }
}

Dart

final doc = PdfDocument.open('invoice.pdf');
for (final table in doc.extractTables(0)) {
  for (var r = 0; r < table.rowCount; r++) {
    final row = [for (var c = 0; c < table.colCount; c++) table.cell(r, c)];
    print(row.join(' | '));
  }
}
doc.close();

R

doc <- pdf_open("invoice.pdf")
for (table in pdf_extract_tables(doc, 0)) {
  for (r in seq_len(table$row_count)) {
    cat(paste(table$cells[r, ], collapse = " | "), "\n")
  }
}

Julia

doc = open_document("invoice.pdf")
for table in extract_tables(doc, 0)
    for r in 1:table.row_count
        println(join(table.cells[r, :], " | "))
    end
end

Zig

var doc = try pdf_oxide.Document.open("invoice.pdf");
const tables = try doc.extractTables(a, 0);
defer pdf_oxide.Document.freeTables(a, tables);
for (tables) |table| {
    var r: i32 = 0;
    while (r < table.rowCount) : (r += 1) {
        var c: i32 = 0;
        while (c < table.colCount) : (c += 1) {
            std.debug.print("{s}", .{table.cell(r, c)});
            if (c + 1 < table.colCount) std.debug.print(" | ", .{});
        }
        std.debug.print("\n", .{});
    }
}

Scala

import fyi.oxide.pdf.PdfDocument
import scala.jdk.CollectionConverters._
import scala.util.Using

Using.resource(PdfDocument.open("invoice.pdf")) { doc =>
  for (table <- doc.page(0).tables().asScala) {
    val grid = Array.ofDim[String](table.rows(), table.cols())
    table.cells().asScala.foreach(c => grid(c.row())(c.col()) = c.text())
    grid.foreach(row => println(row.mkString(" | ")))
  }
}

Clojure

(with-open [d (pdf/open "invoice.pdf")]
  (doseq [table (pdf/tables (pdf/page d 0))]
    (let [grid (make-array String (.rows table) (.cols table))]
      (doseq [c (.cells table)]
        (aset grid (.row c) (.col c) (.text c)))
      (doseq [row grid]
        (println (clojure.string/join " | " row))))))

Objective-C

NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"invoice.pdf" error:&err];
for (POXTable *table in [doc extractTables:0 error:&err]) {
    for (NSInteger r = 0; r < table.rowCount; r++) {
        NSMutableArray<NSString *> *row = [NSMutableArray array];
        for (NSInteger c = 0; c < table.colCount; c++)
            [row addObject:([table cellTextAtRow:r col:c] ?: @"")];
        NSLog(@"%@", [row componentsJoinedByString:@" | "]);
    }
}

Elixir

{:ok, doc} = PdfOxide.open("invoice.pdf")
{:ok, tables} = PdfOxide.extract_tables(doc, 0)
for table <- tables do
  for r <- 0..(table.row_count - 1) do
    row = for c <- 0..(table.col_count - 1), do: PdfOxide.cell(table, r, c)
    IO.puts(Enum.join(row, " | "))
  end
end

Analisar tabelas Markdown em linhas

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0)

# Extract table rows from Markdown
rows = []
for line in md.split("\n"):
    line = line.strip()
    if line.startswith("|") and not line.startswith("|--"):
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        rows.append(cells)

header = rows[0] if rows else []
data = rows[1:] if len(rows) > 1 else []
print(f"Columns: {header}")
for row in data:
    print(row)

WASM

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0);

const rows = [];
for (const line of md.split("\n")) {
    const trimmed = line.trim();
    if (trimmed.startsWith("|") && !trimmed.startsWith("|--")) {
        const cells = trimmed.split("|").slice(1, -1).map(c => c.trim());
        rows.push(cells);
    }
}

const header = rows[0] || [];
const data = rows.slice(1);
console.log("Columns:", header);
data.forEach(row => console.log(row));
doc.free();

Rust

let mut doc = PdfDocument::open("invoice.pdf")?;
let md = doc.to_markdown(0, false)?;

let rows: Vec<Vec<String>> = md.lines()
    .map(|l| l.trim())
    .filter(|l| l.starts_with('|') && !l.starts_with("|--"))
    .map(|l| l.split('|').skip(1).map(|c| c.trim().to_string())
        .take_while(|c| !c.is_empty()).collect())
    .collect();

if let Some(header) = rows.first() {
    println!("Columns: {:?}", header);
    for row in &rows[1..] {
        println!("{:?}", row);
    }
}

Java

import fyi.oxide.pdf.PdfDocument;
import java.util.*;

try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("invoice.pdf"))) {
    String md = doc.toMarkdown(0);
    List<List<String>> rows = new ArrayList<>();
    for (String line : md.split("\n")) {
        line = line.strip();
        if (line.startsWith("|") && !line.startsWith("|--")) {
            String[] parts = line.substring(1, line.length() - 1).split("\\|");
            List<String> cells = new ArrayList<>();
            for (String p : parts) cells.add(p.strip());
            rows.add(cells);
        }
    }
    System.out.println("Columns: " + (rows.isEmpty() ? List.of() : rows.get(0)));
    for (int i = 1; i < rows.size(); i++) System.out.println(rows.get(i));
}

PHP

$doc = PdfDocument::open('invoice.pdf');
$md = $doc->toMarkdown(0);

$rows = [];
foreach (explode("\n", $md) as $line) {
    $line = trim($line);
    if (str_starts_with($line, '|') && !str_starts_with($line, '|--')) {
        $cells = array_map('trim', array_slice(explode('|', $line), 1, -1));
        $rows[] = $cells;
    }
}
$header = $rows[0] ?? [];
echo "Columns: " . implode(', ', $header) . "\n";
foreach (array_slice($rows, 1) as $row) {
    echo implode(' | ', $row) . "\n";
}
$doc->close();

Ruby

PdfOxide::PdfDocument.open('invoice.pdf') do |doc|
  md = doc.to_markdown(0)

  rows = md.lines.map(&:strip)
            .select { |l| l.start_with?('|') && !l.start_with?('|--') }
            .map { |l| l.split('|')[1..-2].map(&:strip) }

  header = rows.first || []
  puts "Columns: #{header.inspect}"
  rows.drop(1).each { |row| puts row.inspect }
end

C++

#include <pdf_oxide/pdf_oxide.hpp>
#include <sstream>
#include <vector>

auto doc = pdf_oxide::Document::open("invoice.pdf");
auto md = doc.to_markdown(0);

std::vector<std::vector<std::string>> rows;
std::istringstream stream(md);
for (std::string line; std::getline(stream, line);) {
    auto s = line.find_first_not_of(" \t");
    if (s == std::string::npos) continue;
    line = line.substr(s);
    if (line.rfind("|", 0) != 0 || line.rfind("|--", 0) == 0) continue;
    std::vector<std::string> cells;
    std::istringstream cs(line.substr(1, line.size() - 2));
    for (std::string cell; std::getline(cs, cell, '|');) cells.push_back(cell);
    rows.push_back(cells);
}

Swift

let doc = try Document.open("invoice.pdf")
let md = try doc.toMarkdown(0)

let rows = md.split(separator: "\n").map { $0.trimmingCharacters(in: .whitespaces) }
    .filter { $0.hasPrefix("|") && !$0.hasPrefix("|--") }
    .map { line -> [String] in
        line.dropFirst().dropLast().split(separator: "|", omittingEmptySubsequences: false)
            .map { $0.trimmingCharacters(in: .whitespaces) }
    }

if let header = rows.first {
    print("Columns:", header)
    for row in rows.dropFirst() { print(row) }
}

Kotlin

PdfDocument.open(java.nio.file.Path.of("invoice.pdf")).use { doc ->
    val md = doc.toMarkdown(0)
    val rows = md.split("\n").map { it.trim() }
        .filter { it.startsWith("|") && !it.startsWith("|--") }
        .map { it.removeSurrounding("|").split("|").map(String::trim) }

    rows.firstOrNull()?.let { println("Columns: $it") }
    rows.drop(1).forEach { println(it) }
}

Dart

final doc = PdfDocument.open('invoice.pdf');
final md = doc.toMarkdown(0);

final rows = md.split('\n').map((l) => l.trim())
    .where((l) => l.startsWith('|') && !l.startsWith('|--'))
    .map((l) => l.substring(1, l.length - 1).split('|').map((c) => c.trim()).toList())
    .toList();

if (rows.isNotEmpty) {
  print('Columns: ${rows.first}');
  for (final row in rows.skip(1)) print(row);
}
doc.close();

R

doc <- pdf_open("invoice.pdf")
md  <- pdf_to_markdown(doc, 0)

lines <- trimws(strsplit(md, "\n")[[1]])
lines <- lines[startsWith(lines, "|") & !startsWith(lines, "|--")]
rows  <- lapply(lines, function(l) {
  cells <- strsplit(l, "\\|")[[1]]
  trimws(cells[2:(length(cells) - 1)])
})

if (length(rows) > 0) {
  cat("Columns:", rows[[1]], "\n")
  for (row in rows[-1]) cat(row, "\n")
}

Julia

doc = open_document("invoice.pdf")
md  = to_markdown(doc, 0)

rows = [strip.(split(l, "|")[2:end-1])
        for l in strip.(split(md, "\n"))
        if startswith(l, "|") && !startswith(l, "|--")]

if !isempty(rows)
    println("Columns: ", rows[1])
    for row in rows[2:end]
        println(row)
    end
end

Zig

var doc = try pdf_oxide.Document.open("invoice.pdf");
const md = try doc.toMarkdown(a, 0);
defer a.free(md);

var lines = std.mem.splitScalar(u8, md, '\n');
while (lines.next()) |raw| {
    const line = std.mem.trim(u8, raw, " \t\r");
    if (!std.mem.startsWith(u8, line, "|") or std.mem.startsWith(u8, line, "|--")) continue;
    const inner = line[1 .. line.len - 1];
    var cells = std.mem.splitScalar(u8, inner, '|');
    while (cells.next()) |cell| {
        std.debug.print("{s}\t", .{std.mem.trim(u8, cell, " \t")});
    }
    std.debug.print("\n", .{});
}

Scala

Using.resource(PdfDocument.open("invoice.pdf")) { doc =>
  val md = doc.toMarkdown(0)
  val rows = md.split("\n").map(_.trim)
    .filter(l => l.startsWith("|") && !l.startsWith("|--"))
    .map(_.stripPrefix("|").stripSuffix("|").split("\\|").map(_.trim).toList)
    .toList

  rows.headOption.foreach(h => println(s"Columns: $h"))
  rows.drop(1).foreach(println)
}

Clojure

(with-open [d (pdf/open "invoice.pdf")]
  (let [md   (pdf/to-markdown d 0)
        rows (->> (clojure.string/split-lines md)
                  (map clojure.string/trim)
                  (filter #(and (.startsWith % "|") (not (.startsWith % "|--"))))
                  (map #(->> (clojure.string/split % #"\|")
                             (drop 1) (butlast) (map clojure.string/trim) vec)))]
    (when-let [header (first rows)]
      (println "Columns:" header)
      (doseq [row (rest rows)] (println row)))))

Objective-C

NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"invoice.pdf" error:&err];
NSString *md = [doc toMarkdown:0 error:&err];

NSMutableArray<NSArray<NSString *> *> *rows = [NSMutableArray array];
for (NSString *raw in [md componentsSeparatedByString:@"\n"]) {
    NSString *line = [raw stringByTrimmingCharactersInSet:
        [NSCharacterSet whitespaceCharacterSet]];
    if (![line hasPrefix:@"|"] || [line hasPrefix:@"|--"]) continue;
    NSArray<NSString *> *parts = [line componentsSeparatedByString:@"|"];
    NSMutableArray<NSString *> *cells = [NSMutableArray array];
    for (NSUInteger i = 1; i + 1 < parts.count; i++)
        [cells addObject:[parts[i] stringByTrimmingCharactersInSet:
            [NSCharacterSet whitespaceCharacterSet]]];
    [rows addObject:cells];
}
if (rows.count > 0) NSLog(@"Columns: %@", rows[0]);

Elixir

{:ok, doc} = PdfOxide.open("invoice.pdf")
{:ok, md} = PdfOxide.to_markdown(doc, 0)

rows =
  md
  |> String.split("\n")
  |> Enum.map(&String.trim/1)
  |> Enum.filter(&(String.starts_with?(&1, "|") and not String.starts_with?(&1, "|--")))
  |> Enum.map(fn line ->
    line |> String.split("|") |> Enum.slice(1..-2//1) |> Enum.map(&String.trim/1)
  end)

case rows do
  [header | data] ->
    IO.puts("Columns: #{inspect(header)}")
    Enum.each(data, &IO.inspect/1)
  [] ->
    :ok
end

Exportar para CSV

import csv
from pdf_oxide import PdfDocument

doc = PdfDocument("invoice.pdf")
md = doc.to_markdown(0)

rows = []
for line in md.split("\n"):
    line = line.strip()
    if line.startswith("|") and not line.startswith("|--"):
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        rows.append(cells)

with open("table.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(rows)

Exportar para Pandas DataFrame

import pandas as pd
from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
md = doc.to_markdown(0)

rows = []
for line in md.split("\n"):
    line = line.strip()
    if line.startswith("|") and not line.startswith("|--"):
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        rows.append(cells)

if rows:
    df = pd.DataFrame(rows[1:], columns=rows[0])
    print(df)

Usando posições de caracteres para análise personalizada de tabelas

Para controle granular, use extração em nível de caractere e análise espacial:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("financial.pdf")
chars = doc.extract_chars(0)

# Group characters by Y position (rows)
rows = {}
for ch in chars:
    row_key = round(ch.y / 2) * 2  # Snap to 2pt grid
    rows.setdefault(row_key, []).append(ch)

# Sort rows top-to-bottom, characters left-to-right
for y in sorted(rows.keys(), reverse=True):
    line_chars = sorted(rows[y], key=lambda c: c.x)
    text = "".join(c.char for c in line_chars)
    print(text)

WASM

const doc = new WasmPdfDocument(bytes);
const chars = doc.extractChars(0);

// Group characters by Y position (rows)
const rows = new Map();
for (const ch of chars) {
    const rowKey = Math.round(ch.y / 2) * 2; // Snap to 2pt grid
    if (!rows.has(rowKey)) rows.set(rowKey, []);
    rows.get(rowKey).push(ch);
}

// Sort rows top-to-bottom, characters left-to-right
const sortedKeys = [...rows.keys()].sort((a, b) => b - a);
for (const y of sortedKeys) {
    const lineChars = rows.get(y).sort((a, b) => a.x - b.x);
    const text = lineChars.map(c => c.char).join("");
    console.log(text);
}
doc.free();

Rust

use std::collections::BTreeMap;

let mut doc = PdfDocument::open("financial.pdf")?;
let chars = doc.extract_chars(0)?;

let mut rows: BTreeMap<i32, Vec<_>> = BTreeMap::new();
for ch in &chars {
    let row_key = ((ch.y / 2.0).round() * 2.0) as i32;
    rows.entry(row_key).or_default().push(ch);
}

for (_, line_chars) in rows.iter().rev() {
    let mut sorted = line_chars.clone();
    sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
    let text: String = sorted.iter().map(|c| c.char).collect();
    println!("{}", text);
}

Go

doc, _ := pdfoxide.Open("financial.pdf")
defer doc.Close()

chars, _ := doc.ExtractChars(0)
rows := map[int][]pdfoxide.Char{}
for _, ch := range chars {
    key := int(math.Round(float64(ch.Y)/2) * 2)
    rows[key] = append(rows[key], ch)
}

keys := make([]int, 0, len(rows))
for k := range rows { keys = append(keys, k) }
sort.Sort(sort.Reverse(sort.IntSlice(keys)))

for _, y := range keys {
    line := rows[y]
    sort.Slice(line, func(i, j int) bool { return line[i].X < line[j].X })
    var b strings.Builder
    for _, c := range line { b.WriteString(c.Char) }
    fmt.Println(b.String())
}

C#

using var doc = PdfDocument.Open("financial.pdf");
var chars = doc.ExtractChars(0);

var rows = chars
    .GroupBy(c => (int)(Math.Round(c.Y / 2) * 2))
    .OrderByDescending(g => g.Key);

foreach (var row in rows)
{
    var line = string.Concat(row.OrderBy(c => c.X).Select(c => c.Char));
    Console.WriteLine(line);
}

C++

#include <pdf_oxide/pdf_oxide.hpp>
#include <map>
#include <vector>
#include <algorithm>

auto doc = pdf_oxide::Document::open("financial.pdf");
auto chars = doc.extract_chars(0);

// Group characters by Y position (rows)
std::map<int, std::vector<pdf_oxide::Char>> rows;
for (const auto& ch : chars) {
    int key = static_cast<int>(std::lround(ch.bbox.y / 2.0) * 2);
    rows[key].push_back(ch);
}

// Sort rows top-to-bottom, characters left-to-right
for (auto it = rows.rbegin(); it != rows.rend(); ++it) {
    auto& line = it->second;
    std::sort(line.begin(), line.end(),
              [](const auto& a, const auto& b) { return a.bbox.x < b.bbox.x; });
    std::string text;
    for (const auto& c : line) text += static_cast<char>(c.character);
    std::cout << text << '\n';
}

Swift

let doc = try Document.open("financial.pdf")
let chars = try doc.extractChars(0)

// Group characters by Y position (rows)
var rows: [Int: [Char]] = [:]
for ch in chars {
    let key = Int((ch.bbox.y / 2).rounded()) * 2  // Snap to 2pt grid
    rows[key, default: []].append(ch)
}

// Sort rows top-to-bottom, characters left-to-right
for y in rows.keys.sorted(by: >) {
    let line = rows[y]!.sorted { $0.bbox.x < $1.bbox.x }
    let text = String(line.compactMap { Unicode.Scalar($0.character).map(Character.init) })
    print(text)
}

Dart

final doc = PdfDocument.open('financial.pdf');
final chars = doc.extractChars(0);

// Group characters by Y position (rows)
final rows = <int, List<Char>>{};
for (final ch in chars) {
  final key = (ch.bbox.y / 2).round() * 2; // Snap to 2pt grid
  rows.putIfAbsent(key, () => []).add(ch);
}

// Sort rows top-to-bottom, characters left-to-right
final keys = rows.keys.toList()..sort((a, b) => b - a);
for (final y in keys) {
  final line = rows[y]!..sort((a, b) => a.bbox.x.compareTo(b.bbox.x));
  final text = String.fromCharCodes(line.map((c) => c.character));
  print(text);
}
doc.close();

R

doc   <- pdf_open("financial.pdf")
chars <- pdf_extract_chars(doc, 0)

# Group characters by Y position (rows), snapped to a 2pt grid
keys <- sapply(chars, function(ch) round(ch$bbox$y / 2) * 2)
for (y in sort(unique(keys), decreasing = TRUE)) {
  line <- chars[keys == y]
  line <- line[order(sapply(line, function(c) c$bbox$x))]
  text <- paste(intToUtf8(sapply(line, function(c) c$character), multiple = TRUE),
                collapse = "")
  cat(text, "\n")
}

Julia

doc   = open_document("financial.pdf")
chars = extract_chars(doc, 0)

# Group characters by Y position (rows), snapped to a 2pt grid
rows = Dict{Int,Vector}()
for ch in chars
    key = round(Int, ch.bbox.y / 2) * 2
    push!(get!(rows, key, []), ch)
end

for y in sort(collect(keys(rows)), rev = true)
    line = sort(rows[y], by = c -> c.bbox.x)
    text = join(Char.(getfield.(line, :character)))
    println(text)
end

Zig

var doc = try pdf_oxide.Document.open("financial.pdf");
const chars = try doc.extractChars(a, 0);
defer pdf_oxide.Document.freeChars(a, chars);

// Group characters by Y position (rows)
var rows = std.AutoArrayHashMap(i32, std.ArrayList(pdf_oxide.Char)).init(a);
for (chars) |ch| {
    const key: i32 = @intFromFloat(@round(ch.bbox.y / 2.0) * 2.0);
    const gop = try rows.getOrPut(key);
    if (!gop.found_existing) gop.value_ptr.* = std.ArrayList(pdf_oxide.Char).init(a);
    try gop.value_ptr.append(ch);
}

// Sort keys descending (top-to-bottom), characters left-to-right
const keys = rows.keys();
std.mem.sort(i32, keys, {}, comptime std.sort.desc(i32));
for (keys) |y| {
    var line = rows.get(y).?;
    std.mem.sort(pdf_oxide.Char, line.items, {}, struct {
        fn lt(_: void, x: pdf_oxide.Char, z: pdf_oxide.Char) bool { return x.bbox.x < z.bbox.x; }
    }.lt);
    for (line.items) |c| {
        var buf: [4]u8 = undefined;
        const len = std.unicode.utf8Encode(@intCast(c.character), &buf) catch 0;
        std.debug.print("{s}", .{buf[0..len]});
    }
    std.debug.print("\n", .{});
}

Objective-C

NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"financial.pdf" error:&err];
NSArray<POXChar *> *chars = [doc extractChars:0 error:&err];

// Group characters by Y position (rows)
NSMutableDictionary<NSNumber *, NSMutableArray<POXChar *> *> *rows =
    [NSMutableDictionary dictionary];
for (POXChar *ch in chars) {
    NSNumber *key = @((NSInteger)(round(ch.bbox.y / 2.0) * 2));
    if (!rows[key]) rows[key] = [NSMutableArray array];
    [rows[key] addObject:ch];
}

// Sort rows top-to-bottom, characters left-to-right
NSArray<NSNumber *> *keys = [[rows allKeys]
    sortedArrayUsingSelector:@selector(compare:)];
for (NSNumber *y in [keys reverseObjectEnumerator]) {
    NSArray<POXChar *> *line = [rows[y] sortedArrayUsingComparator:
        ^(POXChar *x, POXChar *z) { return [@(x.bbox.x) compare:@(z.bbox.x)]; }];
    NSMutableString *text = [NSMutableString string];
    for (POXChar *c in line)
        [text appendString:[[NSString alloc] initWithBytes:&(uint32_t){c.character}
            length:4 encoding:NSUTF32LittleEndianStringEncoding]];
    NSLog(@"%@", text);
}

Elixir

{:ok, doc} = PdfOxide.open("financial.pdf")
{:ok, chars} = PdfOxide.extract_chars(doc, 0)

# Group characters by Y position (rows), snapped to a 2pt grid
chars
|> Enum.group_by(fn ch -> round(ch.bbox.y / 2) * 2 end)
|> Enum.sort_by(fn {y, _} -> -y end)
|> Enum.each(fn {_y, line} ->
  text =
    line
    |> Enum.sort_by(fn c -> c.bbox.x end)
    |> Enum.map(fn c -> <<c.character::utf8>> end)
    |> Enum.join()

  IO.puts(text)
end)

Exportar tabelas para Markdown

Markdown é o formato de saída ideal quando você está alimentando conteúdo de PDF em um modelo de linguagem grande, construindo um pipeline RAG ou armazenando dados extraídos em um formato que é legível por humanos e analisável por máquinas. O PDF Oxide gera tabelas no formato GitHub Flavored Markdown (GFM) nativamente, então nenhuma etapa de conversão adicional é necessária.

from pdf_oxide import PdfDocument

doc = PdfDocument("quarterly-report.pdf")

# Extract all tables across all pages as Markdown
all_tables = []
for i in range(doc.page_count()):
    md = doc.to_markdown(i, detect_headings=True)
    # Split the markdown into sections and find table blocks
    in_table = False
    current_table = []
    for line in md.split("\n"):
        if line.strip().startswith("|"):
            in_table = True
            current_table.append(line)
        else:
            if in_table and current_table:
                all_tables.append("\n".join(current_table))
                current_table = []
            in_table = False

    if current_table:
        all_tables.append("\n".join(current_table))

print(f"Found {len(all_tables)} tables")
for idx, table in enumerate(all_tables):
    print(f"\n--- Table {idx + 1} ---")
    print(table)

A saída de tabela GFM é diretamente compatível com prompts de LLM. Você pode passá-la diretamente para uma chamada de API da OpenAI ou Anthropic e o modelo entenderá a estrutura tabular sem nenhuma formatação adicional:

# Feed extracted table to an LLM for analysis
prompt = f"""Analyze the following financial table and summarize the key trends:

{all_tables[0]}
"""

Essa abordagem é significativamente mais rápida do que extrair tabelas com pdfplumber e depois convertê-las para Markdown você mesmo.

Lidando com tabelas em múltiplas páginas

Tabelas que se estendem por várias páginas são um desafio comum na extração de PDF. Demonstrativos financeiros, listas de inventário e documentos regulatórios frequentemente contêm tabelas que se estendem por duas, cinco ou até dezenas de páginas. A ideia central é extrair a tabela de cada página separadamente e depois unir as linhas, tomando cuidado para tratar cabeçalhos repetidos e artefatos de página.

from pdf_oxide import PdfDocument

doc = PdfDocument("long-report.pdf")

def extract_table_rows(md_text):
    """Extract table rows from markdown text, returning header and data separately."""
    header = None
    data_rows = []
    for line in md_text.split("\n"):
        line = line.strip()
        if not line.startswith("|") or line.startswith("|--"):
            continue
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        if header is None:
            header = cells
        else:
            data_rows.append(cells)
    return header, data_rows

# Collect rows across all pages
combined_header = None
combined_rows = []

for i in range(doc.page_count()):
    md = doc.to_markdown(i)
    header, rows = extract_table_rows(md)

    if header is None:
        continue  # No table on this page

    if combined_header is None:
        combined_header = header
    elif header == combined_header:
        pass  # Skip repeated header on subsequent pages
    else:
        # Different table — save current and start new
        print(f"Table with {len(combined_rows)} rows found")
        combined_header = header
        combined_rows = []

    combined_rows.extend(rows)

if combined_header and combined_rows:
    print(f"Columns: {combined_header}")
    print(f"Total rows: {len(combined_rows)}")
    for row in combined_rows[:5]:
        print(row)
    if len(combined_rows) > 5:
        print(f"... and {len(combined_rows) - 5} more rows")

Esse padrão funciona de forma confiável para tabelas onde a linha de cabeçalho é repetida em cada página (o caso mais comum). Para tabelas onde o cabeçalho aparece apenas na primeira página, você pode simplificar a lógica capturando o cabeçalho apenas da primeira página que contém uma tabela e tratando todas as linhas subsequentes como dados.

Exportar tabelas para CSV ou DataFrame

Depois de extrair os dados da tabela, muitas vezes você precisa deles em um formato estruturado para análise posterior. Os exemplos abaixo mostram como ir de um PDF para um DataFrame do pandas ou um arquivo CSV em poucas linhas.

Exportação em lote: todas as tabelas para arquivos CSV separados

import csv
from pdf_oxide import PdfDocument

doc = PdfDocument("catalog.pdf")
table_count = 0

for i in range(doc.page_count()):
    md = doc.to_markdown(i)
    rows = []
    for line in md.split("\n"):
        line = line.strip()
        if line.startswith("|") and not line.startswith("|--"):
            cells = [cell.strip() for cell in line.split("|")[1:-1]]
            rows.append(cells)

    if len(rows) > 1:  # At least header + one data row
        table_count += 1
        filename = f"table_page{i + 1}_{table_count}.csv"
        with open(filename, "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerows(rows)
        print(f"Saved {filename} ({len(rows) - 1} data rows)")

print(f"Exported {table_count} tables total")

Tabela em múltiplas páginas para DataFrame

Para tabelas que se estendem por várias páginas, combine o padrão de união de páginas com o pandas:

import pandas as pd
from pdf_oxide import PdfDocument

doc = PdfDocument("financial-statement.pdf")

header = None
all_rows = []

for i in range(doc.page_count()):
    md = doc.to_markdown(i)
    for line in md.split("\n"):
        line = line.strip()
        if not line.startswith("|") or line.startswith("|--"):
            continue
        cells = [cell.strip() for cell in line.split("|")[1:-1]]
        if header is None:
            header = cells
        elif cells == header:
            continue  # Skip repeated header
        else:
            all_rows.append(cells)

if header and all_rows:
    df = pd.DataFrame(all_rows, columns=header)
    # Clean up numeric columns
    for col in df.columns:
        # Try to convert columns that look numeric
        cleaned = df[col].str.replace(r"[$,%]", "", regex=True).str.strip()
        try:
            df[col] = pd.to_numeric(cleaned)
        except (ValueError, TypeError):
            pass  # Keep as string

    print(df.dtypes)
    print(df.head(10))
    df.to_csv("financial_data.csv", index=False)

Esse fluxo de trabalho fornece um DataFrame limpo com tipos numéricos adequados, pronto para análise com pandas, plotagem com matplotlib ou carregamento em um banco de dados.

Tabelas complexas: quando usar pdfplumber

A detecção de tabelas do PDF Oxide lida bem com tabelas alinhadas padrão. Para casos complexos — células mescladas, cabeçalhos abrangentes, tabelas sem bordas ou conteúdo de célula em múltiplas linhas — os algoritmos dedicados de extração de tabelas do pdfplumber são mais robustos:

import pdfplumber

with pdfplumber.open("complex-report.pdf") as pdf:
    page = pdf.pages[0]
    tables = page.extract_tables()
    for table in tables:
        for row in table:
            print(row)

Quando usar cada um

Cenário Recomendado
Tabelas alinhadas simples PDF Oxide (29× mais rápido)
Tabelas como parte do Markdown de página inteira PDF Oxide
Células mescladas complexas / cabeçalhos abrangentes pdfplumber
Tabelas sem bordas pdfplumber
Processamento em lote crítico para velocidade PDF Oxide

Use ambos juntos

Extração rápida de texto com PDF Oxide, extração de tabelas complexas com pdfplumber:

from pdf_oxide import PdfDocument
import pdfplumber

# Fast full-text extraction
doc = PdfDocument("report.pdf")
text = doc.extract_text(0)

# Targeted table extraction for complex pages
with pdfplumber.open("report.pdf") as pdf:
    tables = pdf.pages[0].extract_tables()

Páginas relacionadas