├── ranked_resumes.csv ├── resume1.pdf ├── resume2.pdf ├── resume3.pdf ├── requirements.txt ├── __pycache__ └── app.cpython-311.pyc ├── README.md ├── resume_ranker.py ├── static └── styles.css ├── templates └── index.html └── app.py /ranked_resumes.csv: -------------------------------------------------------------------------------- 1 | Rank,Name,Email,Similarity 2 | -------------------------------------------------------------------------------- /resume1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamanporwal/resume-ranker/HEAD/resume1.pdf -------------------------------------------------------------------------------- /resume2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamanporwal/resume-ranker/HEAD/resume2.pdf -------------------------------------------------------------------------------- /resume3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamanporwal/resume-ranker/HEAD/resume3.pdf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.1.1 2 | spacy==3.1.3 3 | PyPDF2==1.26.0 4 | scikit-learn==0.24.2 5 | -------------------------------------------------------------------------------- /__pycache__/app.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamamanporwal/resume-ranker/HEAD/__pycache__/app.cpython-311.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resume Analyzer Web App :memo::computer: 2 | 3 | ![GitHub last commit](https://img.shields.io/github/last-commit/iamamanporwal/resume-ranker) 4 | ![Python Version](https://img.shields.io/badge/python-3.8%2B-blue) 5 | 6 | An interactive web application that analyzes resumes based on a job description using natural language processing techniques. https://www.youtube.com/watch?v=0eo_5oyW11o&t=1s 7 | 8 | ## :rocket: Features 9 | 10 | - Upload job descriptions and resumes in PDF format. 11 | - Process resumes to extract names, emails, and text content. 12 | - Calculate the similarity between the job description and each resume. 13 | - Rank resumes based on similarity percentage. 14 | - Download the ranked resumes in a CSV file. (Fixed by @atiumcache) 15 | - Dark Mode Contributed by @hbalickgoodman 16 | 17 | ## :wrench: Setup and Usage 18 | 19 | 1. Clone the repository: 20 | ```sh 21 | https://github.com/iamamanporwal/resume-ranker.git 22 | ``` 23 | 24 | 2. Navigate to the project directory: 25 | ```sh 26 | cd resume-analyzer 27 | ``` 28 | 29 | 3. Install dependencies (Install the latest libraries instead of the specific ones mentioned in the txt file): 30 | ```sh 31 | pip install -r requirements.txt 32 | ``` 33 | 34 | 4. Run the Flask app: 35 | ```sh 36 | python app.py 37 | ``` 38 | 39 | 5. Access the app in your web browser at `http://localhost:5000`. 40 | 41 | ## :file_folder: Directory Structure 42 | 43 | ``` 44 | ├── app.py 45 | ├── static/ 46 | │ └── styles.css 47 | ├── templates/ 48 | │ └── index.html 49 | ├── uploads/ # Uploaded resumes will be saved here 50 | ├── requirements.txt 51 | ├── README.md 52 | └── .gitignore 53 | ``` 54 | 55 | 56 | ## :memo: Contributing 57 | 58 | Contributions are welcome! Feel free to open an issue or submit a pull request. 59 | 60 | ## :bulb: Inspiration 61 | 62 | This project was inspired by the desire to create an interactive tool for HR professionals to easily analyze job applicants' resumes. 63 | 64 | ## :mailbox: Contact 65 | 66 | Have questions or feedback? Feel free to reach out via [aman07porwal@gmail.com](mailto:aman07porwal@gmail.com). 67 | 68 | ## 69 | Developed with :heart: by Aman Porwal 70 | 71 | 72 | -------------------------------------------------------------------------------- /resume_ranker.py: -------------------------------------------------------------------------------- 1 | import spacy 2 | import PyPDF2 3 | from sklearn.feature_extraction.text import TfidfVectorizer 4 | from sklearn.metrics.pairwise import cosine_similarity 5 | import re 6 | import csv 7 | 8 | csv_filename = "ranked_resumes.csv" 9 | 10 | # Load spaCy NER model 11 | nlp = spacy.load("en_core_web_sm") 12 | 13 | # Sample job description 14 | job_description = "NLP Specialist: Develop and implement NLP algorithms. Proficiency in Python, NLP libraries, and ML frameworks required." 15 | 16 | # List of resume PDF file paths 17 | resume_paths = ["resume1.pdf", "resume2.pdf", "resume3.pdf"] # Add more file paths here 18 | 19 | # Extract text from PDFs 20 | def extract_text_from_pdf(pdf_path): 21 | with open(pdf_path, "rb") as pdf_file: 22 | pdf_reader = PyPDF2.PdfReader(pdf_file) 23 | text = "" 24 | for page in pdf_reader.pages: 25 | text += page.extract_text() 26 | return text 27 | 28 | # Extract emails and names using spaCy NER 29 | def extract_entities(text): 30 | # Extract emails using regular expression 31 | emails = re.findall(r'\S+@\S+', text) 32 | # Extract names using a simple pattern (assuming "First Last" format) 33 | names = re.findall(r'^([A-Z][a-z]+)\s+([A-Z][a-z]+)', text) 34 | if names: 35 | names = [" ".join(names[0])] 36 | 37 | return emails, names 38 | 39 | 40 | # Extract job description features using TF-IDF 41 | tfidf_vectorizer = TfidfVectorizer() 42 | job_desc_vector = tfidf_vectorizer.fit_transform([job_description]) 43 | 44 | # Rank resumes based on similarity 45 | ranked_resumes = [] 46 | for resume_path in resume_paths: 47 | resume_text = extract_text_from_pdf(resume_path) 48 | emails, names = extract_entities(resume_text) 49 | resume_vector = tfidf_vectorizer.transform([resume_text]) 50 | similarity = cosine_similarity(job_desc_vector, resume_vector)[0][0] 51 | ranked_resumes.append((names, emails, similarity)) 52 | 53 | # Sort resumes by similarity score 54 | ranked_resumes.sort(key=lambda x: x[2], reverse=True) 55 | 56 | # Display ranked resumes with emails and names 57 | for rank, (names, emails, similarity) in enumerate(ranked_resumes, start=1): 58 | print(f"Rank {rank}: Names: {names}, Emails: {emails}, Similarity: {similarity:.2f}") 59 | 60 | with open(csv_filename, "w", newline="") as csvfile: 61 | csv_writer = csv.writer(csvfile) 62 | csv_writer.writerow(["Rank", "Name", "Email", "Similarity"]) 63 | 64 | for rank, (names, emails, similarity) in enumerate(ranked_resumes, start=1): 65 | name = names[0] if names else "N/A" 66 | email = emails[0] if emails else "N/A" 67 | csv_writer.writerow([rank, name, email, similarity]) -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | /* Reset some default styles */ 2 | body, 3 | h1, 4 | h2, 5 | p, 6 | table, 7 | th, 8 | td { 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | 14 | 15 | /* Light mode styles */ 16 | body:not(.dark-mode) { 17 | background-color: #ffffff; 18 | color: #000000; 19 | } 20 | 21 | /* Dark mode styles */ 22 | body.dark-mode { 23 | background-color: #1a1a1a; 24 | color: #ffffff; 25 | } 26 | 27 | /* Dark mode toggle styles */ 28 | #dark-mode-toggle-label { 29 | position: relative; 30 | display: inline-block; 31 | width: 60px; 32 | height: 34px; 33 | } 34 | 35 | #dark-mode-toggle-slider { 36 | position: absolute; 37 | cursor: pointer; 38 | top: 0; 39 | left: 0; 40 | right: 0; 41 | bottom: 0; 42 | background-color: #ccc; 43 | border-radius: 34px; 44 | transition: 0.4s; 45 | } 46 | 47 | #dark-mode-toggle-slider:before { 48 | position: absolute; 49 | content: ""; 50 | height: 26px; 51 | width: 26px; 52 | left: 4px; 53 | bottom: 4px; 54 | background-color: #ffffff; 55 | border-radius: 50%; 56 | transition: 0.4s; 57 | } 58 | 59 | #dark-mode-toggle:checked+#dark-mode-toggle-slider { 60 | background-color: #2196F3; 61 | } 62 | 63 | #dark-mode-toggle:checked+#dark-mode-toggle-slider:before { 64 | transform: translateX(26px); 65 | } 66 | 67 | body.dark-mode label { 68 | color: #000000; 69 | /* Label color for dark mode */ 70 | } 71 | 72 | 73 | /* Basic styling */ 74 | body { 75 | font-family: Arial, sans-serif; 76 | background-color: #f2f2f2; 77 | color: #333; 78 | padding: 20px; 79 | transition: background-color 0.4s, color 0.4s; 80 | } 81 | 82 | h1 { 83 | margin-bottom: 20px; 84 | } 85 | 86 | /* Form styling */ 87 | form { 88 | background-color: #fff; 89 | padding: 20px; 90 | border-radius: 8px; 91 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 92 | } 93 | 94 | label, 95 | input[type="file"], 96 | textarea, 97 | input[type="submit"] { 98 | display: block; 99 | margin-bottom: 10px; 100 | } 101 | 102 | input[type="file"] { 103 | margin-top: 5px; 104 | } 105 | 106 | textarea { 107 | width: 100%; 108 | padding: 10px; 109 | border: 1px solid #ccc; 110 | border-radius: 4px; 111 | resize: vertical; 112 | } 113 | 114 | input[type="submit"] { 115 | background-color: #007bff; 116 | color: #fff; 117 | border: none; 118 | padding: 10px 15px; 119 | border-radius: 4px; 120 | cursor: pointer; 121 | } 122 | 123 | /* Table styling */ 124 | table { 125 | border-collapse: collapse; 126 | width: 100%; 127 | margin-top: 20px; 128 | } 129 | 130 | th, 131 | td { 132 | padding: 8px; 133 | text-align: left; 134 | border-bottom: 1px solid #ddd; 135 | } 136 | 137 | th { 138 | background-color: #f2f2f2; 139 | } 140 | 141 | /* Responsive layout */ 142 | @media (max-width: 768px) { 143 | form { 144 | width: 100%; 145 | padding: 15px; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Axis Bank Resume Analyzer 6 | 7 | 14 | 34 | 35 | 36 | 37 |
38 | 40 | 41 |

Axis Bank Resume Analyzer

42 |
43 | 47 | 48 |
49 | 50 | 51 |
52 | 53 | 54 |
55 | 56 |
57 |
58 | {% if results %} 59 |

Ranked Resumes:

60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {% for result in results %} 68 | 69 | 70 | 71 | 72 | 73 | 74 | {% endfor %} 75 |
RankNameEmailSimilarity in %
{{ loop.index }}{{ result[0][0] }}{{ result[1][0] }}{{ result[2] }}
76 | {% if results %} 77 |
78 | 79 | Download CSV 80 | 81 | 82 | {% endif %} 83 | 84 | {% endif %} 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | import spacy 3 | import PyPDF2 4 | from sklearn.feature_extraction.text import TfidfVectorizer 5 | from sklearn.metrics.pairwise import cosine_similarity 6 | import re 7 | import csv 8 | import os 9 | 10 | app = Flask(__name__) 11 | 12 | # Load spaCy NER model 13 | nlp = spacy.load("en_core_web_sm") 14 | 15 | # Initialize results variable 16 | results = [] 17 | 18 | # Extract text from PDFs 19 | def extract_text_from_pdf(pdf_path): 20 | with open(pdf_path, "rb") as pdf_file: 21 | pdf_reader = PyPDF2.PdfReader(pdf_file) 22 | text = "" 23 | for page in pdf_reader.pages: 24 | text += page.extract_text() 25 | return text 26 | 27 | # Extract entities using spaCy NER 28 | def extract_entities(text): 29 | emails = re.findall(r'\S+@\S+', text) 30 | names = re.findall(r'^([A-Z][a-z]+)\s+([A-Z][a-z]+)', text) 31 | if names: 32 | names = [" ".join(names[0])] 33 | return emails, names 34 | 35 | @app.route('/', methods=['GET', 'POST']) 36 | def index(): 37 | results = [] 38 | if request.method == 'POST': 39 | job_description = request.form['job_description'] 40 | resume_files = request.files.getlist('resume_files') 41 | 42 | # Create a directory for uploads if it doesn't exist 43 | if not os.path.exists("uploads"): 44 | os.makedirs("uploads") 45 | 46 | # Process uploaded resumes 47 | processed_resumes = [] 48 | for resume_file in resume_files: 49 | # Save the uploaded file 50 | resume_path = os.path.join("uploads", resume_file.filename) 51 | resume_file.save(resume_path) 52 | 53 | # Process the saved file 54 | resume_text = extract_text_from_pdf(resume_path) 55 | emails, names = extract_entities(resume_text) 56 | processed_resumes.append((names, emails, resume_text)) 57 | 58 | # TF-IDF vectorizer 59 | tfidf_vectorizer = TfidfVectorizer() 60 | job_desc_vector = tfidf_vectorizer.fit_transform([job_description]) 61 | 62 | # Rank resumes based on similarity 63 | ranked_resumes = [] 64 | for (names, emails, resume_text) in processed_resumes: 65 | resume_vector = tfidf_vectorizer.transform([resume_text]) 66 | similarity = cosine_similarity(job_desc_vector, resume_vector)[0][0] * 100 67 | ranked_resumes.append((names, emails, similarity)) 68 | 69 | # Sort resumes by similarity score 70 | ranked_resumes.sort(key=lambda x: x[2], reverse=True) 71 | 72 | results = ranked_resumes 73 | 74 | return render_template('index.html', results=results) 75 | 76 | from flask import send_file 77 | 78 | @app.route('/download_csv') 79 | def download_csv(): 80 | # Generate the CSV content 81 | csv_content = "Rank,Name,Email,Similarity\n" 82 | for rank, (names, emails, similarity) in enumerate(results, start=1): 83 | name = names[0] if names else "N/A" 84 | email = emails[0] if emails else "N/A" 85 | csv_content += f"{rank},{name},{email},{similarity}\n" 86 | 87 | # Create a temporary file to store the CSV content 88 | csv_filename = "ranked_resumes.csv" 89 | with open(csv_filename, "w") as csv_file: 90 | csv_file.write(csv_content) 91 | 92 | # Send the file for download 93 | 94 | csv_full_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), csv_filename) 95 | return send_file(csv_full_path, as_attachment=True, download_name="ranked_resumes.csv") 96 | 97 | 98 | 99 | if __name__ == '__main__': 100 | app.run(debug=True) 101 | --------------------------------------------------------------------------------