You extract text from a PDF invoice or report, expecting rows, and get every value from the first column, then every value from the second, then the third. The data is all there. It has just been reassembled into nonsense.
Invoice
INV-1001
INV-1002
INV-1003
Date
2026-07-01
2026-07-08
2026-07-15
Amount
1,250.00
980.50
4,310.75The cause: PDFs do not store tables
A PDF has no concept of a table, a row, or a cell. Page content is a stream of drawing operators: set this font, move to this position, show this string. What looks like a grid on screen is just text positioned so that it lines up.
Text extractors that walk the content stream in order hand back strings in the order the generator wrote them, and nothing in the format requires that order to match reading order. If the generator drew the table one column at a time, stream order is column order. Your extractor is faithfully reporting what is in the file.
The fix: group text by coordinates, not stream order
Use an extractor that reconstructs the grid from word positions. In Python, pdfplumber's extract_table does this, and the "text" strategies let it infer column and row boundaries from how the words align, which is what you need when the table has no ruling lines:
import pdfplumber
with pdfplumber.open("table.pdf") as pdf:
page = pdf.pages[0]
table = page.extract_table({
"vertical_strategy": "text",
"horizontal_strategy": "text",
})
rows = [r for r in table if any(cell and cell.strip() for cell in r)]
for row in rows:
print(row)Output:
['Invoice', 'Date', 'Amount']
['INV-1001', '2026-07-01', '1,250.00']
['INV-1002', '2026-07-08', '980.50']
['INV-1003', '2026-07-15', '4,310.75']Caveats
The text strategy infers row boundaries from word positions, which can produce extra empty rows, as it did here. Filter them, as above.
If the table does have visible ruling lines, leave the strategies at their defaults. pdfplumber defaults both to "lines", which uses the graphical lines and rectangle edges actually on the page instead of inferring boundaries from the text.
If you only need readable text rather than structured cells, pypdf can preserve the visual layout without a second dependency: page.extract_text(extraction_mode="layout").
None of this helps with a scanned PDF, which holds an image rather than text. There is nothing to position until you OCR the page first.
Back to All Questions