ResumeJSON

Extract skills from a resume: the Python options, and the code

Extract skills from a resume: the options, and the code

To extract skills from a resume you have three honest choices, and they are not variations on one another. You can match against a skills taxonomy with a rule-based matcher such as SkillNER, you can run a general resume parser library that returns a skills list among its other fields, or you can call a parsing API that hands back a skills array as part of a typed document. This article is what each one actually returns, the code to run it, and the question that decides between them — whether you need the strings a CV contains, or codes from a controlled vocabulary.

We build ResumeJSON, a per-parse resume parsing API, so read this as written by an interested party. Every release date and price below was read from PyPI, GitHub or the vendor's own page as of September 2026, and every one of them is checkable in a minute.

The options, compared

OptionWhat it isWhat it gives youState as of September 2026
SkillNERRule-based matcher over a downloaded skills knowledge baseSkill spans found in free text, with taxonomy idsskillner 1.0.3 on PyPI, uploaded 7 November 2021; last push to GitHub 28 January 2024; 213 stars
spaCy PhraseMatcherYour own dictionary, matched over tokenised textExactly the terms in your listspaCy 3.8.16, 24 August 2026, Python >= 3.9, < 3.15 — actively maintained
pyresparserGeneral resume parser, skills among its fieldsA dict with a skills key, plus name, email, degree1.0.6, uploaded 15 December 2019, declares Python 3.3 – 3.7; 960 stars
Textkernel Skills IntelligenceEnterprise parser with skills normalization as an add-onNormalized and classified skills against a taxonomySold on quote or credits — Professional starts at $99/month
A parsing APIOne HTTP call, whole documentskills as an array, beside work, education and the restThis service is one of these

The row that decides it is the last column of your own requirements, not this table. If a recruiter is going to read the skills, the strings in the CV are enough. If a matching algorithm is going to compare two candidates, you need a vocabulary, and that is a different purchase.

Extracting skills is two problems, not one

Almost every "extract skills from resume" tutorial solves the first half and quietly leaves the second.

  1. Finding the candidate strings. A CV says Python, Django, PostgreSQL under a Skills heading, and also says "migrated a Postgres cluster" three paragraphs earlier. Both are evidence. A section-only reader misses the second; a whole-document reader picks up half the prose with it.
  2. Deciding what counts, and what it is called. Is Postgres the same skill as PostgreSQL? Is leadership a skill or a filler word? Is MS Office worth a row? This is the part a taxonomy answers and a regex does not, and it is the part that decides whether your candidate matching is trustworthy.

A dictionary approach solves (2) by fiat: whatever is in your list is a skill. That is a real answer, and for a narrow domain — twenty languages and fifteen frameworks — it is often the right one.

SkillNER: a taxonomy matcher

SkillNER is the package most people find first, and it is a genuine rule-based matcher rather than a wrapper around a model. It works against a skills knowledge base you download separately:

pip install skillner -U
skillner-download ESCO_EN

The package on PyPI is at 1.0.3, uploaded 7 November 2021, and its GitHub repository was last pushed on 28 January 2024 with 213 stars. It has no release in nearly five years, so treat the install as a fixed artefact rather than a maintained dependency: pin it, vendor the knowledge base, and expect to own the compatibility with whatever spaCy version you run beside it.

What you get back is spans with taxonomy ids, which is exactly the thing a hand-rolled dictionary cannot give you. If your goal is to compare two candidates on the same axis, that identifier is the whole point.

spaCy PhraseMatcher: your own dictionary

If your skill vocabulary is small and you know it, the matcher in spaCy is about fifteen lines and has no abandoned dependency in it. spaCy is at 3.8.16, uploaded 24 August 2026, declaring Python >= 3.9, < 3.15 — the only actively maintained library in this article's first three rows.

import spacy
from spacy.matcher import PhraseMatcher

nlp = spacy.load("en_core_web_sm")
SKILLS = ["python", "django", "postgresql", "kubernetes", "terraform"]

matcher = PhraseMatcher(nlp.vocab, attr="LOWER")
matcher.add("SKILL", [nlp.make_doc(s) for s in SKILLS])

doc = nlp(resume_text)
found = sorted({doc[start:end].text.lower() for _, start, end in matcher(doc)})
print(found)

That is genuinely production-viable for a narrow domain. What it will never do is surprise you with a skill you did not think to list — which is a feature when you are filtering for five things, and a defect when you are building a candidate profile.

pyresparser: skills as one field of a parse

pyresparser returns a dict with a skills key alongside name, email, degree and companies. It is the most-linked option in this space, and it is also the oldest: 1.0.6, uploaded 15 December 2019, declaring Python 3.3 – 3.7 in its own classifiers, with its GitHub last pushed 13 September 2023. It has 960 stars, so plenty of people shipped it — on Python versions that are themselves end of life.

Its skills list comes from a CSV shipped inside the package. That is worth knowing before you adopt it: you are not getting a maintained taxonomy, you are getting somebody's 2019 spreadsheet, and extending it means editing a file inside site-packages or forking. Our fuller write-up of that family is Python resume parser: the libraries that work, and the code.

The enterprise end: normalized skills

At the other end of the market, skills are sold as a normalization product rather than as an extraction one. Textkernel lists Skills Normalization as an add-on to its Parser, and describes Skills Intelligence as extracting skills from documents and returning them normalized and classified. Its own FAQ, on textkernel.com, says the output can be normalized against several standards:

Yes. With Textkernel's Parser you can normalize results according to various standards, including O*NET, ISCO, and Textkernel's real-world professions and skills taxonomies.

Their plans, read from the Textkernel pricing page in September 2026, are a free trial with 500 credits, a Professional plan "Starting at $99 /Month" with monthly plans from 500 – 25,000 credits, and an Enterprise plan by quote; a first purchase can take an Accelerator plan of 5,000 credits for $200. We cover the credit arithmetic in Textkernel pricing, explained.

Who should buy this rather than build it: if your product ranks or matches candidates, if you need skills mapped to O\*NET or ISCO codes for reporting, or if you operate across many languages, a normalized taxonomy is not a nice-to-have and no dictionary of yours will substitute for it. Stay with the enterprise vendors.

When a parse gives you the field for free

If what you actually need is "the skills this CV lists", and you also need the work history, the education and the dates — which is the usual case when you are building a job board or an ATS import — then skills extraction is not a separate project. It is one field of a parse.

Here is the whole job against ResumeJSON, which returns typed JSON in about two seconds:

import requests

resp = requests.post(
    "https://resumejson-resume-cv-parser-api.p.rapidapi.com/v1/parse",
    headers={
        "x-rapidapi-key": "YOUR_KEY",
        "x-rapidapi-host": "resumejson-resume-cv-parser-api.p.rapidapi.com",
        "content-type": "application/json",
    },
    json={"text": text},          # or send the file — base64 or multipart
    timeout=30,
)
resume = resp.json()["resume"]
print(resume["skills"])           # -> ["Python", "Django", "PostgreSQL", ...]

skills is an array of strings, beside basics, work, education, certifications, languages and total_years_experience. There are no model downloads, no spaCy pin and no knowledge base to vendor. Scanned and photographed CVs go down the same endpoint. Uploads are accepted to 20 MB and 60,000 characters of text; past either you get a 413 naming the limit rather than a silent truncation.

SkillNER or your own dictionaryResumeJSON
Setuppip, a knowledge base, a spaCy pinOne HTTP call
Skills you did not listFound, if the taxonomy has themRead from the document
Taxonomy idsYes — that is the pointNo — strings as written
The rest of the CVA separate problemSame response
Cost at 100 CVs/monthYour time$0 — the free rung is a hard cap
Cost at 1,000 CVs/monthYour time$29/month, or $0.05 a parse pay-per-use
Runs offlineYesNo

The honest gap is the third row. We return the skills a CV states, in the words it states them in — Postgres stays Postgres. We do not map them onto ESCO, O\*NET or ISCO codes. If your matching depends on two candidates who wrote it differently landing on the same identifier, a taxonomy product is what you want, and that is either SkillNER plus its knowledge base or one of the enterprise vendors above.

How to choose, in one pass

You can see the output shape before you decide anything: the free browser parser runs a real CV through the same endpoint with no signup, and the field reference is generated from the API's own schema, so it cannot drift from what you receive. If you are mapping the result onto a standard document shape, JSON Resume schema: a complete example has the field-by-field table; if you are still weighing build against buy, open source resume parser: what works, and when to buy instead is the broader version of this comparison.

Whichever route you take, decide it on the second problem rather than the first. Finding the strings is a weekend. Agreeing on what they are called is the part that is still there in a year.

All articles