首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用 Python 把简历筛选从一下午变成了10分钟

用 Python 把简历筛选从一下午变成了10分钟

作者头像
用户11081884
发布2026-07-20 20:40:20
发布2026-07-20 20:40:20
430
举报

一个做HR的朋友,她在一家互联网公司负责招聘。有个岗位放出去两周,收到了217份简历。她要一份一份打开看,判断这个候选人是不是符合基本要求:工作年限够不够、有没有相关技术栈、学历达不达标。

她那天下午跟我说:“眼睛都快看瞎了。217份简历,一份看3分钟,光初筛就要10个小时。”

我说:“你把简历打包发我,我帮你搞。”

她将信将疑地发了个压缩包过来。

思路

pdfplumber 解析简历PDF,提取文本内容。然后根据岗位要求定义一套评分规则:关键词匹配、工作年限提取、学历识别。每份简历打个分,最后按分数排序输出Excel。

HR拿到排名表,从高分往低分看就行。前50名认真看,后面的大致扫一眼。

完整代码

先装依赖:

代码语言:javascript
复制
pip install pdfplumber openpyxl xlsxwriter

主程序:

代码语言:javascript
复制
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格式简历的支持,可以加个解析函数:

代码语言:javascript
复制
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 的判断就行。

运行效果

代码语言:javascript
复制
========================================
  简历自动筛选工具
========================================
  简历目录: ./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:

  • 候选人排名 Sheet:按分数从高到低排列,入围的标绿,未入围的标红
  • 统计摘要 Sheet:总数、入围率、平均分等
  • 评分规则 Sheet:每个维度的评分标准说明

小结

217份简历,脚本跑了大概8分钟。生成了排名表,入围了83人。我那个朋友花了一下午看排名前83的简历,确认了52个进入面试。

以前她要花两天初筛,现在10分钟出结果。

“无他,惟手熟尔”!有需要的用起来!

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Nicholas与Pypi 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 思路
  • 完整代码
  • 运行效果
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档