tabula-py read_pdf, tested on 84 tables
4,663
of 5,376 cells right, one fallback line
Six lines, 48 of 84 tables exact
48 of 84 tables exact, every cell
0 empty lists (stream alone: 6)
- Check Java with
java -version. I had OpenJDK 21. - Run
pip install tabula-py jpype1. -
Save the code and call
table_rows("statement.pdf").
import tabula
def tables(path, **how):
dfs = tabula.read_pdf(path, pages="all", pandas_options={"header": None, "dtype": str}, **how)
return [df.fillna("").values.tolist() for df in dfs]
def table_rows(path):
return tables(path, stream=True) or tables(path, stream=True, guess=False)
Each table comes back as a list of rows, every cell as text.
Five settings, same 84 tables
| Setting | Exact | Cells |
|---|---|---|
| default | 42 | 4,114 |
lattice=True |
10 | 762 |
stream=True |
44 | 4,340 |
stream, guess=False |
25 | 3,163 |
stream, then guess=False if empty
|
48 | 4,663 |
The test PDFs come from 3 PDF writers: ruled grids, tables with some lines, and tables with no lines. Every value is made up, so I know each true cell.
Empty list? The guess found nothing
read_pdf first guesses where the table sits. On 6 of 84 PDFs it found no table, so it returned an empty list with no error:
>>> tables("statement.pdf", stream=True)
[]
All 6 were bank statement pages from one PDF writer, reportlab. With
guess=False, tabula reads the whole page, and 4 of the 6
came back exact.
On every page, guess=False did worse. So the code tries
it only after an empty result.
612.40 came back as 612.4
Without "dtype": str, tabula-py turns each column of
numbers into numbers. On 5 of 84 PDFs, 52 cells changed:
| In the PDF | read_pdf gave |
|---|---|
| 612.40 | 612.4 |
| 17.50 | 17.5 |
| 331.10 | 331.1 |
The pandas option in the code above stops it. Every cell stays the text that the PDF shows.
Lattice reads ruled grids, nothing else
lattice=True follows the drawn lines of a table. It read
10 of 12 ruled grids exactly. On the 36 pages with no lines, it found
nothing.
On 2 grids it cut the last letter off the right column:
'Balance' as 'Balanc'
'4,218.40' as '4,218.4'
It is fast: 0.01 s a page, against 0.1 s for stream. On a ruled grid, check the last column.
Java first, then jpype1 for speed
tabula-py runs tabula-java, so it needs Java. With java off the PATH, read_pdf stopped:
tabula.errors.JavaNotFoundError: `java` command is not found from this Python process.Please ensure Java is installed and PATH is set for `java`
Without jpype1, tabula-py logs this line and starts Java again for every call:
Failed to import jpype dependencies. Fallback to subprocess.
0.8 s a call without jpype1
0.061 s a call with jpype1
Both ways gave the same tables on 12 PDFs.
Scanned PDF: no tables at all
tabula reads the text in a PDF, and a scan has none. On my 4 scans it found no tables in any mode. Run OCR first:
ocrmypdf --jobs 1 -q scan.pdf ocr.pdf
| Route | Cells |
|---|---|
| tabula on the scan | 0 |
| OCRmyPDF, then tabula stream | 185 |
| OCRmyPDF, then tabula lattice | 0 |
| Our Actor's code | 254 |
OCR read the ruled lines as | marks, so a cell came back as "| Description". Lattice still found nothing: the lines of a scan are pixels, not drawn lines.
Our Actor reads the scan
Our Apify Actor runs OCR and returns each table as Markdown. On the same scans, its code missed 2 cells of 256.
It costs $0.006 a page read with OCR. Pages with their own text are free.
How I tested it
- PDFs: the 84 test PDFs of the pdfplumber table test, 7 table shapes and 4 data sets from Chromium, LibreOffice and reportlab. 5,376 true cells.
- Score: a cell is right when its text equals the true cell, with runs of spaces made one. Exact means every cell right and no extra column.
- Scans: the 4 ruled grids at 300 dpi, blurred, with gray noise, saved as JPEG. OCRmyPDF 17.13.0 with Tesseract 5.5.4.
- tabula-py 2.11.0 (tabula-java 1.0.5), pandas 3.1.1, jpype1 1.7.1, OpenJDK 21, Python 3.12. Apple Silicon Mac, lowest priority, one run each.
- Speed: 12 ruled grids with lattice, a fresh Python each way. The first call starts Java and is not in the median.
- The Actor: its code from build 0.1.65 (the live build), run on my Mac with its default settings, not a paid run. It read E-101 as E-10] and Corner as Comer.
What I did not test
- Real bank statements. Every value here is made up.
- Tables over 2 pages, and the area and columns options.
- Windows and Linux.
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.
Questions people ask
Why does tabula read_pdf return an empty list?
read_pdf guesses where the table sits. On 6 of my 84 PDFs the guess found nothing. Call it again with guess=False, but only then: on every page, it did worse.
Does tabula-py work on scanned PDFs?
No. A scan has no text, so tabula found no tables on my 4 scans. Run OCR first: after OCRmyPDF, tabula stream read 185 of 256 cells.
Does tabula-py need Java?
Yes. With java off the PATH, read_pdf stopped with JavaNotFoundError. Install jpype1 too: each call then took 0.061 s instead of 0.8 s.