Files
Job-hunter/resume_parser.py
2026-05-14 09:46:14 -04:00

246 lines
8.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
resume_parser.py
----------------
Parses a resume (text or PDF) into a structured JSON profile
used by the matching engine. Run once, outputs profile.json.
"""
import json
import re
import sys
import os
import logging
logger = logging.getLogger(__name__)
# ---- Known skill taxonomy ----
SKILL_TAXONOMY = {
"bgp": ["bgp", "border gateway protocol", "bgp peering", "route maps"],
"ospf": ["ospf", "open shortest path first"],
"eigrp": ["eigrp"],
"mpls": ["mpls"],
"vxlan": ["vxlan"],
"vrrp": ["vrrp"],
"vlans": ["vlan", "vlans"],
"stp": ["stp", "spanning tree"],
"tcp/ip": ["tcp/ip", "tcp", "ip", "tcpip"],
"dns": ["dns", "domain name"],
"dhcp": ["dhcp"],
"qos": ["qos", "quality of service"],
"ipv6": ["ipv6"],
"multicast": ["multicast", "pim", "igmp"],
"nat": ["nat", "network address translation"],
"acls": ["acl", "acls", "access control list"],
"load balancing": ["load balanc"],
"cisco": ["cisco", "catalyst", "nexus"],
"arista": ["arista"],
"juniper": ["juniper", "junos", "srx"],
"mikrotik": ["mikrotik"],
"brocade": ["brocade"],
"fortinet": ["fortinet", "fortigate"],
"pfsense": ["pfsense"],
"barracuda": ["barracuda"],
"vyos": ["vyos"],
"firewall": ["firewall", "ngfw"],
"vpn": ["vpn", "ipsec", "site-to-site"],
"802.1x": ["802.1x", "nac", "network access control"],
"2fa": ["2fa", "mfa", "two-factor", "multi-factor"],
"network segmentation": ["network segmentation", "microsegmentation"],
"aws": ["aws", "amazon web services", "vpc"],
"azure": ["azure"],
"gcp": ["gcp", "google cloud"],
"ibm cloud": ["ibm cloud"],
"vmware": ["vmware", "esxi", "vsphere"],
"proxmox": ["proxmox"],
"lxc": ["lxc", "linux container"],
"docker": ["docker", "container"],
"kubernetes": ["kubernetes", "k8s"],
"terraform": ["terraform"],
"hybrid cloud": ["hybrid cloud"],
"python": ["python"],
"bash": ["bash", "shell script"],
"ansible": ["ansible"],
"git": ["git", "github", "gitlab"],
"iac": ["infrastructure as code", "infrastructure-as-code", "iac"],
"ci/cd": ["ci/cd", "cicd", "jenkins", "pipeline"],
"zabbix": ["zabbix"],
"librenms": ["librenms"],
"nagios": ["nagios"],
"snmp": ["snmp"],
"syslog": ["syslog"],
"datadog": ["datadog"],
"grafana": ["grafana"],
"prometheus": ["prometheus"],
"splunk": ["splunk"],
"solarwinds": ["solarwinds"],
"active directory": ["active directory", "ad"],
"linux": ["linux", "unix", "rhel", "ubuntu", "centos", "debian"],
"windows server": ["windows server", "windows ad"],
"voip": ["voip", "sip"],
"sql": ["sql", "mysql", "postgres", "mssql"],
"certificate management": ["certificate", "ssl", "tls", "pki"],
}
CERT_PATTERNS = [
(r"ccnp", "CCNP"),
(r"ccna", "CCNA"),
(r"ccie", "CCIE"),
(r"aws\s+certified\s+cloud\s+practitioner", "AWS Cloud Practitioner"),
(r"aws\s+certified\s+solutions?\s+architect", "AWS Solutions Architect"),
(r"aws\s+certified", "AWS Certified"),
(r"comptia\s+security\+", "CompTIA Security+"),
(r"comptia\s+network\+", "CompTIA Network+"),
(r"jncia", "JNCIA"),
(r"jncis", "JNCIS"),
(r"fortinet\s+nse", "Fortinet NSE"),
(r"palo\s+alto\s+pcnse", "PCNSE"),
]
TITLE_VARIANTS = {
"network engineer": ["network engineer", "network eng", "noc engineer"],
"infrastructure engineer": ["infrastructure engineer", "infra engineer"],
"network administrator": ["network administrator", "network admin"],
"system administrator": ["system administrator", "sysadmin", "systems administrator"],
"cloud engineer": ["cloud engineer", "cloud network engineer"],
"devops engineer": ["devops", "site reliability", "sre"],
"security engineer": ["security engineer", "network security"],
}
def extract_skills(text):
text_lower = text.lower()
found = {}
for skill_name, patterns in SKILL_TAXONOMY.items():
for pattern in patterns:
if pattern.lower() in text_lower:
count = text_lower.count(pattern.lower())
found[skill_name] = min(count, 5)
break
return found
def extract_titles(text):
titles = []
title_patterns = [
r"(?:^|\n)\s*([\w\s&/]+(?:Engineer|Administrator|Analyst|Manager|Architect|Specialist|Technician|Director))\s*(?:\(|–|-|\d|$)",
]
for pattern in title_patterns:
matches = re.findall(pattern, text, re.IGNORECASE | re.MULTILINE)
for m in matches:
clean = m.strip()
if 5 < len(clean) < 60:
titles.append(clean)
return list(set(titles))
def extract_experience_years(text):
explicit = re.search(r"(\d+)\+?\s*years?\s*(?:of\s+)?experience", text, re.IGNORECASE)
if explicit:
return int(explicit.group(1))
date_ranges = re.findall(r"(\d{2}/\d{4})\s*[–-]\s*(\d{2}/\d{4}|present|current)", text, re.IGNORECASE)
if date_ranges:
from datetime import datetime
total_months = 0
for start_str, end_str in date_ranges:
try:
start = datetime.strptime(start_str, "%m/%Y")
if end_str.lower() in ("present", "current"):
end = datetime.now()
else:
end = datetime.strptime(end_str, "%m/%Y")
months = (end.year - start.year) * 12 + (end.month - start.month)
total_months += max(months, 0)
except ValueError:
continue
if total_months > 0:
return total_months // 12
return 0
def extract_certifications(text):
text_lower = text.lower()
found = []
for pattern, cert_name in CERT_PATTERNS:
if re.search(pattern, text_lower):
found.append(cert_name)
return list(set(found))
def extract_education(text):
education = []
edu_patterns = [
r"(bachelor|master|diploma|degree|b\.?s\.?|m\.?s\.?|ph\.?d|associate)[\w\s,]*(?:in\s+)?([\w\s&]+?)(?:\d{4}|\n|$)",
]
for pattern in edu_patterns:
matches = re.findall(pattern, text, re.IGNORECASE)
for level, field in matches:
education.append({"level": level.strip(), "field": field.strip()[:50]})
return education
def parse_resume_text(text):
skills = extract_skills(text)
titles = extract_titles(text)
years = extract_experience_years(text)
certs = extract_certifications(text)
education = extract_education(text)
normalized_titles = set()
for title in titles:
title_lower = title.lower()
for norm, variants in TITLE_VARIANTS.items():
if any(v in title_lower for v in variants):
normalized_titles.add(norm)
break
else:
normalized_titles.add(title_lower)
return {
"skills": skills,
"titles": list(normalized_titles),
"raw_titles": titles,
"years_experience": years,
"certifications": certs,
"education": education,
"full_text": text,
}
def parse_resume_file(filepath):
ext = os.path.splitext(filepath)[1].lower()
if ext == ".txt":
with open(filepath, "r", encoding="utf-8") as f:
text = f.read()
elif ext == ".pdf":
try:
import pdfplumber
with pdfplumber.open(filepath) as pdf:
text = "\n".join(page.extract_text() or "" for page in pdf.pages)
except ImportError:
logger.error("Install pdfplumber: pip install pdfplumber")
sys.exit(1)
else:
raise ValueError(f"Unsupported file type: {ext}")
return parse_resume_text(text)
def save_profile(profile, output_path="profile.json"):
save_data = {k: v for k, v in profile.items() if k != "full_text"}
with open(output_path, "w") as f:
json.dump(save_data, f, indent=2)
logger.info(f"Profile saved to {output_path}")
return save_data
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
if len(sys.argv) < 2:
print("Usage: python resume_parser.py <resume.txt|resume.pdf>")
sys.exit(1)
filepath = sys.argv[1]
profile = parse_resume_file(filepath)
saved = save_profile(profile)
print(json.dumps(saved, indent=2))