AISeptember 08, 20254 min read

Resume Parser using Python, spaCy, and Streamlit

Extract structured data from resumes with Python, spaCy, and Streamlit — names, emails, phones, and skills pulled from PDFs automatically.

Galvan

Galvan

Founder & Creator

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

  • PDF upload — text-based resumes parsed in seconds.
  • Contact extraction — email, phone, and LinkedIn via regex.
  • Name detection — spaCy's NER finds person names automatically.
  • Skill matching — a 40+ skill dictionary with section-aware matching.
  • Education & experience — degree keywords and year ranges.
  • JSON export — parsed data ready for your ATS or database.
  • Prerequisites

  • Python 3.9+ — from python.org.
  • Dependencies:
  • code
    pip install streamlit spacy pdfplumber
    python -m spacy download en_core_web_sm

    Step 1: Create the Script

    Save as resume_parser.py:

    code
    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

    code
    streamlit run resume_parser.py

    Test 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

  • `OSError: en_core_web_sm not found` — the model download step was skipped; run python -m spacy download en_core_web_sm.
  • Name comes out wrong (a company or city) — NER guesses from context; restrict the search to the first 300 characters (already done) or take the first line heuristically.
  • Empty text from a valid PDF — it's a scanned image; route it through the OCR app first.
  • Skills missing though present — case-sensitivity or tokenization ('node.js' splits); normalize both sides with the same regex tokenizer as the code does.
  • Key Concepts

  • Tool-per-pattern — regex for rules, NER for fuzz, dictionaries for vocabularies.
  • NER — statistical entity extraction without hand-written rules.
  • Set intersection — dictionary skills matching in one line.
  • Structured output — parsing is only useful when it lands in a system.
  • What to Try Next

  • Add section detection — find EXPERIENCE/EDUCATION headers and parse within bounds.
  • Add years-of-experience estimation from the date ranges you already capture.
  • Rank resumes against a job description by skill overlap percentage.
  • Batch mode for a folder of PDFs into one Excel via the report generator.
  • 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.