影刀RPA 自动化简历筛选系统:HR提效实战
影刀RPA 自动化简历筛选系统:HR提效实战
作者:林焱
写在前面
招聘旺季,HR每天要看几百份简历,光靠人工筛选效率极低。影刀可以帮HR搭一套自动简历筛选系统:批量解析PDF/Word简历 → 按岗位要求评分 → 自动分类 → 生成候选人汇总表,把80%的初筛工作交给自动化。
店群矩阵自动化突破运营极限!
temu店群自动化报活动案例
一、简历解析:从PDF/Word提取信息
importosimportreimportjsonimportpdfplumber# pip install pdfplumberfromdocximportDocument# pip install python-docxfrompathlibimportPathimportpandasaspddefextract_text_from_pdf(pdf_path):"""从PDF简历提取文字"""text=""try:withpdfplumber.open(pdf_path)aspdf:forpageinpdf.pages:page_text=page.extract_text()ifpage_text:text+=page_text+"\n"exceptExceptionase:print(f"PDF解析失败:{pdf_path}-{e}")returntextdefextract_text_from_docx(docx_path):"""从Word简历提取文字"""text=""try:doc=Document(docx_path)forparaindoc.paragraphs:text+=para.text+"\n"# 提取表格中的文字fortableindoc.tables:forrowintable.rows:forcellinrow.cells:text+=cell.text+" "text+="\n"exceptExceptionase:print(f"Word解析失败:{docx_path}-{e}")returntextdefextract_resume_text(file_path):"""根据文件类型提取简历文字"""ext=Path(file_path).suffix.lower()ifext=='.pdf':returnextract_text_from_pdf(file_path)elifextin['.docx','.doc']:returnextract_text_from_docx(file_path)else:print(f"不支持的文件格式:{ext}")return""二、关键信息提取
defextract_resume_info(text,filename=""):""" 从简历文字中提取结构化信息 """info={"文件名":filename,"姓名":"","手机":"","邮箱":"","学历":"","毕业学校":"","专业":"","工作年限":"","当前公司":"","期望薪资":"","技能关键词":[],"全文":text[:500]# 保存前500字方便人工核查}# 1. 提取手机号phone_match=re.search(r'1[3-9]\d{9}',text)ifphone_match:info["手机"]=phone_match.group()# 2. 提取邮箱email_match=re.search(r'[\w\.-]+@[\w\.-]+\.\w+',text)ifemail_match:info["邮箱"]=email_match.group()# 3. 提取学历edu_patterns={"博士":r'博士',"硕士":r'硕士|研究生|MBA|MPA',"本科":r'本科|学士|大学本科',"大专":r'大专|专科|高职',}foredu_level,patterninedu_patterns.items():ifre.search(pattern,text):info["学历"]=edu_levelbreak# 4. 提取工作年限(从"X年工作经验"或计算工作起始年份)years_match=re.search(r'(\d+)\s*年.*?经[历验]|工作.*?(\d+)\s*年',text)ifyears_match:years=years_match.group(1)oryears_match.group(2)info["工作年限"]=f"{years}年"# 5. 提取期望薪资salary_match=re.search(r'期望.*?(\d+[\-~到至]\d+)[k千K]?.*?[月/年]?|(\d+[\-~到至]\d+)[k千K].*?期望',text,re.IGNORECASE)ifsalary_match:info["期望薪资"]=salary_match.group(1)orsalary_match.group(2)# 6. 提取常见技能关键词skill_keywords=[# 编程语言"Python","Java","JavaScript","TypeScript","Go","C++","C#","PHP","Swift",# 前端"Vue","React","Angular","HTML","CSS","Node.js",# 后端/数据库"MySQL","Redis","MongoDB","PostgreSQL","Spring Boot","Django","FastAPI",# 工具/平台"Git","Docker","Kubernetes","AWS","阿里云","Jenkins",# 数据/AI"机器学习","深度学习","TensorFlow","PyTorch","pandas","numpy","SQL",# 业务技能"Excel","VBA","PowerBI","Tableau","Photoshop","Figma","Axure"]found_skills=[skillforskillinskill_keywordsifskill.lower()intext.lower()]info["技能关键词"]=found_skillsreturninfo三、岗位匹配评分
defscore_resume(resume_info,job_requirements):""" 根据岗位要求对简历评分(0-100分) job_requirements示例: { "必须技能": ["Python", "SQL", "机器学习"], # 有则加分,没有则扣重分 "加分技能": ["pandas", "TensorFlow", "Docker"], # 有则加分 "最低学历": "本科", # 不满足则降为低分 "最低年限": 2, # 最少工作年限 "期望薪资范围": [15, 25], # 期望薪资范围(单位K) } """score=50# 基础分reasons=[]# 1. 必须技能检查(每个+10分,缺失-15分)required_skills=job_requirements.get("必须技能",[])forskillinrequired_skills:ifskillinresume_info.get("技能关键词",[]):score+=10reasons.append(f"+10:具备必须技能「{skill}」")else:score-=15reasons.append(f"-15:缺少必须技能「{skill}」")# 2. 加分技能(每个+5分)bonus_skills=job_requirements.get("加分技能",[])forskillinbonus_skills:ifskillinresume_info.get("技能关键词",[]):score+=5reasons.append(f"+5:具备加分技能「{skill}」")# 3. 学历检查edu_order={"大专":1,"本科":2,"硕士":3,"博士":4}min_edu=job_requirements.get("最低学历","大专")candidate_edu=resume_info.get("学历","")candidate_edu_level=edu_order.get(candidate_edu,0)required_edu_level=edu_order.get(min_edu,1)ifcandidate_edu_level>=required_edu_level:ifcandidate_edu_level>required_edu_level:score+=5# 超过要求加5分reasons.append(f"+5:学历({candidate_edu})高于要求({min_edu})")else:score-=20reasons.append(f"-20:学历({candidate_edu})不满足要求({min_edu})")# 4. 工作年限min_years=job_requirements.get("最低年限",0)years_text=resume_info.get("工作年限","")years_match=re.search(r'(\d+)',years_text)ifyears_match:years=int(years_match.group(1))ifyears>=min_years:bonus=min((years-min_years)*3,15)# 超出年限每年加3分,最多15分score+=bonusifbonus>0:reasons.append(f"+{bonus}:工作年限{years}年,超出要求{min_years}年")else:score-=10reasons.append(f"-10:工作年限{years}年,不足要求{min_years}年")# 5. 薪资范围salary_range=job_requirements.get("期望薪资范围")salary_text=resume_info.get("期望薪资","")ifsalary_rangeandsalary_text:salary_match=re.search(r'(\d+)',salary_text)ifsalary_match:candidate_salary=int(salary_match.group(1))ifcandidate_salary<=salary_range[1]:reasons.append(f"薪资期望{candidate_salary}K,在预算范围内")else:score-=5reasons.append(f"-5:薪资期望{candidate_salary}K,超出预算{salary_range[1]}K")# 分数归一化到0-100score=max(0,min(100,score))returnscore,reasonsdefclassify_candidate(score):"""根据分数分类候选人"""ifscore>=75:return"A类(强烈推荐)"elifscore>=55:return"B类(可面试)"elifscore>=40:return"C类(备选)"else:return"D类(不推荐)"四、批量处理简历
importglobdefbatch_screen_resumes(resume_folder,job_requirements,output_file=None):""" 批量筛选指定文件夹中的简历 """ifoutput_fileisNone:output_file=f"简历筛选结果_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.xlsx"# 获取所有简历文件pdf_files=glob.glob(f"{resume_folder}/**/*.pdf",recursive=True)docx_files=glob.glob(f"{resume_folder}/**/*.docx",recursive=True)all_files=pdf_files+docx_filesprint(f"共发现{len(all_files)}份简历,开始处理...")results=[]fori,file_pathinenumerate(all_files,1):filename=os.path.basename(file_path)print(f"[{i}/{len(all_files)}] 处理:{filename}")# 提取文本text=extract_resume_text(file_path)ifnottext.strip():print(f" ⚠️ 文本提取为空,跳过")continue# 提取信息resume_info=extract_resume_info(text,filename)# 评分score,reasons=score_resume(resume_info,job_requirements)category=classify_candidate(score)result={"文件名":filename,"分类":category,"综合评分":score,"姓名":resume_info.get("姓名",""),"手机":resume_info.get("手机",""),"邮箱":resume_info.get("邮箱",""),"学历":resume_info.get("学历",""),"工作年限":resume_info.get("工作年限",""),"期望薪资":resume_info.get("期望薪资",""),"技能关键词":", ".join(resume_info.get("技能关键词",[])),"评分理由":"\n".join(reasons[:5]),# 显示前5条理由"文件路径":file_path}results.append(result)# 按分数排序results.sort(key=lambdax:x["综合评分"],reverse=True)# 保存Exceldf=pd.DataFrame(results)withpd.ExcelWriter(output_file,engine='openpyxl')aswriter:# A类候选人df_a=df[df['分类'].str.startswith('A')]df_b=df[df['分类'].str.startswith('B')]df_all=df df_a.to_excel(writer,sheet_name="A类(强烈推荐)",index=False)df_b.to_excel(writer,sheet_name="B类(可面试)",index=False)df_all.to_excel(writer,sheet_name="全部汇总",index=False)# 输出统计print(f"\n=== 筛选结果统计 ===")print(f"总简历数:{len(results)}")forcategoryin['A类(强烈推荐)','B类(可面试)','C类(备选)','D类(不推荐)']:count=sum(1forrinresultsifr['分类']==category)print(f"{category}:{count}人")print(f"\n结果已保存:{output_file}")returnresults# 配置岗位要求DATA_ANALYST_REQUIREMENTS={"必须技能":["Python","SQL","pandas"],"加分技能":["机器学习","PowerBI","Tableau","numpy"],"最低学历":"本科","最低年限":2,"期望薪资范围":[15,30]# 15K-30K}# 执行批量筛选results=batch_screen_resumes("./简历库/数据分析岗/",DATA_ANALYST_REQUIREMENTS)五、踩坑记录
坑1:PDF提取效果参差不齐
扫描版PDF(图片PDF)pdfplumber提取到的是空文本。需要先判断是否是图片PDF,如果是则调用OCR(百度OCR或tesseract)先识别文字再处理。
坑2:简历信息提取精度有限
正则提取手机号、邮箱准确率较高(90%+),但学历、年限的提取容易出错(简历写法太多样化)。对于A类候选人,建议人工复核关键字段。
坑3:隐私合规问题
处理简历涉及个人信息,需要确保数据安全:不上传到公网、不留存超过招聘需要的期限、严格控制访问权限。
坑4:Word 97-2003格式(.doc)
python-docx只支持.docx格式,.doc格式需要先转换。用LibreOffice命令行批量转换:soffice --convert-to docx *.doc。
总结
简历自动筛选系统的核心价值:把HR从每天几百份简历的初筛工作中解放出来,专注于面试和评估。评分模型需要根据公司实际情况调整权重,前期可以和HR一起校准几批简历,让结果贴近真实判断标准。
署名:林焱
