
一个做HR的朋友,她在一家互联网公司负责招聘。有个岗位放出去两周,收到了217份简历。她要一份一份打开看,判断这个候选人是不是符合基本要求:工作年限够不够、有没有相关技术栈、学历达不达标。
她那天下午跟我说:“眼睛都快看瞎了。217份简历,一份看3分钟,光初筛就要10个小时。”
我说:“你把简历打包发我,我帮你搞。”
她将信将疑地发了个压缩包过来。
用 pdfplumber 解析简历PDF,提取文本内容。然后根据岗位要求定义一套评分规则:关键词匹配、工作年限提取、学历识别。每份简历打个分,最后按分数排序输出Excel。
HR拿到排名表,从高分往低分看就行。前50名认真看,后面的大致扫一眼。
先装依赖:
pip install pdfplumber openpyxl xlsxwriter主程序:
import pdfplumber
import os
import re
import json
from pathlib import Path
import pandas as pd
from datetime import datetime
# ============ 配置区 ============
# 简历文件夹路径
RESUME_DIR = "./resumes"
# 输出文件路径
OUTPUT_FILE = "candidate_ranking.xlsx"
# 岗位评分规则(可以按需修改)
SCORING_RULES = {
# 关键词及其权重(匹配到就加分)
"keywords": {
"Python": 10,
"pandas": 8,
"数据分析": 8,
"SQL": 7,
"机器学习": 6,
"Excel": 5,
"BI": 5,
"Tableau": 5,
"PowerBI": 5,
"统计学": 4,
"数据可视化": 4,
"A/B测试": 3,
"回归分析": 3,
"ETL": 3,
},
# 工作年限评分
"experience": {
"min_years": 2, # 最低要求
"ideal_years": 5, # 理想年限
"max_score": 15, # 年限最高得分
},
# 学历评分
"education": {
"博士": 15,
"硕士": 12,
"研究生": 12,
"本科": 8,
"大专": 4,
"专科": 4,
},
# 最低入围分数
"pass_score": 30,
}
# ============ 简历解析 ============
def extract_text_from_pdf(pdf_path):
"""从PDF简历中提取全部文本"""
text = ""
try:
with pdfplumber.open(pdf_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" ✗ 解析失败 [{pdf_path}]: {e}")
return None
return text.strip()
def extract_years_of_experience(text):
"""从简历文本中提取工作年限"""
# 常见的表述方式
patterns = [
r"(\d+)\s*年(?:以上)?(?:工作)?经[验历]",
r"工作经[验历][::]\s*(\d+)\s*年",
r"(\d+)\s*年(?:以上)?(?:相关)?工作",
r"从事.*?(\d+)\s*年",
]
for pattern in patterns:
match = re.search(pattern, text)
if match:
years = int(match.group(1))
# 过滤不合理的数据
if 0 <= years<= 40:
return years
# 尝试通过工作经历的时间段来估算
date_patterns = [
r"20(\d{2})[./\-年]\d{1,2}[./\-月]?\s*[-–—至到]\s*(?:20(\d{2})|至今|今|现在)",
r"20(\d{2})\s*[-–—至到]\s*(?:20(\d{2})|至今|今|现在)",
]
max_years = 0
for pattern in date_patterns:
matches = re.findall(pattern, text)
for match in matches:
try:
start_year = int(match[0])
end_year = int(match[1]) if match[1] else (datetime.now().year-2000)
years = end_year-start_year
if 0<= years <= 40:
max_years = max(max_years, years)
except (ValueError, IndexError):
continue
return max_yearsifmax_years > 0 else None
def extract_education(text):
"""从简历文本中识别最高学历"""
# 按学历从高到低匹配
edu_levels = ["博士", "硕士", "研究生", "本科", "大专", "专科", "MBA", "EMBA"]
for edu in edu_levels:
if edu in text:
return edu
return"未识别"
def extract_basic_info(text):
"""提取基本信息(姓名、电话、邮箱)"""
info = {}
# 提取手机号
phone_match = re.search(r"1[3-9]\d{9}", text)
info["phone"] = phone_match.group() ifphone_matchelse""
# 提取邮箱
email_match = re.search(r"[\w.-]+@[\w.-]+\.\w+", text)
info["email"] = email_match.group() if email_match else""
# 姓名通常在前几行
lines = text.strip().split("\n")[:5]
# 简单策略:找最短的非空行(通常是姓名)
for line in lines:
line = line.strip()
if 2<= len(line) <= 4 and re.match(r"^[\u4e00-\u9fa5]+$", line):
info["name"] = line
break
else:
info["name"] = ""
return info
# ============ 评分逻辑 ============
def score_resume(text, rules):
"""给一份简历打分"""
scores = {}
details = []
# 1. 关键词匹配
keyword_score = 0
matched_keywords = []
text_lower = text.lower()
for keyword, weight in rules["keywords"].items():
if keyword.lower() in text_lower:
keyword_score += weight
matched_keywords.append(keyword)
scores["关键词"] = keyword_score
details.append(f"匹配关键词: {', '.join(matched_keywords) if matched_keywords else '无'}")
# 2. 工作年限评分
years = extract_years_of_experience(text)
exp_config = rules["experience"]
if years is not None:
if years >= exp_config["ideal_years"]:
exp_score = exp_config["max_score"]
elif years >= exp_config["min_years"]:
# 线性插值
ratio = (years-exp_config["min_years"]) / (exp_config["ideal_years"] -exp_config["min_years"])
exp_score = int(exp_config["max_score"] *0.5+exp_config["max_score"] *0.5*ratio)
else:
exp_score = 0
details.append(f"工作年限: {years}年")
else:
exp_score = 0
details.append("工作年限: 未识别")
scores["经验"] = exp_score
# 3. 学历评分
education = extract_education(text)
edu_score = rules["education"].get(education, 0)
scores["学历"] = edu_score
details.append(f"学历: {education}")
# 总分
total = sum(scores.values())
return {
"total_score": total,
"scores": scores,
"details": "; ".join(details),
"years": years,
"education": education,
"matched_keywords": matched_keywords,
}
# ============ 批量处理 ============
def process_all_resumes(resume_dir, rules):
"""处理文件夹中的所有简历"""
results = []
# 支持的文件格式
extensions = {".pdf"}
# 获取所有简历文件
files = [f for f in os.listdir(resume_dir)
if Path(f).suffix.lower() in extensions]
total = len(files)
print(f"找到 {total} 份简历\n")
for idx, filename in enumerate(files, 1):
filepath = os.path.join(resume_dir, filename)
print(f"[{idx}/{total}] 处理: {filename}")
# 提取文本
text = extract_text_from_pdf(filepath)
if text is None:
results.append({
"文件名": filename,
"姓名": "解析失败",
"总分": 0,
"状态": "解析失败",
})
continue
# 提取基本信息
basic_info = extract_basic_info(text)
# 评分
score_result = score_resume(text, rules)
# 判断是否入围
status = "入围"ifscore_result["total_score"] >= rules["pass_score"] else"未入围"
results.append({
"文件名": filename,
"姓名": basic_info.get("name", ""),
"手机": basic_info.get("phone", ""),
"邮箱": basic_info.get("email", ""),
"工作年限": score_result["years"] or"未识别",
"学历": score_result["education"],
"关键词分": score_result["scores"]["关键词"],
"经验分": score_result["scores"]["经验"],
"学历分": score_result["scores"]["学历"],
"总分": score_result["total_score"],
"状态": status,
"匹配关键词": ", ".join(score_result["matched_keywords"]),
"评分详情": score_result["details"],
})
print(f" → {basic_info.get('name', '?')} | "
f"{score_result['total_score']}分 | {status}")
return results
# ============ 输出报告 ============
def generate_report(results, output_path, pass_score):
"""生成候选人排名Excel"""
df = pd.DataFrame(results)
# 按总分降序排列
df = df.sort_values("总分", ascending=False).reset_index(drop=True)
df.index += 1 # 排名从1开始
df.index.name = "排名"
# 写入Excel
with pd.ExcelWriter(output_path, engine="xlsxwriter") as writer:
workbook = writer.book
# 格式定义
header_fmt = workbook.add_format({
"bold": True,
"bg_color": "#4472C4",
"font_color": "white",
"border": 1,
"align": "center",
})
pass_fmt = workbook.add_format({
"bg_color": "#C6EFCE",
"font_color": "#006100",
"border": 1,
})
fail_fmt = workbook.add_format({
"bg_color": "#FFC7CE",
"font_color": "#9C0006",
"border": 1,
})
# 写入数据
df.to_excel(writer, sheet_name="候选人排名", startrow=3)
sheet = writer.sheets["候选人排名"]
# 标题
title_fmt = workbook.add_format({"bold": True, "font_size": 16})
sheet.write(0, 0, "简历筛选报告", title_fmt)
# 统计信息
total = len(results)
passed = len([r for r in results if r["状态"] == "入围"])
sheet.write(1, 0, f"总简历数: {total} | 入围: {passed} | 入围率: {passed/total*100:.1f}% | 入围线: {pass_score}分")
sheet.write(2, 0, f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
# 写入表头
for col_num, col_name in enumerate(["排名"] +list(df.columns)):
sheet.write(3, col_num, col_name, header_fmt)
# 设置列宽
col_widths = [6, 20, 8, 14, 25, 8, 8, 8, 8, 8, 8, 8, 30, 40]
for i, width in enumerate(col_widths):
sheet.set_column(i, i, width)
# 条件格式:入围标绿,未入围标红
status_col = list(df.columns).index("状态") +1 # +1 因为排名列
for row_num in range(4, 4+len(df)):
status_value = df.iloc[row_num-4]["状态"]
fmt = pass_fmtifstatus_value == "入围"else fail_fmt
sheet.write(row_num, status_col, status_value, fmt)
# Sheet 2: 统计摘要
summary_data = {
"统计项": [
"总简历数", "入围人数", "入围率", "入围分数线",
"平均得分", "最高分", "最低分",
],
"数值": [
total,
passed,
f"{passed / total * 100:.1f}%",
pass_score,
f"{df['总分'].mean():.1f}",
df["总分"].max(),
df["总分"].min(),
],
}
summary_df = pd.DataFrame(summary_data)
summary_df.to_excel(writer, sheet_name="统计摘要", index=False)
# Sheet 3: 评分规则说明
rules_text = [
["评分维度", "规则说明", "分值范围"],
["关键词匹配", "简历中包含岗位相关关键词则加分", f"0-{sum(SCORING_RULES['keywords'].values())}"],
["工作年限", f"最低{SCORING_RULES['experience']['min_years']}年,"
f"理想{SCORING_RULES['experience']['ideal_years']}年",
f"0-{SCORING_RULES['experience']['max_score']}"],
["学历", "博士>硕士>本科>大专", f"0-{max(SCORING_RULES['education'].values())}"],
["入围线", f"总分 >= {SCORING_RULES['pass_score']} 分", ""],
]
rules_df = pd.DataFrame(rules_text[1:], columns=rules_text[0])
rules_df.to_excel(writer, sheet_name="评分规则", index=False)
print(f"\n✓ 报告已生成: {output_path}")
# ============ 主函数 ============
def main():
print("="*40)
print(" 简历自动筛选工具")
print("="*40)
print(f" 简历目录: {RESUME_DIR}")
print(f" 入围分数: {SCORING_RULES['pass_score']}")
print(f" 关键词数: {len(SCORING_RULES['keywords'])}")
print("="*40)
# 检查简历目录
if not os.path.exists(RESUME_DIR):
print(f"\n✗ 简历目录不存在: {RESUME_DIR}")
print("请创建目录并放入简历PDF文件")
return
# 处理所有简历
print()
results = process_all_resumes(RESUME_DIR, SCORING_RULES)
# 生成报告
generate_report(results, OUTPUT_FILE, SCORING_RULES["pass_score"])
# 打印前10名
print("\n"+"-"*40)
print("TOP 10 候选人:")
print("-"*40)
sorted_results = sorted(results, key=lambda x: x["总分"], reverse=True)
for i, r in enumerate(sorted_results[:10], 1):
name = r.get("姓名", "?") or"?"
print(f" {i:2d}. {name:<6s} | {r['总分']:3d}分 | "
f"{r.get('工作年限', '?')}年 | {r.get('学历', '?')} | {r['状态']}")
if __name__ == "__main__":
main()如果你想增加对Word格式简历的支持,可以加个解析函数:
from docx import Document
def extract_text_from_docx(docx_path):
"""从Word简历中提取文本"""
try:
doc = Document(docx_path)
text = "\n".join([para.text for para in doc.paragraphs if para.text.strip()])
# 也提取表格中的内容(很多简历用表格排版)
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
text += "\n"+cell.text
return text.strip()
except Exception as e:
print(f" ✗ 解析Word失败 [{docx_path}]: {e}")
return None然后在 process_all_resumes 里加上 .docx 的判断就行。
========================================
简历自动筛选工具
========================================
简历目录: ./resumes
入围分数: 30
关键词数: 14
========================================
找到 217 份简历
[1/217] 处理: 张三_数据分析师.pdf
→ 张三 | 58分 | 入围
[2/217] 处理: 李四_Python开发.pdf
→ 李四 | 45分 | 入围
[3/217] 处理: 王五_产品经理.pdf
→ 王五 | 12分 | 未入围
...
[217/217] 处理: 赵六_应届生.pdf
→ 赵六 | 8分 | 未入围
✓ 报告已生成: candidate_ranking.xlsx
----------------------------------------
TOP 10 候选人:
----------------------------------------
1. 陈某某 | 72分 | 7年 | 硕士 | 入围
2. 刘某某 | 68分 | 5年 | 本科 | 入围
3. 张某某 | 65分 | 6年 | 硕士 | 入围
4. 王某某 | 61分 | 4年 | 本科 | 入围
5. 李某某 | 58分 | 5年 | 本科 | 入围
6. 赵某某 | 55分 | 3年 | 硕士 | 入围
7. 孙某某 | 52分 | 4年 | 本科 | 入围
8. 周某某 | 50分 | 3年 | 本科 | 入围
9. 吴某某 | 48分 | 5年 | 本科 | 入围
10. 郑某某 | 45分 | 2年 | 硕士 | 入围打开生成的Excel:
217份简历,脚本跑了大概8分钟。生成了排名表,入围了83人。我那个朋友花了一下午看排名前83的简历,确认了52个进入面试。
以前她要花两天初筛,现在10分钟出结果。
“无他,惟手熟尔”!有需要的用起来!
本文分享自 Nicholas与Pypi 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!