Introduction
HR teams read hundreds of resumes; recruiters spend seconds each. A resume parser turns that chore into an API: upload a PDF, get back the candidate's name, contact details, education, and skills as clean structured data. This app combines three tools — pdfplumber for text extraction, spaCy for language understanding, and regex for the patterns machines write (emails, phones) — and it is the natural next step after the OCR app, which gets text out of *images* the same way this gets it out of PDFs.
Features
Prerequisites
pip install streamlit spacy pdfplumber
python -m spacy download en_core_web_smStep 1: Create the Script
Save as resume_parser.py:
import streamlit as st
import re
import json
import pdfplumber
import spacy
st.set_page_config(page_title="Resume Parser", page_icon="📄")
st.title("📄 Resume Parser")
nlp = spacy.load("en_core_web_sm")
SKILLS = {"python", "java", "javascript", "react", "sql", "html", "css", "node.js",
"streamlit", "pandas", "numpy", "tensorflow", "pytorch", "aws", "docker",
"git", "excel", "figma", "photoshop", "seo", "communication", "leadership"}
uploaded = st.file_uploader("Upload a resume PDF", type=["pdf"])
if uploaded:
with pdfplumber.open(uploaded) as pdf:
text = "\n".join(page.extract_text() or "" for page in pdf.pages)
with st.expander("Raw extracted text"):
st.text(text[:2000])
doc = nlp(text[:3000])
emails = re.findall(r"[\w.+-]+@[\w-]+\.[\w.]+", text)
phones = re.findall(r"(?:\+\d{1,3}[ -]?)?\d{10}|\d{3}[-.]\d{3}[-.]\d{4}", text)
linkedin = re.findall(r"linkedin\.com/[\w/]+", text.lower())
names = [e.text for e in doc.ents if e.label_ == "PERSON"]
degrees = re.findall(r"(B\.?Tech|B\.?E\.?|B\.?Sc|M\.?Tech|M\.?Sc|MBA|Ph\.?D)[\w .]*", text, re.I)
years = re.findall(r"(19|20)\d{2}\s*[-–]\s*((19|20)\d{2}|Present)", text)
words = set(re.findall(r"[a-zA-Z+#.]+", text.lower()))
skills = sorted(SKILLS & words)
result = {
"name": names[0] if names else None,
"email": emails[0] if emails else None,
"phone": phones[0] if phones else None,
"linkedin": linkedin[0] if linkedin else None,
"education": list(dict.fromkeys(d.strip() for d in degrees))[:3],
"experience_ranges": len(years),
"skills": skills,
}
c1, c2 = st.columns(2)
with c1:
st.subheader("👤 Contact")
st.markdown(f"**Name:** {result['name'] or '—'}")
st.markdown(f"**Email:** {result['email'] or '—'}")
st.markdown(f"**Phone:** {result['phone'] or '—'}")
st.markdown(f"**LinkedIn:** {result['linkedin'] or '—'}")
with c2:
st.subheader("🎓 Education & experience")
for d in result["education"]:
st.markdown(f"- {d}")
st.caption(f"{result['experience_ranges']} date range(s) found")
st.subheader("🛠 Skills")
if skills:
st.markdown(" · ".join(f"**{s}**" for s in skills))
else:
st.caption("No known skills matched — extend the dictionary.")
st.download_button("⬇️ Download JSON", json.dumps(result, indent=2), "parsed.json", "application/json")Step 2: Run the App
streamlit run resume_parser.pyTest with any text-based PDF resume — scanned-image resumes return empty text (that's the OCR app's job).
How It Works
Parsing is a toolbox problem, and each tool owns a different pattern. Regex owns *rigid formats* — emails and phone numbers follow rules, so patterns match them near-perfectly. spaCy's named entity recognition owns *fuzzy* patterns: a person's name is whatever its statistical model flags as PERSON in the first chunk of text. Dictionary intersection owns *skills*: a curated set intersected with the document's word set — simple, explainable, and easy to extend.
pdfplumber extracts text with layout awareness, which matters because resumes are visual documents — two-column layouts can interleave lines, which is exactly why the name lookup takes the *first* PERSON entity and why the raw-text expander exists for debugging weird extractions.
The JSON export closes the loop: parsing only pays off when the output plugs into something — an applicant tracking system, a spreadsheet, or the Excel report generator.
Common Errors & Fixes
python -m spacy download en_core_web_sm.Key Concepts
What to Try Next
FAQ
Why spaCy instead of an LLM?
spaCy runs locally in milliseconds at zero cost per resume, with deterministic output. An LLM parses messier documents better but costs per call and can hallucinate — for contact fields and skills, the toolbox approach wins on reliability.
How accurate is name extraction?
On clean text, very good — 90%+. Decorative formatting, ALL-CAPS names, or names the model has never seen can trip it; that's why the raw text view is one click away for verification.
Can it handle .docx files?
Yes with python-docx — extract paragraphs, join with newlines, and the rest of the pipeline is unchanged.