A table in a PDF is often a visual arrangement of text rather than a stored spreadsheet. Converting it to CSV requires deciding which pieces form a row, which labels are headers, and what to do when a row continues on another page.
This example extracts a 72-row service report into four CSV columns: item, quantity, amount, and source page. It validates the complete ordered item list before writing the file. Download the example projects, fixtures, and results.
Start with the logical row schema
The report contains service names from Service 001 through Service 072, quantity 1 for every row, and amount 12.50. Its grand total is a summary, not another item row. Repeated column headers should also be excluded.
The extraction schema describes the row data explicitly. Monetary amounts are decimal strings, while quantity and original page number are integers. The schema does not ask the API to infer arbitrary spreadsheet formulas or reproduce column widths.
The complete implementation is in tables.py. Its prompt asks for document order, original one-based page numbers, joined wrapped text within a row, and exclusion of repeated headers and section labels. These are extraction instructions, not guarantees; the subsequent validation is what prevents an unexpected result from being accepted silently.
Run the PDF-to-CSV example
Install the Python requirements, authenticate with npx @relaypdf/cli setup --env .env, and load the generated environment variables. Then run:
python tables.py fixtures/report.pdf work/report.csv
For an image-based version of the same report, request vision reading:
python tables.py your-scanned-service-report.pdf work/report-scan.csv --vision
The second command still expects this tutorial's 72 services and amounts. Before using a different report, replace the fixture-specific acceptance checks with rules appropriate to that document. Do not remove all validation just to make an unfamiliar input pass.
--vision sets the extraction request's ocr flag. It changes how the document is read; it does not output a searchable PDF. Use the OCR operation when that is the artifact you need.
Validate before serializing
The script first checks that all submitted pages were analyzed. It validates the schema, then compares the extracted item list with the expected ordered sequence. This catches omitted items, duplicate rows, and unexpected reordering.
It also checks the fixture's quantity and amount values and ensures source pages are within the document. A broader application should validate against known row identifiers, control totals, or a source-system record count when available. A row count alone can pass when one missing row is replaced by a duplicate.
The candidate JSON is retained beside the CSV after validation. Keeping structured data makes it possible to inspect provenance and rerun a different CSV export without submitting the document again.
Recorded extraction result
The production API run on September 7, 2026 recovered all 72 service rows in the expected order. The script verified every quantity and amount, checked page coverage, and wrote the resulting CSV. The structured response preserves the returned source pages and citations for inspection.
The merged-section variant also returned all 72 expected rows while excluding the extra heading. Download its CSV output and source PDF. These checks cover the supplied layouts, not a general success rate for arbitrary merged cells. Evaluate each additional layout separately.
The 120-DPI scanned version also passed the 72-row checks using the vision path. Download the scanned input, scan-derived CSV, and structured scan result. This clean synthetic scan is a narrow test; it does not negate the degraded-scan failures shown in the OCR guide.
Handle the difficult layouts explicitly
| Layout issue | Suggested treatment | Acceptance check |
|---|---|---|
| Repeated page headers | Exclude header labels from item rows | Expected row identifiers and count |
| A description wrapping onto another line | Join text belonging to the same logical row | Amount and identifier stay with the description |
| A merged section heading | Exclude it, or carry it into a separate category column | No unexplained blank amount rows |
| A table continuing onto another page | Preserve document order and source page | No duplicate or omitted boundary rows |
| A scanned table | Use recognition and inspect ambiguous cells | Compare important values with source evidence |
The supplied multipage report exercises repeated headers and page boundaries. A companion merged-section fixture is included in the archive. These fixtures do not cover every table layout; nested tables, rotated columns, and partially cropped scans deserve their own tests.
Write CSV with a library
Do not build CSV by joining values with commas. A description may contain a comma, a quotation mark, or a newline. Python's CSV writer handles field quoting correctly:
import csv
import io
output = io.StringIO(newline="")
writer = csv.writer(output)
writer.writerow(["item", "quantity", "amount", "page"])
for row in data["rows"]:
writer.writerow([row["item"], row["quantity"], row["amount"], row["page"]])
The downloadable implementation also prefixes potentially formula-like text cells before spreadsheet use. Ordinary CSV quoting alone does not stop spreadsheet software from interpreting a cell beginning with = as a formula. Keep machine-import and human-spreadsheet requirements explicit if you need to preserve the exact original text.
CSV carries no reliable column types. Spreadsheet software can strip leading zeros from identifiers or reinterpret dates. Import identifier columns as text and specify the delimiter and encoding in your downstream workflow.
Know what the result represents
This pipeline reconstructs data from the PDF. If the upstream application already has the original database rows or spreadsheet, export those directly when possible. They avoid the interpretation step entirely.
The supplied report PDF, expected service sequence in the source, and runnable script make the tutorial reproducible. Service measurements and any unavailable checks are disclosed in the downloadable results; authored expected rows are not represented as measured extraction output.
For schema design and request behavior, read PDF to JSON in Python. For currency and total validation, use the invoice extraction guide. The data extraction reference documents the API used here, and PDF data extraction describes the product surface.