Malware-detection / app /services /resume_parser.py
kumar-aditya's picture
Upload 60 files
c8f4a46 verified
Raw
History Blame Contribute Delete
7.07 kB
"""
Resume parser: regex-based section detection + structured field extraction.
Returns a ParsedResume dict with keys:
contact, summary, skills, experience, projects, education, certifications, raw_sections
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
# ── Section header patterns (order matters β€” more specific first) ──────────
_SECTION_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
("certifications", re.compile(r"^certif|^licen|^credential", re.I | re.M)),
("projects", re.compile(r"^project|^portfolio|^open.?source", re.I | re.M)),
("education", re.compile(r"^educ|^academic|^qualif|^degree", re.I | re.M)),
("experience", re.compile(r"^(work\s+)?experience|^employment|^career|^professional\s+background", re.I | re.M)),
("skills", re.compile(r"^(technical\s+)?skills?|^competenc|^technologies|^tech\s+stack|^tools", re.I | re.M)),
("summary", re.compile(r"^summary|^objective|^profile|^about|^overview|^professional\s+summary", re.I | re.M)),
("contact", re.compile(r"^contact|^personal\s+info|^details", re.I | re.M)),
]
# Fallback: lines that look like section headers (ALL CAPS or Title-case short lines)
_HEADER_LINE = re.compile(r"^([A-Z][A-Z\s&/\-]{2,30})$")
# Common action verbs for quality scoring
ACTION_VERBS = {
"developed", "built", "designed", "implemented", "created", "deployed",
"optimised", "optimized", "reduced", "increased", "improved", "led",
"managed", "architected", "engineered", "automated", "integrated",
"delivered", "launched", "scaled", "migrated", "refactored", "shipped",
"established", "coordinated", "analysed", "analyzed",
}
# Quantification signals
_QUANT_RE = re.compile(r"\d+\s*(%|x|k|ms|s|mb|gb|tb|users?|requests?|hours?|days?|weeks?|months?|years?)", re.I)
@dataclass
class ParsedResume:
contact: str = ""
summary: str = ""
skills_raw: str = ""
experience_raw: str = ""
projects_raw: str = ""
education_raw: str = ""
certifications_raw: str = ""
skills_list: list[str] = field(default_factory=list)
experience_bullets: list[str] = field(default_factory=list)
raw_sections: dict[str, str] = field(default_factory=dict)
full_text: str = ""
# Quality signals
action_verb_count: int = 0
quantified_bullet_count: int = 0
def to_dict(self) -> dict:
return {
"contact": self.contact,
"summary": self.summary,
"skills": self.skills_list,
"experience_raw": self.experience_raw,
"experience_bullets": self.experience_bullets,
"projects_raw": self.projects_raw,
"education_raw": self.education_raw,
"certifications_raw": self.certifications_raw,
"raw_sections": self.raw_sections,
"quality": {
"action_verb_count": self.action_verb_count,
"quantified_bullet_count": self.quantified_bullet_count,
},
}
def parse_resume(text: str) -> ParsedResume:
"""Parse resume text into structured sections."""
pr = ParsedResume(full_text=text)
lines = text.splitlines()
sections = _split_into_sections(lines)
pr.raw_sections = sections
pr.contact = sections.get("contact", _extract_contact_block(lines))
pr.summary = sections.get("summary", "")
pr.skills_raw = sections.get("skills", "")
pr.experience_raw = sections.get("experience", "")
pr.projects_raw = sections.get("projects", "")
pr.education_raw = sections.get("education", "")
pr.certifications_raw = sections.get("certifications", "")
pr.skills_list = _parse_skills(pr.skills_raw)
pr.experience_bullets = _extract_bullets(pr.experience_raw + "\n" + pr.projects_raw)
# Quality signals
all_bullets = "\n".join(pr.experience_bullets)
pr.action_verb_count = sum(
1 for b in pr.experience_bullets
if any(b.strip().lower().startswith(v) for v in ACTION_VERBS)
)
pr.quantified_bullet_count = len(_QUANT_RE.findall(all_bullets))
return pr
# ── Private helpers ────────────────────────────────────────────────────────
def _split_into_sections(lines: list[str]) -> dict[str, str]:
"""
Walk lines and assign them to labelled buckets based on header detection.
"""
sections: dict[str, list[str]] = {}
current: str | None = None
for raw_line in lines:
line = raw_line.strip()
if not line:
if current:
sections.setdefault(current, []).append("")
continue
label = _detect_section_label(line)
if label:
current = label
sections.setdefault(current, [])
elif current is not None:
sections[current].append(line)
# Lines before any detected section β†’ treat as contact/top block
else:
sections.setdefault("contact", []).append(line)
return {k: "\n".join(v).strip() for k, v in sections.items()}
def _detect_section_label(line: str) -> str | None:
"""Return a canonical section name if this line looks like a section header."""
clean = line.strip().rstrip(":").strip()
for name, pat in _SECTION_PATTERNS:
if pat.match(clean):
return name
# Fallback: short ALL-CAPS or Title-Case line with no punctuation
if _HEADER_LINE.match(clean) and len(clean.split()) <= 4:
return None # Don't auto-classify unknown headers
return None
def _extract_contact_block(lines: list[str]) -> str:
"""Take the first non-empty lines as the contact/header block."""
out: list[str] = []
for line in lines[:15]:
stripped = line.strip()
if stripped:
out.append(stripped)
elif out:
break
return "\n".join(out)
def _parse_skills(skills_text: str) -> list[str]:
"""Split skills section into individual skill tokens."""
if not skills_text:
return []
# Split on common delimiters: comma, bullet, pipe, newline, semicolon
raw = re.split(r"[,|\nβ€’Β·\-–;/]+", skills_text)
skills: list[str] = []
for item in raw:
item = item.strip()
# Skip very long items (probably a sentence, not a skill)
if item and len(item) <= 50 and len(item) >= 1:
skills.append(item)
return skills
def _extract_bullets(text: str) -> list[str]:
"""Extract bullet-point lines from experience/projects sections."""
bullets: list[str] = []
for line in text.splitlines():
stripped = line.strip()
# Lines starting with bullet char, dash, or asterisk
if stripped and (stripped[0] in "-β€’Β·*β–Έβ–ͺ" or re.match(r"^\d+\.", stripped)):
bullet = re.sub(r"^[-β€’Β·*β–Έβ–ͺ\d\.]+\s*", "", stripped).strip()
if len(bullet) > 20:
bullets.append(bullet)
return bullets