NaikPranav11's picture
Initial clean deployment
1207440
Raw
History Blame Contribute Delete
11.1 kB
import os
import re
import pdfplumber
import docx2txt
import spacy
from typing import List, Dict, Optional
import pprint
# Load NLP model
try:
nlp = spacy.load("en_core_web_sm")
except OSError:
print("SpaCy model 'en_core_web_sm' not found.")
print("Please run: python -m spacy download en_core_web_sm")
exit()
# Text Extraction
def extract_text_from_pdf(file_path: str) -> str:
"""Extract text from PDF with proper page separation"""
text = ""
try:
with pdfplumber.open(file_path) as pdf:
for page in pdf.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
except Exception as e:
print(f"Error reading PDF file {file_path}: {e}")
return text
def extract_text_from_docx(file_path: str) -> str:
"""Extract text from DOCX while preserving structure"""
try:
return docx2txt.process(file_path)
except Exception as e:
print(f"Error reading DOCX file {file_path}: {e}")
return ""
def clean_text(text: str) -> str:
"""Normalize whitespace and clean artifacts"""
text = re.sub(r'\n+', '\n', text) # Collapse multiple newlines
text = re.sub(r'[^\S\n]+', ' ', text) # Collapse multiple spaces/tabs
return text.strip()
def validate_email(email: str) -> bool:
"""Validate email format strictly"""
if not email:
return False
# Check for invalid characters
if any(char in email for char in ['|', '\n', '\t', ' ', ',', ';']):
return False
# Standard email regex
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
def validate_phone(phone: str) -> bool:
"""Validate phone number format"""
if not phone:
return False
# Remove common formatting characters
cleaned = re.sub(r'[\s\-\(\)]+', '', phone)
# Check if it's a reasonable phone number (7-15 digits)
return bool(re.match(r'^\+?\d{7,15}$', cleaned))
# Entity Extraction
def extract_entities(text: str) -> Dict[str, Optional[str]]:
"""
Extract name, email, and phone with robust validation
Returns: {'name': str, 'email': str, 'phone': str}
"""
# Extract name from first 3 lines (where it usually appears)
first_lines = '\n'.join(text.split('\n')[:3])
doc = nlp(first_lines)
name = ""
# Try to find PERSON entity
for ent in doc.ents:
if ent.label_ == "PERSON":
# Clean the name - take only the first line and remove extra whitespace
potential_name = ent.text.split('\n')[0].strip()
potential_name = re.sub(r'\s+', ' ', potential_name)
# Validate name (should not contain numbers or special chars except spaces, hyphens, apostrophes)
if re.match(r"^[A-Za-z\s\-'\.]+$", potential_name) and len(potential_name) > 2:
name = potential_name
break
# Fallback: Use first line if no PERSON entity found
if not name:
first_line = text.split('\n')[0].strip()
# Clean and validate
first_line = re.sub(r'\s+', ' ', first_line)
if re.match(r"^[A-Za-z\s\-'\.]+$", first_line) and len(first_line) > 2:
name = first_line
# Extract email with strict validation
email = None
# Find all potential emails
email_pattern = r'\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b'
email_matches = re.findall(email_pattern, text)
# Validate and pick the first valid one
for match in email_matches:
# Clean the match (remove any trailing/leading dots or special chars)
cleaned_email = match.strip().lower()
if validate_email(cleaned_email):
email = cleaned_email
break
# Extract phone with validation
phone = None
# Multiple phone patterns
phone_patterns = [
r'\+?\d{1,3}[\s\-]?\(?\d{3}\)?[\s\-]?\d{3}[\s\-]?\d{4}', # US/International
r'\+?\d{10,15}', # Simple international
r'\(?\d{3}\)?[\s\-]?\d{3}[\s\-]?\d{4}', # US format
]
for pattern in phone_patterns:
phone_match = re.search(pattern, text)
if phone_match:
potential_phone = phone_match.group(0).strip()
if validate_phone(potential_phone):
phone = potential_phone
break
return {
"name": name if name else None,
"email": email,
"phone": phone
}
# Skills Extraction
def extract_skills(text: str, skill_list: List[str]) -> List[str]:
"""Find skills using case-insensitive whole-word matching"""
found_skills = set()
text_lower = text.lower()
for skill in skill_list:
# Match whole words to avoid partial matches
pattern = rf'\b{re.escape(skill.lower())}\b'
if re.search(pattern, text_lower):
found_skills.add(skill)
return sorted(found_skills)
# Education Extraction
def extract_education(text: str) -> List[str]:
"""Extract education degrees with flexible matching"""
patterns = [
r"(?:B\.?Tech|Bachelor[\s']*(?:of|in)?[\s']*Technology)\b.*?(?:Computer Science|Artificial Intelligence|IT|\bAI\b|\bCS\b|Information Technology)",
r"(?:M\.?Tech|Master[\s']*(?:of|in)?[\s']*Technology)\b.*?(?:Data Science|Machine Learning|Computer)",
r"B\.?[Ee]\.?\b.*?(?:Computer|Electrical|Electronics)",
r"B\.?[Ss]c\.?\b.*?(?:Computer|Physics|Mathematics|IT)",
r"\bPhD\b.*?(?:Computer Science|Engineering|Technology)",
r"(?:Bachelor|Master).*?(?:Computer Science|Information Technology|Software Engineering)",
r"(?:B\.?A\.|Bachelor of Arts).*?(?:Computer|Technology)",
r"(?:M\.?S\.|Master of Science).*?(?:Computer|Data|Technology)"
]
found = set()
for pattern in patterns:
matches = re.findall(pattern, text, re.IGNORECASE)
for match in matches:
# Clean the match
cleaned = re.sub(r'\s+', ' ', match.strip())
if len(cleaned) > 5: # Reasonable length
found.add(cleaned)
return sorted(found) if found else []
# Experience Extraction
def calculate_experience_years(experience_dates: List[str]) -> float:
"""Calculate total years of experience from date ranges"""
total_years = 0.0
for date_range in experience_dates:
# Find all 4-digit years in the string
years = list(map(int, re.findall(r'\b(19|20)\d{2}\b', date_range)))
# Handle "YYYY-YYYY" or "Month YYYY - Month YYYY"
if len(years) >= 2:
start_year = min(years)
end_year = max(years)
total_years += (end_year - start_year)
return round(total_years, 1)
def extract_experience_dates(text: str) -> List[str]:
"""Extract all experience date ranges"""
patterns = [
r'\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+(19|20)\d{2}\s*[\-–to]+\s*(?:(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+)?(19|20)\d{2}|Present|Current',
r'\b(19|20)\d{2}\s*[\-–to]+\s*(19|20)\d{2}\b',
r'\b(19|20)\d{2}\s*[\-–to]+\s*(?:Present|Current)\b'
]
matches = []
for pattern in patterns:
matches.extend(re.findall(pattern, text, re.IGNORECASE))
# Convert tuples to strings
return [' '.join(m) if isinstance(m, tuple) else m for m in matches]
# Main Parser
def parse_resume(file_path: str) -> Optional[Dict]:
"""Parse resume and return structured data with robust validation"""
if not os.path.exists(file_path):
print(f"Error: File not found at {file_path}")
return None
# Extract text based on file type
if file_path.endswith(".pdf"):
text = extract_text_from_pdf(file_path)
elif file_path.endswith(".docx"):
text = extract_text_from_docx(file_path)
else:
print("Error: Only PDF and DOCX files are supported")
return None
if not text or len(text.strip()) < 50:
print("Could not extract sufficient text from the file.")
return None
text = clean_text(text)
# Define comprehensive skills list
skills_list = [
# Programming Languages
"Python", "Java", "JavaScript", "TypeScript", "C++", "C#", "Ruby", "Go", "Rust",
"PHP", "Swift", "Kotlin", "Scala", "R", "MATLAB", "Perl",
# Web Technologies
"HTML", "CSS", "React", "Angular", "Vue.js", "Node.js", "Express", "Django",
"Flask", "FastAPI", "Spring Boot", "ASP.NET", "jQuery", "Bootstrap", "Tailwind",
# Databases
"SQL", "MySQL", "PostgreSQL", "MongoDB", "Redis", "Cassandra", "Oracle",
"SQL Server", "SQLite", "DynamoDB", "Elasticsearch",
# Cloud & DevOps
"AWS", "Azure", "Google Cloud", "GCP", "Docker", "Kubernetes", "Jenkins",
"Git", "GitHub", "GitLab", "CI/CD", "Terraform", "Ansible",
# Data Science & ML
"Machine Learning", "Deep Learning", "TensorFlow", "PyTorch", "Keras",
"Scikit-learn", "Pandas", "NumPy", "Matplotlib", "Seaborn", "NLP",
"Natural Language Processing", "Computer Vision", "Data Analysis",
"Big Data", "Spark", "Hadoop", "Tableau", "Power BI",
# Other Tools
"Linux", "Unix", "Bash", "PowerShell", "Agile", "Scrum", "JIRA",
"Confluence", "Postman", "REST API", "GraphQL", "Microservices"
]
# Extract all information
entities = extract_entities(text)
experience_dates = extract_experience_dates(text)
result = {
"name": entities.get("name"),
"email": entities.get("email"),
"phone": entities.get("phone"),
"education": extract_education(text),
"total_experience_years": calculate_experience_years(experience_dates),
"skills": extract_skills(text, skills_list),
"full_text": text[:5000] # Limit to first 5000 chars
}
# Validate critical fields
if not result["name"]:
result["name"] = "Unknown Candidate"
if not result["email"]:
print("Warning: No valid email found in resume")
return result
# --- Main Execution ---
if __name__ == "__main__":
# Test with a file
file_path = "test_resume.pdf" # Change this to your test file
if os.path.exists(file_path):
parsed_data = parse_resume(file_path)
if parsed_data:
print("--- Resume Parsing Complete ---")
pprint.pprint(parsed_data)
print("-----------------------------")
else:
print(f"Test file not found: {file_path}")
print("Please provide a test resume file to parse.")