Most advice on tailoring a resume starts with "read the job description carefully." That's true and nearly useless, because a posting is a messy blend of hard requirements, wishlist items, and boilerplate. Before you rewrite anything, it helps to know which lines actually matter.

Here's a small, honest approach: treat the posting as data. Split it into lines, classify each by how strongly it's worded, and compare against your own resume. It won't replace judgment, but it turns a vague feeling ("I think I'm a fit?") into a short list of concrete gaps.

Why wording matters more than position

Recruiters and hiring managers write postings in a loose hierarchy, even when they don't mean to:

  • Strong signals: "required", "must have", "you will", "X+ years of"
  • Soft signals: "nice to have", "a plus", "bonus", "familiarity with", "exposure to"
  • Noise: benefits, mission statements, "fast-paced environment"

A posting that lists fourteen technologies under "Requirements" rarely expects all fourteen. But a skill that appears in the title, in the first responsibilities bullet, and again in requirements is almost certainly the core of the job. Frequency and wording together are a better proxy than a flat keyword list.

The script

This uses only the standard library, so it runs anywhere.

import re, sys from collections import Counter STRONG = re.compile(r"\b(required|must|you will|\d+\+? years|proficien\w+|experience (with|in))\b", re.I) SOFT = re.compile(r"\b(nice to have|a plus|bonus|preferred|familiar\w*|exposure)\b", re.I) TOKEN = re.compile(r"[A-Za-z][A-Za-z+#.\-]{1,}") STOP = set("and the for with you our are will have your from that this they their able work team".split()) def terms(text): return {t.lower().strip(".-") for t in TOKEN.findall(text)} - STOP def triage(job_text, resume_text): resume_terms = terms(resume_text) freq = Counter(t for t in TOKEN.findall(job_text.lower()) if t not in STOP) buckets = {"must": [], "nice": [], "other": []} for line in filter(None, (l.strip(" -*•\t") for l in job_text.splitlines())): kind = "nice" if SOFT.search(line) else "must" if STRONG.search(line) else "other" missing = sorted(t for t in terms(line) - resume_terms if freq[t] >= 1 and len(t) > 2) buckets[kind].append((line, missing)) return buckets if __name__ == "__main__": job, resume = open(sys.argv[1]).read(), open(sys.argv[2]).read() for kind, rows in triage(job, resume).items(): print(f"\n== {kind.upper()} ==") for line, missing in rows: if kind != "other" and missing: print(f"- {line[:90]}\n not in resume: {', '.join(missing[:6])}") 

Run it with python triage.py job.txt resume.txt. Soft signals are checked first on purpose: "Familiarity with Kubernetes is a plus" contains words that could trip the strong pattern, but it's clearly optional.

What to do with the output

Look at the "must" bucket first, and read each "not in resume" list skeptically. There are three kinds of result:

  1. A real gap. You've never used the tool or done the thing. Don't fake it. Decide whether the gap is small enough to close in a weekend, or whether this role is a stretch you apply to anyway with an honest cover letter.
  2. A vocabulary mismatch. You did the work, but called it something else. The posting says "CI/CD pipelines" and your resume says "automated build and deploy scripts." This is the most valuable find. Reword using the posting's term, because both ATS filters and human skimmers match on the familiar phrase.
  3. A false positive. Words like "collaborative" or "stakeholders" show up as missing. Ignore them. The script is crude; you are the filter.

The goal is not to reach zero missing terms. Pasting every missing word into a skills section is keyword stuffing, and it tends to backfire in the interview, when someone asks you to explain the thing you listed.

Limits you should know about

  • It's lexical, not semantic. "Postgres" and "PostgreSQL" count as different terms. Add a small alias map if you use it often.
  • Line splitting is fragile. Postings copied from web pages sometimes collapse bullets into paragraphs. Pasting into a plain-text editor first helps.
  • Seniority and scope aren't visible to it. "Led a team of five" versus "contributed to" is invisible to a word match, and it's often what decides the screen.

If you'd rather not maintain a script, tools that compare a posting against a CV do the same job with better matching. NextCV's job match page is one example, but the process matters more than the tool: find the real requirements, find the real gaps, and describe your actual experience in the posting's language.

A workflow that holds up

  1. Paste the posting into a file and run the triage.
  2. Mark each missing "must" term as real gap, vocabulary mismatch, or noise.
  3. For vocabulary mismatches, edit the existing bullet; don't add new claims.
  4. For real gaps, decide whether to apply anyway, and address them directly rather than hiding them.
  5. Re-read the final resume against the original posting, not the script's output.

Ten minutes of this beats an hour of guessing, and it keeps the resume truthful, which is the part that survives a technical interview.

What signals do you use to separate a hard requirement from a wishlist item? I'd like to hear patterns I've missed.