Spaces:
Sleeping
Sleeping
File size: 7,067 Bytes
c8f4a46 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | """
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
|