Text
Search text
page.search() finds a string or regular expression on a page and tells you exactly where each match is. That turns a lot of extraction into two steps: find the label, then read what is next to it.
Short answer
import pdfplumber
with pdfplumber.open("samples/invoice.pdf") as pdf:
page = pdf.pages[0]
for m in page.search("Total due"):
print(m["text"], "| x0", round(m["x0"], 1), "top", round(m["top"], 1), "| chars:", len(m["chars"]))Total due | x0 397.0 top 474.1 | chars: 8Each match is a dict with text, x0, top, x1, bottom, the regex groups and the matching chars. Search one page at a time; to search a document, loop over pdf.pages.
Regular expressions and groups
The pattern is a regular expression unless you pass regex=False. Groups come back in "groups", which makes label–value pairs a single pass:
import pdfplumber
with pdfplumber.open("samples/invoice.pdf") as pdf:
page = pdf.pages[0]
for m in page.search(r"(Invoice no\.|Issue date|Due date)\s+(\S+)"):
print(m["groups"], "at top", round(m["top"], 1))('Invoice no.', 'INV-2026-0417') at top 90.9
('Issue date', '2026-10-03') at top 104.9
('Due date', '2026-11-02') at top 118.9main_group=1 makes the match (and its box) cover only the first group instead of the whole pattern. Matches cannot span what pdfplumber considers separate lines unless you allow it in the pattern (\s+ matches the line break in the extracted text).
Case-insensitive search
import pdfplumber
with pdfplumber.open("samples/report.pdf") as pdf:
page = pdf.pages[0]
print(len(page.search("follow-up")), "match(es) with case=True")
print(len(page.search("follow-up", case=False)), "match(es) with case=False")1 match(es) with case=True
5 match(es) with case=FalseOn page 1, the report writes follow-up once, in its status line, and Follow-up four times: in the summary sentence and in each of the three flagged table rows.
Find the page a match is on
import pdfplumber
with pdfplumber.open("samples/report.pdf") as pdf:
for page in pdf.pages:
for m in page.search(r"Site \d+\s+Follow-up"):
print(f"page {page.page_number}, top {m['top']:5.1f}: {m['text']}")page 1, top 405.9: Site 09 Follow-up
page 1, top 439.9: Site 11 Follow-up
page 1, top 456.9: Site 12 Follow-up
page 2, top 141.9: Site 17 Follow-up
page 2, top 260.9: Site 24 Follow-up
page 2, top 277.9: Site 25 Follow-up
page 2, top 311.9: Site 27 Follow-up
page 2, top 498.9: Site 38 Follow-up
page 2, top 532.9: Site 40 Follow-up
page 2, top 549.9: Site 41 Follow-up
page 2, top 600.9: Site 44 Follow-uppage.page_number is 1-based, as in a viewer. For large documents, stop at the first page when one hit is all you need, and close pages as you go (performance).
Read the value next to a label
Invoices, statements and forms put values beside or below labels. Find the label, then crop the band to its right (or below it) and read that:
import pdfplumber
with pdfplumber.open("samples/invoice.pdf") as pdf:
page = pdf.pages[0]
label = page.search("Total due (USD)", regex=False)[0]
# Everything on the same line, right of the label. Start 1 pt after it:
# crop() keeps objects that merely touch the box edge.
band = page.crop((label["x1"] + 1, label["top"] - 2, page.width, label["bottom"] + 2))
print(band.extract_text())1,118.23For a value below a label, crop from the label’s bottom downwards, limited to roughly the label’s column. The invoice recipe builds a complete field extractor this way.
Highlight matches
Matches are boxes, so the visual debugger can draw them straight onto the page:
import pdfplumber
with pdfplumber.open("samples/report.pdf") as pdf:
page = pdf.pages[1]
hits = page.search(r"Follow-up")
im = page.to_image(resolution=100)
im.draw_rects(hits, stroke="red", fill=(255, 0, 0, 40))
im.save("report-follow-ups.png")
print(len(hits), "matches highlighted ->", "report-follow-ups.png")8 matches highlighted -> report-follow-ups.pngOptions
| Option | Default | Effect |
|---|---|---|
regex |
True |
False treats the pattern as a literal string |
case |
True |
False ignores case |
main_group |
0 |
Which regex group defines the match and its box |
return_chars |
True |
Include the matching char objects |
return_groups |
True |
Include the regex groups |
layout |
False |
Search the layout=True text; other text options (tolerances) also apply |
Search runs on the same text that extract_text() builds, so the tolerances that merge or split words affect what matches. Whitespace-only and zero-width matches are dropped because they have no position on the page.