DEV Community

Cover image for Most resume advice is vibes. This one is mechanical : extract the text layer the parser sees, and fix what's missing.
ADIKPETO Zinédine
ADIKPETO Zinédine

Posted on

Most resume advice is vibes. This one is mechanical : extract the text layer the parser sees, and fix what's missing.

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
Enter fullscreen mode Exit fullscreen mode
import pdfplumber

with pdfplumber.open("resume.pdf") as pdf:
    text = "\n".join(page.extract_text() or "" for page in pdf.pages)

print(text)
Enter fullscreen mode Exit fullscreen mode

Three outcomes:

  1. 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.
  2. Garbled output. Words like certied instead of certified, or ow instead of flow. That's a ligature problem: the font encodes fi and fl as single glyphs with no Unicode mapping. Common with fancy display fonts in InDesign or LaTeX exports.
  3. 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")
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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/fl words
  • 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, not resume_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)