In today’s competitive talent market, recruiters often sift through hundreds of resumes for a single role. Manually comparing each resume against a job description is tedious and inconsistent.
In this article, we’ll build a Resume Parser & Job Matcher a REST API that:
Extracts text from PDF, DOCX, or TXT resumes.
Compares the resume against one or more job descriptions.
Returns a match score (0–100) based on cosine similarity and skill overlap.
We’ll use FastAPI for the web framework, spaCy for text preprocessing, scikit‑learn for TF‑IDF vectorisation, and pdfplumber / python-docx for file parsing.
** What You’ll Learn**
- How to parse text from different resume formats.
- How to clean and tokenise text using spaCy.
- How to compute similarity between documents with TF‑IDF.
- How to combine multiple heuristics (keyword overlap) into a final score.
- How to expose everything as a modern, self‑documenting API.
Prerequisites
- Python 3.9+ installed
- Basic familiarity with Python and virtual environments
- A tool to test APIs (cURL, Postman, or just the Swagger UI)
Step 1 – Project Setup
Create a new directory and set up a virtual environment:
# Windows
mkdir resume parser job matcher
cd resume parser job matcher
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# macOS / Linux
python3 -m venv venv
source venv/bin/activate
Create a requirements.txt file:
fastapi==0.104.1
uvicorn==0.24.0
python-multipart==0.0.6
pdfplumber==0.10.3
python-docx==1.1.0
spacy==3.7.2
scikit-learn==1.3.2
pydantic==2.5.0
Install everything:
pip install -r requirements.txt
Pip install https://github.com/explosion/spacy-models/releases/download/zh_core_web_trf-3.8.0/zh_core_web_trf-3.8.0.tar.gz
#!/bin/bash
Step 2 – Project Structure
We’ll organise the code like this:
RESUME PARSER JOB MATCHER/
├── utils/
│ ├──init.py
│ ├── generator.py
│ ├── parser.py
│ ├── matcher.py
│ └── skills.py
├── uploads/ # (created automatically)
├── templates/
├── index.html
├── requirements.txt
└── app.py
Step 3 – Resume Parser (utils/parser.py)
This module extracts plain text from uploaded files.
import os
import re
from typing import Dict, Any, Optional
from datetime import datetime
import PyPDF2
import pdfplumber
from docx import Document
from dateutil import parser as date_parser
import spacy
class ResumeParser:
def __init__(self):
"""Initialize resume parser"""
try:
self.nlp = spacy.load("en_core_web_sm")
except:
import subprocess
subprocess.run(["python", "-m", "spacy", "download", "en_core_web_sm"])
self.nlp = spacy.load("en_core_web_sm")
# Email pattern
self.email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
# Phone pattern
self.phone_pattern = r'(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}'
# Name patterns (simple approach - first proper noun at beginning)
self.name_patterns = [
r'^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)', # Capitalized words at start
r'(?:name|full name)[:\s]*([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)',
]
# Education patterns
self.education_keywords = [
'bachelor', 'master', 'phd', 'doctorate', 'b.tech', 'm.tech',
'b.e.', 'm.e.', 'b.sc', 'm.sc', 'b.a.', 'm.a.', 'b.b.a.', 'm.b.a.',
'high school', 'diploma', 'associate degree'
]
# Experience patterns
self.experience_patterns = [
r'(\d+)\+?\s*(?:years?|yrs?)',
r'experience\s*(?:of|:)?\s*(\d+)\+?\s*(?:years?|yrs?)',
]
def parse_pdf(self, file_path: str) -> str:
"""Extract text from PDF file"""
text = ""
try:
# Try pdfplumber first (better for tables)
with pdfplumber.open(file_path) as pdf:
for page in pdf.pages:
page_text = page.extract_text()
if page_text:
text += page_text + "\n"
# Fallback to PyPDF2 if pdfplumber fails
if not text.strip():
with open(file_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
for page in pdf_reader.pages:
text += page.extract_text() + "\n"
except Exception as e:
print(f"Error parsing PDF: {e}")
return text
def parse_docx(self, file_path: str) -> str:
"""Extract text from DOCX file"""
text = ""
try:
doc = Document(file_path)
for paragraph in doc.paragraphs:
text += paragraph.text + "\n"
except Exception as e:
print(f"Error parsing DOCX: {e}")
return text
def extract_contact_info(self, text: str) -> Dict[str, Optional[str]]:
"""Extract email, phone, and name from text"""
contact = {
'email': None,
'phone': None,
'name': None
}
# Extract email
email_match = re.search(self.email_pattern, text)
if email_match:
contact['email'] = email_match.group()
# Extract phone
phone_match = re.search(self.phone_pattern, text)
if phone_match:
contact['phone'] = phone_match.group().strip()
# Extract name (simplified)
lines = text.split('\n')
for i, line in enumerate(lines[:5]): # Check first 5 lines
line = line.strip()
# Name is usually at the top, not too long, with proper capitalization
if line and len(line.split()) <= 4 and line[0].isupper():
# Check if it contains any common resume section headers
if not any(header in line.lower() for header in ['resume', 'cv', 'curriculum', 'vitae', 'email', 'phone']):
# Check if each word starts with capital
words = line.split()
if all(word[0].isupper() for word in words if word):
contact['name'] = line
break
return contact
def extract_education(self, text: str) -> list:
"""Extract education information"""
education = []
lines = text.split('\n')
for i, line in enumerate(lines):
line_lower = line.lower()
# Check if line contains education keywords
if any(keyword in line_lower for keyword in self.education_keywords):
edu_entry = {
'degree': line.strip(),
'institution': None,
'year': None
}
# Look for institution in surrounding lines
for j in range(max(0, i-2), min(len(lines), i+3)):
if j != i and len(lines[j].strip()) > 5:
if not any(keyword in lines[j].lower() for keyword in self.education_keywords):
edu_entry['institution'] = lines[j].strip()
break
# Look for year
year_match = re.search(r'(19|20)\d{2}', line)
if year_match:
edu_entry['year'] = year_match.group()
education.append(edu_entry)
return education
def extract_experience(self, text: str) -> Dict[str, Any]:
"""Extract work experience summary"""
exp_info = {
'years_of_experience': 0,
'companies': [],
'titles': [],
'summary': ''
}
# Extract years of experience
for pattern in self.experience_patterns:
match = re.search(pattern, text.lower())
if match:
exp_info['years_of_experience'] = int(match.group(1))
break
# Extract job titles and companies (simplified)
lines = text.split('\n')
job_title_pattern = r'([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+(?:at|@|-)\s+([A-Z][a-zA-Z\s&]+)'
for line in lines:
match = re.search(job_title_pattern, line)
if match:
exp_info['titles'].append(match.group(1))
exp_info['companies'].append(match.group(2).strip())
return exp_info
def parse_resume(self, file_path: str) -> Dict[str, Any]:
"""
Parse resume and extract all information
"""
# Extract text based on file type
if file_path.lower().endswith('.pdf'):
text = self.parse_pdf(file_path)
elif file_path.lower().endswith('.docx'):
text = self.parse_docx(file_path)
else:
raise ValueError("Unsupported file format. Please use PDF or DOCX.")
if not text:
raise ValueError("Could not extract text from file")
# Extract all information
result = {
'file_name': os.path.basename(file_path),
'full_text': text,
'contact': self.extract_contact_info(text),
'education': self.extract_education(text),
'experience': self.extract_experience(text),
'raw_text': text # Keep raw text for skill extraction
}
return result
Step 4 – Job Matcher (utils/matcher.py)
This file implements a Job Matching Engine that compares a candidate’s resume with a job description and produces a match score (0–100). It uses multiple weighted criteria to give a balanced evaluation.
Main Purpose
Read structured resume data (skills, experience, education, full text) and job data.
Calculate four independent match scores:
Skill match (50% weight) – compares required skills vs. resume skills.
Experience match (20%) – compares years of experience.
Education match (15%) – compares degree level.
Semantic match (15%) – compares the full‑text using TF‑IDF + cosine similarity.
Combine them into a total score.
Generate recommendations for the candidate (e.g., which skills are missing, how to improve).
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from typing import Dict, List, Tuple, Any
import re
from .skills import SkillExtractor
class JobMatcher:
def __init__(self):
"""Initialize job matcher"""
self.skill_extractor = SkillExtractor()
self.tfidf_vectorizer = TfidfVectorizer(
max_features=1000,
stop_words='english',
ngram_range=(1, 2)
)
# Weights for different matching criteria
self.weights = {
'skill_match': 0.5,
'experience_match': 0.2,
'education_match': 0.15,
'semantic_match': 0.15
}
def calculate_skill_match(self, resume_skills: Dict, job_skills: Dict) -> Dict[str, Any]:
"""
Calculate skill match percentage between resume and job
"""
# Flatten skills lists
resume_skill_list = [skill for skills in resume_skills.values() for skill in skills]
job_skill_list = [skill for skills in job_skills.values() for skill in skills]
# Calculate matches
matched_skills = []
missing_skills = []
for job_skill in job_skill_list:
if any(self.skill_similarity(job_skill, res_skill) > 0.8 for res_skill in resume_skill_list):
matched_skills.append(job_skill)
else:
missing_skills.append(job_skill)
# Calculate scores by category
category_scores = {}
for category in resume_skills.keys():
res_cat_skills = resume_skills[category]
job_cat_skills = job_skills[category]
if job_cat_skills:
matched_cat = [s for s in job_cat_skills if s in res_cat_skills]
category_scores[category] = len(matched_cat) / len(job_cat_skills)
else:
category_scores[category] = 1.0
# Overall skill match score
if job_skill_list:
skill_match_score = len(matched_skills) / len(job_skill_list)
else:
skill_match_score = 0.0
return {
'score': skill_match_score,
'matched_skills': matched_skills,
'missing_skills': missing_skills,
'category_scores': category_scores
}
def skill_similarity(self, skill1: str, skill2: str) -> float:
"""
Calculate similarity between two skills (handles variations)
"""
skill1 = skill1.lower().strip()
skill2 = skill2.lower().strip()
if skill1 == skill2:
return 1.0
# Check if one is substring of the other
if skill1 in skill2 or skill2 in skill1:
return 0.9
# Check word overlap
words1 = set(skill1.split())
words2 = set(skill2.split())
if words1 and words2:
intersection = words1.intersection(words2)
if intersection:
return len(intersection) / max(len(words1), len(words2))
return 0.0
def calculate_experience_match(self, resume_exp: Dict, job_requirements: Dict) -> float:
"""
Calculate experience match score
"""
resume_years = resume_exp.get('years_of_experience', 0)
required_years = job_requirements.get('required_experience', 0)
if required_years == 0:
return 1.0
if resume_years >= required_years:
return 1.0
else:
return resume_years / required_years
def calculate_education_match(self, resume_education: List, job_requirements: Dict) -> float:
"""
Calculate education match score
"""
required_degree = job_requirements.get('required_education', '').lower()
if not required_degree:
return 1.0
if not resume_education:
return 0.0
# Define education levels
edu_levels = {
'phd': 5,
'doctorate': 5,
'master': 4,
'mba': 4,
'bachelor': 3,
'associate': 2,
'diploma': 1,
'high school': 0
}
# Find highest education level in resume
resume_level = 0
for edu in resume_education:
edu_text = edu.get('degree', '').lower()
for level_name, level_value in edu_levels.items():
if level_name in edu_text and level_value > resume_level:
resume_level = level_value
# Get required education level
required_level = 0
for level_name, level_value in edu_levels.items():
if level_name in required_degree:
required_level = level_value
break
if resume_level >= required_level:
return 1.0
else:
return resume_level / required_level if required_level > 0 else 0.5
def calculate_semantic_match(self, resume_text: str, job_text: str) -> float:
"""
Calculate semantic similarity between resume and job description
"""
try:
# Use TF-IDF and cosine similarity
texts = [resume_text, job_text]
tfidf_matrix = self.tfidf_vectorizer.fit_transform(texts)
similarity = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0]
return float(similarity)
except:
return 0.0
def match_resume_to_job(self, resume_data: Dict, job_data: Dict) -> Dict[str, Any]:
"""
Match resume to job and return detailed scores
"""
# Extract skills
resume_skills = resume_data.get('skills', {})
job_skills = job_data.get('skills', {})
# Calculate individual scores
skill_match = self.calculate_skill_match(resume_skills, job_skills)
experience_match = self.calculate_experience_match(
resume_data.get('experience', {}),
job_data.get('requirements', {})
)
education_match = self.calculate_education_match(
resume_data.get('education', []),
job_data.get('requirements', {})
)
semantic_match = self.calculate_semantic_match(
resume_data.get('full_text', ''),
job_data.get('description', '')
)
# Calculate weighted total score
total_score = (
self.weights['skill_match'] * skill_match['score'] +
self.weights['experience_match'] * experience_match +
self.weights['education_match'] * education_match +
self.weights['semantic_match'] * semantic_match
)
# Generate recommendations
recommendations = self.generate_recommendations(
skill_match['missing_skills'],
experience_match,
education_match
)
return {
'total_score': round(total_score * 100, 2),
'skill_match': {
'score': round(skill_match['score'] * 100, 2),
'matched_skills': skill_match['matched_skills'],
'missing_skills': skill_match['missing_skills']
},
'experience_match': round(experience_match * 100, 2),
'education_match': round(education_match * 100, 2),
'semantic_match': round(semantic_match * 100, 2),
'recommendations': recommendations,
'breakdown': {
'skill_weight': self.weights['skill_match'],
'experience_weight': self.weights['experience_match'],
'education_weight': self.weights['education_match'],
'semantic_weight': self.weights['semantic_match']
}
}
def generate_recommendations(self, missing_skills: List[str],
exp_score: float, edu_score: float) -> List[str]:
"""
Generate recommendations based on missing qualifications
"""
recommendations = []
if missing_skills:
if len(missing_skills) <= 3:
skills_str = ', '.join(missing_skills)
recommendations.append(f"Add these key skills to your resume: {skills_str}")
else:
recommendations.append(f"Consider gaining experience in: {', '.join(missing_skills[:5])}")
if len(missing_skills) > 5:
recommendations.append(f"And {len(missing_skills) - 5} other skills")
if exp_score < 0.7:
recommendations.append("Highlight your relevant work experience more prominently")
if edu_score < 0.7:
recommendations.append("Consider adding relevant certifications or courses")
if not recommendations:
recommendations.append("Your profile matches well with this position!")
return recommendations
Step 5 – Generator (utils/generator.py)
This file is a data generation utility – it creates synthetic (fake) resumes and job descriptions that look realistic. It’s designed to help developers test a resume parser / job matcher without needing real, sensitive candidate data.
import random
import json
import os
from datetime import datetime, timedelta
from typing import List, Dict
class SampleDataGenerator:
def __init__(self):
"""Initialize sample data generator"""
self.first_names = [
"James", "Mary", "John", "Patricia", "Robert", "Jennifer",
"Michael", "Linda", "William", "Elizabeth", "David", "Susan",
"Richard", "Jessica", "Joseph", "Sarah", "Thomas", "Karen",
"Charles", "Nancy", "Christopher", "Lisa", "Daniel", "Betty"
]
self.last_names = [
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia",
"Miller", "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez",
"Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore",
"Jackson", "Martin", "Lee", "Perez", "Thompson", "White"
]
self.companies = [
"Tech Solutions Inc.", "DataCorp", "InnovateSoft", "CloudNine",
"AI Dynamics", "WebCraft", "AppGenius", "SecureNet", "DevOps Pro",
"Analytics Plus", "Smart Systems", "Digital Edge", "Future Tech",
"Global IT Solutions", "NextGen Software", "Prime Technologies"
]
self.job_titles = [
"Software Engineer", "Data Scientist", "DevOps Engineer",
"Frontend Developer", "Backend Developer", "Full Stack Developer",
"Machine Learning Engineer", "Cloud Architect", "Product Manager",
"Project Manager", "Business Analyst", "QA Engineer",
"Systems Administrator", "Network Engineer", "Security Analyst"
]
self.universities = [
"Stanford University", "MIT", "University of California",
"Carnegie Mellon", "University of Washington", "Georgia Tech",
"University of Texas", "Purdue University", "NYU", "Columbia University",
"University of Michigan", "Cornell University", "Harvard University"
]
self.degree_levels = [
"Bachelor of Science in", "Master of Science in",
"Bachelor of Arts in", "PhD in", "Associate Degree in"
]
self.majors = [
"Computer Science", "Information Technology", "Data Science",
"Software Engineering", "Electrical Engineering", "Mathematics",
"Statistics", "Business Administration", "Information Systems"
]
self.skill_descriptions = {
"programming_languages": [
"Proficient in", "Experienced with", "Working knowledge of",
"Expert in", "Skilled in"
],
"frameworks_libraries": [
"Extensive experience with", "Built applications using",
"Developed projects with", "Proficient in"
],
"databases": [
"Experience with", "Designed and implemented",
"Managed", "Optimized queries in"
],
"cloud_devops": [
"Deployed applications on", "Managed infrastructure in",
"Implemented CI/CD with", "Certified in"
]
}
def generate_resume_text(self, name: str, title: str) -> str:
"""Generate realistic resume text"""
skills_db = {
"programming_languages": ["Python", "Java", "JavaScript", "C++", "Ruby", "Go", "Rust", "TypeScript"],
"frameworks_libraries": ["Django", "React", "Spring", "TensorFlow", "PyTorch", "Flask", "Node.js"],
"databases": ["PostgreSQL", "MongoDB", "MySQL", "Redis", "Elasticsearch"],
"cloud_devops": ["AWS", "Docker", "Kubernetes", "Jenkins", "Terraform", "Azure"]
}
# Select random skills
selected_skills = {}
for category, skills in skills_db.items():
num_skills = random.randint(3, 6)
selected_skills[category] = random.sample(skills, min(num_skills, len(skills)))
# Generate experience
num_jobs = random.randint(2, 4)
total_years = 0
experience_section = ""
for i in range(num_jobs):
company = random.choice(self.companies)
years = random.randint(1, 4)
total_years += years
if i == 0:
role = title
else:
role = random.choice(self.job_titles)
experience_section += f"""
{role}
{company} | {years} years
• Developed and maintained applications using {', '.join(selected_skills['programming_languages'][:2])}
• Implemented {random.choice(selected_skills['frameworks_libraries'])} for various projects
• Collaborated with cross-functional teams to deliver high-quality software
• {random.choice(['Optimized', 'Improved', 'Enhanced'])} system performance by {random.randint(20, 40)}%
• Managed {random.choice(['database', 'cloud', 'infrastructure'])} using {', '.join(selected_skills['databases'][:1] + selected_skills['cloud_devops'][:1])}
"""
# Generate education
education = []
edu_level = random.choice(self.degree_levels)
major = random.choice(self.majors)
uni = random.choice(self.universities)
grad_year = random.randint(2015, 2023)
education.append(f"{edu_level} {major}, {uni} - {grad_year}")
if random.random() > 0.7: # 30% chance of advanced degree
edu_level = random.choice(["Master of Science in", "PhD in"])
major = random.choice(self.majors)
uni = random.choice(self.universities)
grad_year = random.randint(2018, 2024)
education.append(f"{edu_level} {major}, {uni} - {grad_year}")
# Generate skills section
skills_section = "\nTECHNICAL SKILLS\n"
for category, skills in selected_skills.items():
skills_section += f"• {category.replace('_', ' ').title()}: {', '.join(skills)}\n"
# Generate projects section
projects_section = "\nPROJECTS\n"
for i in range(random.randint(2, 4)):
tech_stack = random.sample(selected_skills['programming_languages'], 2) + \
random.sample(selected_skills['frameworks_libraries'], 1)
projects_section += f"• Project {i+1}: Built using {', '.join(tech_stack)}\n"
projects_section += f" {random.choice(['Developed', 'Created', 'Designed'])} a {random.choice(['web app', 'mobile app', 'API', 'data pipeline'])} for {random.choice(['internal use', 'clients', 'open source'])}\n"
# Generate certifications (optional)
certs_section = ""
if random.random() > 0.5:
certs = random.sample(["AWS Certified", "Azure Certified", "Google Cloud Certified", "Scrum Master", "PMP"], 2)
certs_section = "\nCERTIFICATIONS\n"
for cert in certs:
certs_section += f"• {cert}\n"
# Complete resume text
resume_text = f"""
{name}
{random.choice(['email@example.com', name.lower().replace(' ', '.') + '@email.com'])} | {random.choice(['123-456-7890', '555-123-4567'])} | {random.choice(['San Francisco, CA', 'New York, NY', 'Seattle, WA', 'Austin, TX'])}
PROFESSIONAL SUMMARY
Experienced {title} with {total_years}+ years of experience in software development.
Proven track record of delivering high-quality solutions using modern technologies.
EXPERIENCE
{experience_section}
EDUCATION
{chr(10).join(['• ' + edu for edu in education])}
{certs_section}
{skills_section}
{projects_section}
"""
return resume_text
def generate_job_description(self) -> Dict:
"""Generate a realistic job description"""
title = random.choice(self.job_titles)
company = random.choice(self.companies)
skills_db = {
"programming_languages": ["Python", "Java", "JavaScript", "C++", "Go", "TypeScript"],
"frameworks_libraries": ["Django", "React", "Spring", "TensorFlow", "Node.js", "Angular"],
"databases": ["PostgreSQL", "MongoDB", "MySQL", "Redis"],
"cloud_devops": ["AWS", "Docker", "Kubernetes", "Jenkins", "Git"],
"soft_skills": ["Communication", "Teamwork", "Problem Solving", "Leadership"],
"project_management": ["Agile", "Scrum", "JIRA"],
"data_science": ["Machine Learning", "Data Analysis", "Statistics"]
}
# Select required skills
required_skills = {}
for category, skills in skills_db.items():
num_skills = random.randint(2, 5)
required_skills[category] = random.sample(skills, min(num_skills, len(skills)))
# Generate requirements
exp_years = random.choice([2, 3, 5, 5, 7, 10])
edu_level = random.choice(["Bachelor's degree", "Master's degree"])
job_text = f"""
Job Title: {title}
Company: {company}
Location: {random.choice(['Remote', 'Hybrid', 'On-site'])} - {random.choice(['San Francisco, CA', 'New York, NY', 'Austin, TX'])}
About the Role:
We are seeking an experienced {title} to join our growing team.
The ideal candidate will have strong technical skills and a passion for building scalable solutions.
Responsibilities:
• Design and develop high-quality software solutions
• Collaborate with cross-functional teams to deliver features
• Write clean, maintainable, and efficient code
• Participate in code reviews and technical discussions
• Troubleshoot and debug applications
• Stay up-to-date with emerging technologies
Required Qualifications:
• {exp_years}+ years of experience in software development
• {edu_level} in Computer Science or related field
• Strong proficiency in {', '.join(required_skills['programming_languages'][:3])}
• Experience with {', '.join(required_skills['frameworks_libraries'][:2])}
• Knowledge of {', '.join(required_skills['databases'][:2])}
• Familiarity with {', '.join(required_skills['cloud_devops'][:2])}
• {', '.join(required_skills['soft_skills'][:3])} skills
• Experience with {', '.join(required_skills['project_management'][:2])} methodologies
Preferred Qualifications:
• Master's degree preferred
• Experience with {random.choice(required_skills.get('data_science', ['Machine Learning']))}
• {random.choice(['AWS', 'Azure', 'Google Cloud'])} certification
• Open source contributions
Benefits:
• Competitive salary and equity
• Health, dental, and vision insurance
• 401(k) matching
• Flexible work hours
• Professional development budget
• Generous PTO
"""
job_data = {
'title': title,
'company': company,
'description': job_text,
'skills': required_skills,
'requirements': {
'required_experience': exp_years,
'required_education': edu_level,
'location': random.choice(['Remote', 'Hybrid', 'On-site'])
}
}
return job_data
def generate_sample_resumes(self, num_resumes: int = 5) -> List[Dict]:
"""Generate multiple sample resumes"""
resumes = []
for i in range(num_resumes):
name = f"{random.choice(self.first_names)} {random.choice(self.last_names)}"
title = random.choice(self.job_titles)
resume_data = {
'file_name': f"sample_resume_{i+1}.txt",
'name': name,
'title': title,
'text': self.generate_resume_text(name, title)
}
resumes.append(resume_data)
return resumes
def generate_sample_jobs(self, num_jobs: int = 5) -> List[Dict]:
"""Generate multiple sample job descriptions"""
jobs = []
for i in range(num_jobs):
job_data = self.generate_job_description()
job_data['file_name'] = f"sample_job_{i+1}.txt"
jobs.append(job_data)
return jobs
def save_sample_data(self, directory: str = 'data'):
"""Save sample data to files"""
# Create directory if it doesn't exist
os.makedirs(os.path.join(directory, 'sample_resumes'), exist_ok=True)
os.makedirs(os.path.join(directory, 'sample_jobs'), exist_ok=True)
# Generate and save resumes
resumes = self.generate_sample_resumes(5)
for i, resume in enumerate(resumes):
file_path = os.path.join(directory, 'sample_resumes', f"resume_{i+1}.txt")
with open(file_path, 'w') as f:
f.write(resume['text'])
# Also save metadata
meta_path = os.path.join(directory, 'sample_resumes', f"resume_{i+1}_meta.json")
with open(meta_path, 'w') as f:
json.dump({
'name': resume['name'],
'title': resume['title']
}, f, indent=2)
# Generate and save jobs
jobs = self.generate_sample_jobs(5)
for i, job in enumerate(jobs):
file_path = os.path.join(directory, 'sample_jobs', f"job_{i+1}.txt")
with open(file_path, 'w') as f:
f.write(job['description'])
# Also save metadata
meta_path = os.path.join(directory, 'sample_jobs', f"job_{i+1}_meta.json")
with open(meta_path, 'w') as f:
json.dump({
'title': job['title'],
'company': job['company'],
'requirements': job['requirements']
}, f, indent=2)
print(f"Generated {len(resumes)} sample resumes and {len(jobs)} sample jobs in {directory}/")
Step 6 – Skills (utils/skills.py)
It implements a skill extraction engine that analyses unstructured text (e.g., resumes, job descriptions) and extracts professional skills, categorising them into groups like programming languages, frameworks, databases, cloud tools, soft skills, etc.
Initialization (init)
When a SkillExtractor object is created:
Loads spaCy model – en_core_web_sm is loaded. If not present, it automatically downloads it.
Builds a skill database – A dictionary skills_db containing 8 categories, each with a list of relevant skills.
Examples:
`programming_languages: python, java, c++,
frameworks_libraries: django, react, tensorflow,
databases: mysql, mongodb, redis,
cloud_devops: aws, docker, kubernetes, git,
soft_skills: communication, leadership, problem solving,
project_management: agile, scrum, jira,
data_science: machine learning, nlp, computer vision,
certifications: aws certified, pmp, scrum master,
`
Flattens all skills into self.all_skills for fast searching.
Defines regex patterns to capture skill‑like phrases (e.g., “proficient in Python”, “Skills: Java, SQL”).
- extract_skills(text: str) -> Dict[str, List[str]] This method takes a plain
Step 7 – init (utils/init.py)
# Make utils a Python package
from .parser import ResumeParser
from .skills import SkillExtractor
from .matcher import JobMatcher
from .generator import SampleDataGenerator
__all__ = ['ResumeParser', 'SkillExtractor', 'JobMatcher', 'SampleDataGenerator']
Step 8 – app (app.py)
This file is the main backend of a web application that allows users to:
`Upload a resume (PDF/DOCX)
Enter a job description manually
Get a match score between the resume and the job
`
It uses Flask as the web framework, stores temporary data in server-side sessions, and relies on several custom utility modules (parser, skills, matcher, generator) to handle the core logic.
from flask import Flask, render_template, request, jsonify, session
import os
import json
import tempfile
from werkzeug.utils import secure_filename
from utils.parser import ResumeParser
from utils.skills import SkillExtractor
from utils.matcher import JobMatcher
from utils.generator import SampleDataGenerator
app = Flask(__name__)
app.secret_key = '664fcd6b37618f5da8eab5c65598c9cd99c73b08442005ad3eb7e7d81c204ebf' # Change this in production
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
# Allowed file extensions
ALLOWED_EXTENSIONS = {'pdf', 'docx'}
# Initialize components
resume_parser = ResumeParser()
skill_extractor = SkillExtractor()
job_matcher = JobMatcher()
data_generator = SampleDataGenerator()
# Ensure upload folder exists
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
def allowed_file(filename):
"""Check if file extension is allowed"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/')
def index():
"""Home page"""
return render_template('index.html')
@app.route('/upload_resume', methods=['POST'])
def upload_resume():
"""Upload and parse resume"""
if 'resume' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400
file = request.files['resume']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
if not allowed_file(file.filename):
return jsonify({'error': 'File type not allowed. Please upload PDF or DOCX'}), 400
try:
# Save file temporarily
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
# Parse resume
resume_data = resume_parser.parse_resume(filepath)
# Extract skills
skills = skill_extractor.extract_skills(resume_data['full_text'])
resume_data['skills'] = skills
resume_data['skill_summary'] = skill_extractor.get_skill_summary(skills)
# Store in session (simplified - in production use database)
session['resume_data'] = resume_data
# Clean up
os.remove(filepath)
return jsonify({
'success': True,
'resume_data': {
'name': resume_data['contact']['name'],
'email': resume_data['contact']['email'],
'skills_summary': resume_data['skill_summary'],
'experience': resume_data['experience']
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/add_job', methods=['POST'])
def add_job():
"""Add job description manually"""
data = request.json
if not data or 'description' not in data:
return jsonify({'error': 'No job description provided'}), 400
try:
job_description = data['description']
job_title = data.get('title', 'Unknown Position')
# Extract skills from job description
job_skills = skill_extractor.extract_skills(job_description)
# Extract requirements (simplified)
requirements = {
'required_experience': 0,
'required_education': ''
}
# Try to extract years of experience
import re
exp_match = re.search(r'(\d+)[\+]?\s*(?:years?|yrs?).*?(?:experience)', job_description.lower())
if exp_match:
requirements['required_experience'] = int(exp_match.group(1))
# Store job data
job_data = {
'title': job_title,
'description': job_description,
'skills': job_skills,
'requirements': requirements
}
session['job_data'] = job_data
return jsonify({
'success': True,
'job_data': {
'title': job_title,
'skills_summary': skill_extractor.get_skill_summary(job_skills),
'requirements': requirements
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/match', methods=['POST'])
def match():
"""Match resume with job"""
if 'resume_data' not in session or 'job_data' not in session:
return jsonify({'error': 'Please upload both resume and job description first'}), 400
try:
resume_data = session['resume_data']
job_data = session['job_data']
# Perform matching
match_result = job_matcher.match_resume_to_job(resume_data, job_data)
return jsonify({
'success': True,
'match_result': match_result
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/generate_sample', methods=['POST'])
def generate_sample():
"""Generate sample data for testing"""
try:
# Generate sample resume
resumes = data_generator.generate_sample_resumes(1)
sample_resume = resumes[0]
# Generate sample job
jobs = data_generator.generate_sample_jobs(1)
sample_job = jobs[0]
# Parse and process resume
resume_data = {
'file_name': 'sample_resume.txt',
'full_text': sample_resume['text'],
'contact': {
'name': sample_resume['name'],
'email': f"{sample_resume['name'].lower().replace(' ', '.')}@example.com",
'phone': '555-123-4567'
},
'skills': skill_extractor.extract_skills(sample_resume['text']),
'experience': {'years_of_experience': 5, 'companies': [], 'titles': []}
}
# Store in session
session['resume_data'] = resume_data
session['job_data'] = sample_job
return jsonify({
'success': True,
'message': 'Sample data generated successfully',
'resume': {
'name': sample_resume['name'],
'title': sample_resume['title']
},
'job': {
'title': sample_job['title'],
'company': sample_job['company']
}
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/clear', methods=['POST'])
def clear_session():
"""Clear session data"""
session.clear()
return jsonify({'success': True})
if __name__ == '__main__':
# Generate sample data if it doesn't exist
if not os.path.exists('data/sample_resumes'):
print("Generating sample data...")
data_generator.save_sample_data()
app.run(debug=True, port=5000)
Step 9 – index (Template/index.html)
This HTML file is the client‑side user interface for a Resume Parser & Job Matcher web application. It provides an interactive dashboard where users can upload a resume (PDF/DOCX), enter a job description, and receive a detailed match analysis. The page communicates with a backend API (built with FastAPI or Flask) using fetch requests.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Resume Parser & Job Matcher</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
text-align: center;
color: white;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.section {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.section h2 {
color: #333;
margin-bottom: 20px;
font-size: 1.5em;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 10px;
}
.upload-area {
border: 2px dashed #ddd;
border-radius: 10px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover {
border-color: #667eea;
background: #f8f9ff;
}
.upload-area i {
font-size: 48px;
color: #667eea;
margin-bottom: 15px;
}
.upload-area p {
color: #666;
margin-bottom: 10px;
}
.upload-area small {
color: #999;
}
.file-input {
display: none;
}
.job-input {
margin-top: 20px;
}
textarea {
width: 100%;
height: 200px;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 14px;
resize: vertical;
transition: border-color 0.3s;
}
textarea:focus {
outline: none;
border-color: #667eea;
}
.button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 16px;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
margin-top: 15px;
width: 100%;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.sample-button {
background: linear-gradient(135deg, #48c6ef 0%, #6f86d6 100%);
margin-bottom: 10px;
}
.clear-button {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.info-card {
background: #f8f9ff;
border-radius: 10px;
padding: 20px;
margin: 15px 0;
}
.info-card h3 {
color: #333;
margin-bottom: 10px;
}
.skill-tag {
display: inline-block;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 5px 15px;
border-radius: 20px;
margin: 5px;
font-size: 14px;
}
.skill-category {
margin: 15px 0;
}
.skill-category h4 {
color: #555;
margin-bottom: 10px;
text-transform: capitalize;
}
.results-section {
grid-column: 1 / -1;
}
.match-score {
text-align: center;
margin: 20px 0;
}
.score-circle {
width: 150px;
height: 150px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
color: white;
font-size: 36px;
font-weight: bold;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
}
.match-breakdown {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.breakdown-item {
background: #f8f9ff;
padding: 15px;
border-radius: 10px;
text-align: center;
}
.breakdown-item h4 {
color: #555;
margin-bottom: 10px;
}
.breakdown-item .score {
font-size: 24px;
font-weight: bold;
color: #667eea;
}
.recommendations {
background: #e8f5e9;
border-radius: 10px;
padding: 20px;
margin: 20px 0;
}
.recommendations h3 {
color: #2e7d32;
margin-bottom: 10px;
}
.recommendations ul {
list-style-type: none;
}
.recommendations li {
padding: 8px 0;
padding-left: 25px;
position: relative;
}
.recommendations li:before {
content: "✓";
color: #2e7d32;
position: absolute;
left: 0;
}
.loading {
text-align: center;
padding: 20px;
display: none;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.status-message {
padding: 15px;
border-radius: 10px;
margin: 10px 0;
display: none;
}
.status-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.button-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
@media (max-width: 768px) {
.main-content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>📄 Resume Parser & Job Matcher</h1>
<div class="button-group">
<button class="button sample-button" onclick="generateSampleData()">🎲 Generate Sample Data</button>
<button class="button clear-button" onclick="clearAll()">🗑️ Clear All</button>
</div>
<div class="main-content">
<!-- Resume Upload Section -->
<div class="section">
<h2>📄 Upload Resume</h2>
<div class="upload-area" onclick="document.getElementById('resumeInput').click()">
<i>📎</i>
<p>Click to upload or drag and drop</p>
<p>PDF or DOCX (Max 16MB)</p>
<small>Supported formats: PDF, DOCX</small>
</div>
<input type="file" id="resumeInput" class="file-input" accept=".pdf,.docx" onchange="uploadResume()">
<div id="resumeInfo" class="info-card" style="display: none;">
<h3 id="resumeName"></h3>
<p id="resumeEmail"></p>
<div id="resumeSkills"></div>
</div>
</div>
<!-- Job Description Section -->
<div class="section">
<h2>💼 Job Description</h2>
<div class="job-input">
<input type="text" id="jobTitle" placeholder="Job Title (optional)" style="width: 100%; padding: 10px; margin-bottom: 10px; border: 2px solid #e0e0e0; border-radius: 5px;">
<textarea id="jobDescription" placeholder="Paste job description here..."></textarea>
<button class="button" onclick="addJobDescription()">Add Job Description</button>
</div>
<div id="jobInfo" class="info-card" style="display: none;">
<h3 id="jobTitleDisplay"></h3>
<div id="jobSkills"></div>
<p id="jobRequirements"></p>
</div>
</div>
</div>
<!-- Match Button -->
<button class="button" onclick="matchResume()" id="matchButton" style="margin-bottom: 20px;" disabled>🔍 Match Resume with Job</button>
<!-- Results Section -->
<div class="section results-section" id="resultsSection" style="display: none;">
<h2>📊 Match Results</h2>
<div class="match-score">
<div class="score-circle" id="totalScore">0%</div>
<h3>Overall Match</h3>
</div>
<div class="match-breakdown" id="breakdown"></div>
<div class="recommendations" id="recommendations"></div>
<div id="skillsComparison"></div>
</div>
<!-- Loading and Status -->
<div class="loading" id="loading">
<div class="loading-spinner"></div>
<p>Processing...</p>
</div>
<div class="status-message" id="statusMessage"></div>
</div>
<script>
let resumeData = null;
let jobData = null;
function showStatus(message, isError = false) {
const statusDiv = document.getElementById('statusMessage');
statusDiv.textContent = message;
statusDiv.className = 'status-message ' + (isError ? 'status-error' : 'status-success');
statusDiv.style.display = 'block';
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
function displaySkills(skills, containerId) {
const container = document.getElementById(containerId);
container.innerHTML = '';
if (!skills || Object.keys(skills).length === 0) {
container.innerHTML = '<p>No skills extracted</p>';
return;
}
for (const [category, skillList] of Object.entries(skills)) {
if (skillList.length > 0) {
const categoryDiv = document.createElement('div');
categoryDiv.className = 'skill-category';
const categoryTitle = document.createElement('h4');
categoryTitle.textContent = category.replace(/_/g, ' ');
categoryDiv.appendChild(categoryTitle);
skillList.forEach(skill => {
const tag = document.createElement('span');
tag.className = 'skill-tag';
tag.textContent = skill;
categoryDiv.appendChild(tag);
});
container.appendChild(categoryDiv);
}
}
}
async function uploadResume() {
const fileInput = document.getElementById('resumeInput');
const file = fileInput.files[0];
if (!file) return;
const formData = new FormData();
formData.append('resume', file);
showLoading(true);
try {
const response = await fetch('/upload_resume', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
resumeData = data.resume_data;
// Display resume info
document.getElementById('resumeName').textContent = resumeData.name || 'Unknown';
document.getElementById('resumeEmail').textContent = resumeData.email || 'No email';
if (resumeData.skills_summary) {
displaySkills(resumeData.skills_summary.skills_by_category || {}, 'resumeSkills');
}
document.getElementById('resumeInfo').style.display = 'block';
// Enable match button if both are present
if (jobData) document.getElementById('matchButton').disabled = false;
showStatus('Resume uploaded successfully!');
} else {
showStatus(data.error || 'Error uploading resume', true);
}
} catch (error) {
showStatus('Error uploading resume', true);
console.error(error);
} finally {
showLoading(false);
}
}
async function addJobDescription() {
const title = document.getElementById('jobTitle').value;
const description = document.getElementById('jobDescription').value;
if (!description.trim()) {
showStatus('Please enter a job description', true);
return;
}
showLoading(true);
try {
const response = await fetch('/add_job', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: title,
description: description
})
});
const data = await response.json();
if (data.success) {
jobData = data.job_data;
// Display job info
document.getElementById('jobTitleDisplay').textContent = jobData.title || 'Unknown Position';
if (jobData.skills_summary) {
displaySkills(jobData.skills_summary.skills_by_category || {}, 'jobSkills');
}
if (jobData.requirements) {
document.getElementById('jobRequirements').innerHTML = `
<strong>Requirements:</strong><br>
Experience: ${jobData.requirements.required_experience || 0} years<br>
Education: ${jobData.requirements.required_education || 'Not specified'}
`;
}
document.getElementById('jobInfo').style.display = 'block';
// Enable match button if both are present
if (resumeData) document.getElementById('matchButton').disabled = false;
showStatus('Job description added successfully!');
} else {
showStatus(data.error || 'Error adding job description', true);
}
} catch (error) {
showStatus('Error adding job description', true);
console.error(error);
} finally {
showLoading(false);
}
}
async function matchResume() {
if (!resumeData || !jobData) {
showStatus('Please upload both resume and job description first', true);
return;
}
showLoading(true);
try {
const response = await fetch('/match', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
displayMatchResults(data.match_result);
document.getElementById('resultsSection').style.display = 'block';
showStatus('Matching completed successfully!');
} else {
showStatus(data.error || 'Error matching', true);
}
} catch (error) {
showStatus('Error matching resume with job', true);
console.error(error);
} finally {
showLoading(false);
}
}
function displayMatchResults(results) {
// Total score
document.getElementById('totalScore').textContent = results.total_score + '%';
// Breakdown
const breakdown = document.getElementById('breakdown');
breakdown.innerHTML = `
<div class="breakdown-item">
<h4>Skills Match</h4>
<div class="score">${results.skill_match.score}%</div>
</div>
<div class="breakdown-item">
<h4>Experience Match</h4>
<div class="score">${results.experience_match}%</div>
</div>
<div class="breakdown-item">
<h4>Education Match</h4>
<div class="score">${results.education_match}%</div>
</div>
<div class="breakdown-item">
<h4>Semantic Match</h4>
<div class="score">${results.semantic_match}%</div>
</div>
`;
// Recommendations
const recommendations = document.getElementById('recommendations');
recommendations.innerHTML = `
<h3>💡 Recommendations</h3>
<ul>
${results.recommendations.map(rec => `<li>${rec}</li>`).join('')}
</ul>
`;
// Skills comparison
const skillsComparison = document.getElementById('skillsComparison');
if (results.skill_match.matched_skills.length > 0 || results.skill_match.missing_skills.length > 0) {
skillsComparison.innerHTML = `
<h3>Skills Analysis</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;">
<div>
<h4 style="color: #2e7d32;">✓ Matched Skills (${results.skill_match.matched_skills.length})</h4>
<div>
${results.skill_match.matched_skills.map(skill =>
`<span class="skill-tag" style="background: #2e7d32;">${skill}</span>`
).join('')}
</div>
</div>
<div>
<h4 style="color: #c62828;">✗ Missing Skills (${results.skill_match.missing_skills.length})</h4>
<div>
${results.skill_match.missing_skills.map(skill =>
`<span class="skill-tag" style="background: #c62828;">${skill}</span>`
).join('')}
</div>
</div>
</div>
`;
}
}
async function generateSampleData() {
showLoading(true);
try {
const response = await fetch('/generate_sample', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
// Update UI with sample data
resumeData = data.resume;
jobData = data.job;
document.getElementById('resumeName').textContent = data.resume.name;
document.getElementById('resumeEmail').textContent = 'sample@email.com';
document.getElementById('resumeInfo').style.display = 'block';
document.getElementById('jobTitleDisplay').textContent = data.job.title;
document.getElementById('jobInfo').style.display = 'block';
document.getElementById('matchButton').disabled = false;
showStatus('Sample data generated successfully!');
} else {
showStatus(data.error || 'Error generating sample data', true);
}
} catch (error) {
showStatus('Error generating sample data', true);
console.error(error);
} finally {
showLoading(false);
}
}
async function clearAll() {
try {
const response = await fetch('/clear', {
method: 'POST'
});
if (response.ok) {
// Reset UI
resumeData = null;
jobData = null;
document.getElementById('resumeInput').value = '';
document.getElementById('jobDescription').value = '';
document.getElementById('jobTitle').value = '';
document.getElementById('resumeInfo').style.display = 'none';
document.getElementById('jobInfo').style.display = 'none';
document.getElementById('resultsSection').style.display = 'none';
document.getElementById('matchButton').disabled = true;
showStatus('All data cleared');
}
} catch (error) {
showStatus('Error clearing data', true);
}
}
// Drag and drop functionality
const dropZone = document.querySelector('.upload-area');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#667eea';
dropZone.style.background = '#f8f9ff';
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#ddd';
dropZone.style.background = 'white';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#ddd';
dropZone.style.background = 'white';
const files = e.dataTransfer.files;
if (files.length > 0) {
document.getElementById('resumeInput').files = files;
uploadResume();
}
});
</script>
</body>
</html>
Generate sample data
python -c "from utils.generator import SampleDataGenerator; SampleDataGenerator().save_sample_data()"
Run the application: python app.py
Open your browser and go to http://localhost:5000
Test the system
Upload a PDF/DOCX resume (or click “Generate Sample Data”).
Add a job description (or use the auto‑generated one).
Click “Get Match Score” to see the result.
Top comments (0)