├── 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 |  4 |  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 |
41 | | Rank | 63 |Name | 64 |Similarity in % | 66 ||
|---|---|---|---|
| {{ loop.index }} | 70 |{{ result[0][0] }} | 71 |{{ result[1][0] }} | 72 |{{ result[2] }} | 73 |