Skip to content

PDF scraping: extract tables and text from PDFs with Python

We ran pypdf, pdfminer.six, pdfplumber and PyMuPDF on a real IRS tax table. Speeds ran from 0.45 to 28 seconds, and both table finders split the rows.

HProxy TeamOctober 12, 2026Updated October 12, 20265 min read

PDF scraping: extract tables and text from PDFs with Python

A PDF does not store tables or paragraphs. It stores characters at positions on a page, so scraping one means rebuilding the structure from those positions.

We tested four Python libraries on a real table on 11 October 2026: IRS Publication 1040 for 2025, 28 pages of tax tables. One library was 60 times faster than another, both table finders split the rows, and one method rebuilt every row.

The test file

The IRS publishes the tax tables as a PDF. Works of the US government carry no copyright in the US: "Copyright protection under this title is not available for any work of the United States Government". The IRS robots.txt sets no rule for the /pub/ folder the file sits in.

We picked one row and read it by eye on the rendered page. At least $50,000 but less than $50,050, the tax is 5,920, 5,526, 5,920 and 5,663 for the four filing statuses:

Part of page 8 of IRS Publication 1040: the rows from 49,700 to 50,250, with the row 50,000 to 50,050 showing 5,920, 5,526, 5,920 and 5,663
IRS Publication 1040 (2025), page 8, rendered by us with PyMuPDF; a work of the US government

Every result below is checked against that row.

Text: which library keeps the row

We read the whole file with each library, three times, on our server:

Terminal output: four libraries on the 28-page IRS table, with time, characters and whether the test row came out in order; the table finders' result; zero characters from a scanned page; and two ways to rebuild the rows
Our own capture, 11 October 2026: pdf_test.py and extract_rows.py on our server
Seconds to read the text of a 28-page IRS PDF (median of 3 runs)
PyMuPDF 1.28.2
0.45 s
pypdf 6.20.0
10.25 s
pdfplumber 0.11.10
23.56 s
pdfminer.six 20260107
28.2 s
Source: HProxy test on our server, 11 October 2026: Python 3.12.3, IRS Publication 1040 (2025) · hproxy.comProxy.

PyMuPDF took 0.45 seconds. pypdf took 10.25, pdfplumber 23.56 and pdfminer.six 28.20. Three of the four kept the test row in order.

pdfminer.six did not. Its output mixed the first two columns, so the row never came out in order. Its documentation explains the approach: its layout analysis "groups characters into words and lines, then it groups lines into boxes". On this page, those boxes did not follow the rows.

Tables: why the table finders failed

pdfplumber and PyMuPDF both have a table finder, and both found three tables on page 8. Neither gave the row as six cells. Both put 50,000, 50,050 and 5,920 into one cell, together with the 19 rows below them. Only the two shaded columns came out one value per row.

The reason is in the settings. By default, pdfplumber draws cell borders from "the page's graphical lines", and the sides of rectangles count as lines. The IRS table shades two columns with grey boxes, so the borders followed the shading, not the rows. PyMuPDF's finder takes the same settings and gave the same split.

Switching both to the text strategy, which places borders between words, kept every value. But it put all three blocks of the page in one wide row and split 50,000 into "5" and "0,000".

What worked: words by their position

What worked was to skip the table finder. PyMuPDF can return every word with its coordinates. Group the numbers by the line they sit on, read them left to right, and cut each line into blocks of six:

import re
from collections import defaultdict

import pymupdf

NUMBER = re.compile(r"\d{1,3}(?:,\d{3})*")


def rows_by_position(path):
    rows = []
    with pymupdf.open(path) as doc:
        for page in doc:
            text = page.get_text()
            if "Tax Table" not in text or "EIC" in text:
                continue  # tax-table pages only
            lines = defaultdict(list)
            for x0, y0, x1, y1, word, *_ in page.get_text("words"):
                if NUMBER.fullmatch(word):
                    lines[round(y1)].append((x0, word))
            for _, words in sorted(lines.items()):
                values = [w for _, w in sorted(words)]  # left to right
                if len(values) >= 6 and len(values) % 6 == 0:
                    rows += [tuple(values[i:i + 6]) for i in range(0, len(values), 6)]
    return rows


rows = rows_by_position("p1040.pdf")
len(rows)
# 2066
("50,000", "50,050", "5,920", "5,526", "5,920", "5,663") in rows
# True

That took 0.61 seconds for the whole file. It found 2,066 rows: 2,062 distinct ones, plus the 4 rows of a sample table on page 2. Every row started where the one before it ended, from $0 to $100,000.

Then check the result. We tested each row against the one before it. The same file, read with one pattern of six numbers over the plain text, gave 3,296 broken rows and missed the test row.

Scanned PDFs need OCR

A scanned page is a picture of text, not text. We saved page 8 as an image inside a PDF, the way a scanner does. All four libraries returned 0 characters:

with pymupdf.open("scanned-page8.pdf") as doc:
    doc[0].get_text()
# ''

So check each page for text first. Pages that come back empty need an OCR engine, and the rest do not. We had no OCR engine on our server, so this page does not test one.

Downloading PDFs from a website

Fetching a PDF is an ordinary download. Before you script hundreds of them, read the site's robots.txt and terms, as with any scraping. Our page on whether web scraping is legal covers what to look for.

A proxy matters only at that step, when a site limits how many files one address may fetch. Our guide to proxies for web scraping shows the setup.

Check the licence

All four libraries are free to download, but not under the same terms:

LibraryVersion testedLicence on PyPI
pypdf6.20.0BSD-3-Clause
pdfminer.six20260107MIT
pdfplumber0.11.10MIT
PyMuPDF1.28.2GNU AGPL 3.0 or Artifex commercial

If you build a product on PyMuPDF, read the AGPL or Artifex's commercial terms first, or use one of the other three.

What this page could not check

  • One PDF on one server, three runs per library. Other tables may suit the table finders better.
  • The position method fits this table's layout. Other documents need their own grouping.
  • We did not test OCR, camelot or tabula.
  • We will rerun these tests on the next edition of the tables by 11 January 2027.

Sources

  • IRS, Publication 1040 (2025), Tax and Earned Income Credit Tables: irs.gov.
  • 17 U.S.C. 105, works of the United States Government: law.cornell.edu.
  • pdfminer.six, converting a PDF file to text: pdfminersix.readthedocs.io.
  • pdfplumber, README (table extraction settings): github.com.
  • PyMuPDF, Page: pymupdf.readthedocs.io.
  • PyPI package pages for pypdf, pdfplumber, pdfminer.six and PyMuPDF, read 11 October 2026: pypi.org.
  • Our own tests on our server, 11 October 2026: pdf_test.py at 21:57 UTC, extract_rows.py at 21:57 UTC, table_strategies.py at 22:05 UTC.

Frequently asked questions

What is the best Python library to scrape PDFs?

For speed and text in reading order, PyMuPDF: it read a 28-page IRS table in 0.45 seconds, where pypdf took 10.25 and pdfplumber 23.56. Check the licence first: PyMuPDF is AGPL 3.0 or commercial. pypdf is BSD, and pdfplumber and pdfminer.six are MIT.

How do I extract a table from a PDF in Python?

Try a table finder first, such as pdfplumber's extract_tables or PyMuPDF's find_tables, then check a row you know. On our IRS table both split the rows wrongly. Reading words with their positions and grouping them by line rebuilt all 2,062 rows.

Can Python read a scanned PDF?

Not without OCR. A scanned page is a picture, and all four libraries we tested returned 0 characters from one. Check each page for text first, and send only the empty pages to OCR.

How do I scrape PDF files from a website?

Download them like any other file, after reading the site's robots.txt and terms. The IRS robots.txt sets no rule for its /pub/ folder, and works of the US government have no US copyright.

Are free PDF scrapers really free to use?

The four libraries we tested cost nothing, but their licences differ. PyMuPDF is dual licensed: GNU AGPL 3.0 or a commercial licence from Artifex. pypdf is BSD-3-Clause, and pdfplumber and pdfminer.six are MIT.

Get proxies that are alive right now

Our free proxy list re-checks every exit every few minutes across 100+ countries, with a live last-checked time, so you copy IPs that worked moments ago, not a stale text dump. When the location has to survive a real check, the paid network holds up.

129M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup

HProxy.

Honest guides and comparisons on proxies, scraping and staying unblocked, from the team that runs the network.

RSS feed