You send 60 applications. You get 2 replies. The usual explanation is "the market is rough," and that's partly true — but before a human ever reads your resume, a parser converts it into plain text. If that conversion loses your job titles, your contact info, or half your skills section, you were never in the running.
The good news: the conversion is deterministic and you can run it yourself.
TL;DR — Open your resume with pdfplumber, print the extracted text, and read it as if you were the recruiter. If it looks wrong, it is wrong.
Step 1: See what the parser sees
pip install pdfplumber
import pdfplumber
with pdfplumber.open("resume.pdf") as pdf:
text = "\n".join(page.extract_text() or "" for page in pdf.pages)
print(text)
Three outcomes:
- Empty or nearly empty output. Your PDF has no text layer — it's an image. This happens when you export from Canva as an image-based PDF, scan a printout, or screenshot a page. The parser gets nothing. Most ATS platforms do not OCR.
-
Garbled output. Words like
certiedinstead ofcertified, orowinstead offlow. That's a ligature problem: the font encodesfiandflas single glyphs with no Unicode mapping. Common with fancy display fonts in InDesign or LaTeX exports. - Readable but scrambled order. This is the interesting one.
Step 2: The two-column trap
Two-column templates look great and extract badly. Text extraction follows the PDF's internal content stream, not your visual layout, so a line from the sidebar can end up glued to a line from the main body.
Check whether you have columns at all:
import pdfplumber
with pdfplumber.open("resume.pdf") as pdf:
page = pdf.pages[0]
words = page.extract_words()
mid = page.width / 2
left = sum(1 for w in words if w["x0"] < mid)
right = len(words) - left
print(f"left: {left} words | right: {right} words")
If both numbers are substantial, you have a real two-column layout. Now go back to your print(text) output and check whether any line mixes content from both sides. If it does, a parser looking for "job title followed by dates" will find "Senior Backend Engineer Python, Go, Kubernetes" and index nonsense.
The fix is boring and it works: single column, no sidebars, no text boxes, no tables for layout. Save the two-column version for the human-facing portfolio.
Step 3: Check what's actually in your headers and footers
Contact details in a PDF header or footer are frequently dropped or attached to the wrong page section. Quick check — is your email in the extracted text at all?
import re
emails = re.findall(r"[\w.+-]+@[\w-]+\.[\w.]+", text)
phones = re.findall(r"\+?\d[\d\s().-]{7,}\d", text)
print(emails, phones)
Empty list means a recruiter who liked your resume has no way to contact you. This is a more common failure than people assume, and it's invisible from the rendered page.
Step 4: Term overlap with the job description
Keyword matching gets oversold — no serious ATS scores you out of consideration on raw term frequency alone, and "keyword stuffing" is bad advice. But recruiters do run boolean searches over the parsed text, and if the posting says "Kubernetes" while your resume only says "K8s," you won't come up in that search.
Save the job posting as job.txt and diff the vocabulary:
import re
from collections import Counter
STOP = {
"and", "the", "for", "with", "you", "our", "are", "will", "have", "this",
"that", "from", "your", "who", "all", "can", "not", "work", "team",
"role", "years", "experience", "ability", "including", "strong",
}
def terms(s):
words = re.findall(r"[a-zA-Z][a-zA-Z0-9+#.\-]{1,}", s.lower())
return [w for w in words if w not in STOP and len(w) > 2]
jd = open("job.txt", encoding="utf-8").read()
resume_terms = set(terms(text))
missing = [(w, c) for w, c in Counter(terms(jd)).most_common(60)
if w not in resume_terms]
for word, count in missing[:20]:
print(f"{count:3d} {word}")
Read the output as a question, not a checklist: for each of these, have I actually done this and just called it something else? Rename what you've genuinely done. Ignore the rest. Adding "Kubernetes" to a resume when you've never touched it gets you a technical screen you will fail.
The checklist
- Text layer exists (
extract_text()returns real content) - No ligature corruption — search the output for
fi/flwords - Single column, or at minimum a reading order that isn't interleaved
- Email and phone present in the extracted text, not only in a header
- Job titles and dates on the same logical line as their employer
- No skills hidden inside tables, icons, or SVG graphics
- Filename is
firstname-lastname-resume.pdf, notresume_final_v3.pdf
Run this once per resume template, not once per application. Layout problems are template problems — fix the template and the fix carries across every application you send.
Doing this at scale
The script above is fine for one resume against one posting. It stops being fine around application number fifteen, when you're pasting job descriptions into a text file and diffing vocabulary by hand at midnight.
That repetition is what pushed me to build Futurole — a job search copilot that runs the parsing and matching pass automatically, then tracks what you sent where. Same mechanics as this post, minus the file management. If you'd rather keep it in a script, the code above is genuinely all you need.
Either way: run the extraction once before your next application. Reading your own resume as the machine reads it is a strange experience, and usually a productive one.
What broke in your extracted output? Drop it in the comments — the ligature one in particular catches people who did everything else right.
Top comments (0)