
简介这是一份面向计算机相关专业课程设计或毕业设计场景的Python智能简历解析系统源码包聚焦简历解析与人岗匹配两大核心功能。系统通过构造合适的Prompt调用Grok-beta大模型将非结构化的简历内容抽取为结构化JSON数据匹配环节则融合语义相似度与结构化数据相似度借助google-bert/bert-base-chinese预训练模型完成岗位适配判断。压缩包共9个文件以6个Python脚本为主包含模型调用、解析调度、匹配计算等模块辅以YAML配置文件、txt停用词表和Markdown说明文档整体仅19KB目录简洁明了便于快速定位代码逻辑。目前已有75人学习下载适合想理解大模型简历解析流程或需要可运行参考实现的课程设计、毕业设计初学者。通过阅读源码可掌握Prompt编写、模型接口调用、语义匹配策略等关键环节也能为课程报告、答辩演示提供扎实的代码支撑与优化起点。1. 智能简历解析系统到底解决什么从几百份 PDF 里捞出需要的人课程设计选“简历解析”这个题材意味着你挑了一个看起来不大、做起来却很咬手的题目。简历解析要把 PDF、Word 里的非结构化文本拆成姓名、电话、学历、技能、工作经历这些字段人岗匹配再拿这些字段和岗位 JD 做比对输出一个可解释的匹配分数。这套东西能解决的真实问题是筛选成本几百份简历靠人工看不仅慢而且每个人对“合适”的判断都不一样。它适合两类读者一类是正在做 Python 课程设计、需要能演示能答辩的学生另一类是想给招聘工具做内部原型的从业者。源码结构通常不复杂真正的难点在解析的健壮性和匹配的可解释性上。2. 简历解析用 Python 把 PDF/Word 变成结构化字段的实现2.1 先做“文本抽取”不要直接去翻文件内容简历解析的第一步不是抽取字段而是统一文本来源。PDF 和 Word 的存储机制差异很大PDF 里的文字可能按字体指令存放Word 的正文则在一个文档流里。如果一上来就写字段正则你会同时面对格式和内容两个变量排查时根本分不清是文件没读对还是规则没写对。# extract.py import pdfplumber from docx import Document def extract_text(path: str) - str: 统一入口把 PDF / DOCX 导出为纯文本供后续解析和打分使用。 if path.lower().endswith(.pdf): with pdfplumber.open(path) as pdf: # 有些页面提取不到文本会返回 None用空串兜底 parts [page.extract_text() or for page in pdf.pages] return \n.join(parts) if path.lower().endswith(.docx): doc Document(path) # 只取正文段落表格里的内容可以单独再拼 return \n.join(p.text for p in doc.paragraphs) raise ValueError(仅支持 pdf / docx 格式不支持 .doc)逻辑说明pdfplumber 以页面为单位提取文本普通单栏简历可以直接用遇到多栏排版文字会串行需要把page.extract_text(x_tolerance1)这类参数调大按横坐标分组。python-docx 只能读 .docx不能碰老式 .doc这一点在避坑章展开。把“提取文本”单独做成一个函数后续调试时只要打印len(text)就能快速判断文件是否读成功。2.2 电话、邮箱、学历的规则解析字段解析最常见的做法是正则 规则表。原因是这三个字段格式高度固定不需要上模型改了也好解释。写正则时不要追求一条表达式覆盖所有情况先覆盖主流程再逐步补边界。# parser.py import re def parse_contact(text: str) - dict: result {phone: [], email: [], education: None} # 兼容 86 前缀和常见的 138-xxxx-xxxx 写法 phone_pat r(?:\?86[- ]?)?1[3-9]\d{9} result[phone] re.findall(phone_pat, text) email_pat r[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,} result[email] re.findall(email_pat, text) # 按学历等级顺序取第一个命中避免被项目经历描述干扰 edu_pat r(博士|硕士|本科|大专) m re.search(edu_pat, text) result[education] m.group(1) if m else None return result参数说明电话正则可选的(?:\?86[- ]?)?是为了吃掉国家码但findall会返回完整匹配包括前缀后续入库前需要做一次清洗统一成纯数字。邮箱正则允许点号、下划线、百分号但不允许中文字符。学历这里用re.search而不是findall取的是正文中第一次出现的高学历词如果把“要求本科以上”这类 JD 描述和简历放在同一个文本里这个顺序就很重要。课程设计阶段的字段精度做到 90% 左右足够剩下的是边界处理。2.3 技能与公司名用自定义词典 jieba而不是堆死规则技能词“Java、Spring Boot、MySQL”和公司名这类开放集合靠正则规则表会越堆越失控。常见做法是保留规则表覆盖基础字段对开放集合引入分词。先把领域词典加载进 jieba再对切分结果做集合归属判断。# skills_dict.py import jieba jieba.load_userdict(skills.txt) # 每行一个技能词可含词频 SKILLS set() with open(skills.txt, encodingutf-8) as f: for line in f: word line.strip().split()[0] # 兼容“词 词频 词性”三列写法 if word: SKILLS.add(word) def extract_skills(text: str) - list: words jieba.lcut(text) # dict.fromkeys 用来去重同时保持首次出现顺序 return list(dict.fromkeys(w for w in words if w in SKILLS))逻辑说明skills.txt里的词不一定是热词比如“Java”默认会被 jieba 切成单个字母不加入词典就永远匹配不上。自定义词典只需要把常用技术栈和课程设计项目里出现的专有名词放进去。过滤条件len(w) 1要慎用因为“C”这种单字母语言会丢而“C”长度是三不受影响。公司名抽取是同一个套路只把词典换成companies.txt。这样整个解析模块就是“文本抽取 基础正则 词典归属”三层思路清晰答辩时也容易讲。3. 人岗匹配从关键词命中到 TF-IDF 相似度的完整链路3.1 先硬性过滤再谈相似度人岗匹配不能直接把简历全文和 JD 丢给相似度算法。原因在于硬条件的权重一个人写了五年 Java但 JD 要求硕士他的经验再丰富也不应该进入候选队列。课程设计里最简单的做法是把学历和年限拆成独立判断用“硬过滤 软打分”两步走。# matcher.py EDU_LEVEL {大专: 1, 本科: 2, 硕士: 3, 博士: 4} def hard_pass(resume: dict, jd: dict) - bool: 学历和工作年限硬门槛不通过直接返回 False。 min_edu jd.get(min_edu) if min_edu and EDU_LEVEL.get(resume.get(education), 0) EDU_LEVEL[min_edu]: return False # 年限从文本中分出一个独立字段这里只做示例 min_years jd.get(min_years, 0) years resume.get(years, 0) if years min_years: return False return True逻辑说明EDU_LEVEL字典本身就是等级表把“本科在读”这种特殊值单独标记后再来做比较否则会把在读生误当成已毕业本科。年限字段课程设计阶段可以从“X年XX经验”这种正则里抽解析不到就标 0。注意硬过滤阶段不要因为缺字段就放行宁可标记“待人工确认”否则匹配结果的可信度会整体崩塌。3.2 技能命中率与 TF-IDF 余弦相似度硬过滤之后是两层软打分一层是技能命中率用来做可解释的“为什么匹配”另一层是文本相似度用来覆盖分词词典没收录的同义表达。中文文本不能直接过 sklearn 的默认分词必须先把 jieba 的结果交给TfidfVectorizer。# match_score.py import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def _tokenize(text: str) - list: return [w for w in jieba.lcut(text) if len(w) 1] def text_similarity(resume_text: str, job_text: str) - float: vec TfidfVectorizer( tokenizer_tokenize, token_patternNone, # 使用自定义 tokenizer 时必须禁用默认 pattern min_df1, sublinear_tfTrue ) matrix vec.fit_transform([resume_text, job_text]) score cosine_similarity(matrix[0:1], matrix[1:2])[0][0] return round(float(score), 4)逻辑说明token_patternNone是必须写的因为传入了自定义tokenizer后默认的正则 pattern 会报错。len(w) 1能过滤掉语气词和标点但也会丢掉独立的“C”这个缺口由技能命中分补上。sublinear_tfTrue对词频做了 log 变换防止长简历里频繁出现的“熟悉”两个词垄断权重。实际跑下来两个毫无关联的文本余弦相似度也可能有 0.2所以不要把 0.5 当成绝对合格线要结合自己的简历样本观察分布再定。3.3 三个必调参数词典覆盖、min_df、权重配比第一词典是相似度上限。JD 里写“微服务”而词典里没有jieba 会切成“微”和“服务”技能命中和 TF-IDF 同时失效。做法是把 JD 和简历合并跑一遍高频词把明显是技能词的补进skills.txt。第二min_df在两人语料下必须设 1设 2 会把所有低频技能词全部滤掉只有做批量简历分析时才有理由调成 2 以上。第三最终匹配分建议用0.7 * 技能命中率 0.3 * 文本相似度这个比例不是标准答案课程设计答辩时让两个分数同时展示在页面上比调大某一项权重更有说服力。技能命中率计算方式是“简历技能命中数 / JD 要求技能数”分母为零时置空不参与加权。4. 课程设计工程落地Flask 接口、SQLite 存储与页面展示4.1 用目录结构保住可读性不要堆进一个文件这个源码不复杂最容易翻车的地方是把上传、解析、匹配、存储全部写进一个app.py。工程拆成模块后每一段都能单独测试。常见的组织方式如下如果你的 zip 里结构不同先找入口文件app.py或main.py。resume_parser/ ├── app.py # Flask 入口与路由 ├── parser.py # 文本抽取 基础字段解析 ├── matcher.py # 硬过滤 技能命中 相似度 ├── skills.txt # 自定义词典 ├── requirements.txt ├── templates/ │ ├── index.html # 上传页 │ └── result.html # 解析结果与匹配分展示 └── uploads/ # 上传文件临时目录依赖方面用 pip 安装flask pdfplumber python-docx jieba scikit-learn即可。Python 版本按课程常用 3.9 到 3.11 都行。如果你还在用 VSCode 调试记得确认右下角解释器选的是同一个 Python 环境很多依赖装不上其实是解释器指向错了。4.2 上传接口与解析返回Flask 端接收文件、保存到 uploads、调用解析模块、返回 JSON。一个最小可用的接口如下# app.py import os from flask import Flask, request, jsonify, render_template from werkzeug.utils import secure_filename from parser import extract_text, parse_contact, extract_skills app Flask(__name__) app.config[MAX_CONTENT_LENGTH] 5 * 1024 * 1024 # 限制 5MB app.config[UPLOAD_FOLDER] uploads ALLOWED {pdf, docx} def allowed(name: str) - bool: return name.rsplit(., 1)[-1].lower() in ALLOWED app.route(/parse, methods[POST]) def parse(): f request.files.get(resume) if not f or not allowed(f.filename): return jsonify({error: 请上传 pdf 或 docx 文件}), 400 filename secure_filename(f.filename) path os.path.join(app.config[UPLOAD_FOLDER], filename) f.save(path) text extract_text(path) contact parse_contact(text) skills extract_skills(text) return jsonify({ **contact, skills: skills, raw_length: len(text) # 用于快速判断 PDF 是否解析出了文本层 }) if __name__ __main__: os.makedirs(app.config[UPLOAD_FOLDER], exist_okTrue) app.run(debugTrue, port5000)逻辑说明MAX_CONTENT_LENGTH设为 5MB 是为了防止超大 PDF 把内存打满课程演示时不会遇到但加了能让代码更完整。secure_filename会过滤掉中文文件名导致一些文件保存后变成一串英文加数字如果你要在结果页展示原文件名需要额外再保留一份f.filename否则用户传“张三的简历.pdf”下载下来是乱码名。raw_length是排查扫描版 PDF 的关键字段解析结果为空时先看它。4.3 数据库表结构设计课程设计一般都要求数据库设计。功能再小也建议拆成三张表简历表、岗位表、匹配结果表。字段设计如下表字段类型说明resumeidINTEGER PK自增主键resumenameTEXT姓名resumephoneTEXT手机号resumeemailTEXT邮箱resumeeducationTEXT学历等级词resumeskillsTEXT逗号分隔技能resumeraw_pathTEXT源文件路径resumecreated_atDATETIME创建时间jobidINTEGER PK岗位IDjobtitleTEXT岗位名称jobmin_eduTEXT最低学历要求jobrequired_skillsTEXT逗号分隔技能match_resultidINTEGER PK主键match_resultresume_idINTEGER外键match_resultjob_idINTEGER外键match_resultsimilarityREAL文本相似度match_resultskill_hitINTEGER命中技能数match_resultpass_flagINTEGER硬过滤是否通过对应的建表 SQLCREATE TABLE resume ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, phone TEXT, email TEXT, education TEXT, skills TEXT, raw_path TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE match_result ( id INTEGER PRIMARY KEY AUTOINCREMENT, resume_id INTEGER, job_id INTEGER, similarity REAL, skill_hit INTEGER, pass_flag INTEGER DEFAULT 0 );逻辑说明技能字段用逗号分隔的 TEXT 不满足第三范式但在课程设计这个规模下足够直观也方便直接展示。如果老师要求 MySQL把AUTOINCREMENT换成AUTO_INCREMENTTEXT改成VARCHAR(255)或TEXT均可。pass_flag单独存是为了在结果页清晰地展示“硬过滤失败但软分高”的边界候选人这类数据在答辩时最有分析价值。4.4 演示流程的推荐顺序很多同学喜欢现场上传一个随机简历结果翻车。稳妥的做法是准备三个文件一份完全匹配的简历、一份学历不达标的简历、一份格式混乱但字段齐全的简历。演示时先跑第一份展示解析出的字段和匹配分再跑第二份展示硬过滤逻辑生效最后用第三份展示异常处理。把三份文件放入uploads旁边的demo_cases/目录比现场找文件靠谱得多。5. 简历解析避坑指南格式、编码与字段边界六个排查点5.1 先确认运行环境再怀疑解析代码现象源码在本机跑通换个电脑就报模块找不到或者 Flask 服务起不来。原因Python 解释器不一致或依赖装到了另一个环境。解决项目根目录下执行pip install -r requirements.txt然后在app.py顶部打印print(sys.version)确认解释器路径。VSCode 里常见的问题是右下角解释器选的是全局环境而依赖装在虚拟环境。先把环境对齐再动代码这是最省时间的排查顺序。5.2 扫描版 PDF 解析结果全空不是软件 bug现象上传 PDF 后返回的raw_length为 0所有字段为空。原因PDF 分成文本型和扫描型两种扫描件本质上是一张图片pdfplumber 拿不到文本层。解决临时方案是用 OCR 工具先识别常见做法是引入 PaddleOCR 或 pytesseract 做二次识别。课程设计阶段如果不想引入额外模型就在前端提示“该文件疑似扫描件请上传文本型 PDF”。在答辩现场这个提示本身就是一个加分功能点。5.3 .doc 文件解出来全是乱码现象上传 .doc 老格式简历解析出来是一堆乱码或根本读不了。原因python-docx 只支持 .docx.doc 是 OLE 复合文档格式。解决后端在ALLOWED集合里把doc去掉只接收 .docx 和 .pdf这是最负责任的做法。如果非支持不可可以调用 LibreOffice 命令行把 .doc 转成 .docx 再解析但不要把转换逻辑塞进解析函数里否则代码耦合会很重。5.4 “本科在读”被误当成本科现象一位 2025 届应届生写“本科在读”解析结果却显示“本科”硬过滤直接通过。原因正则只匹配到了“本科”两个字忽略了“在读”修饰。解决在学历清洗函数里单独处理“在读”“肄业”等后缀把“本科在读”映射成一个独立等级值例如2.5。这样硬过滤判断时它既大于大专、又小于本科才算符合真实情况。这个细节很多源码不会做全补上它答辩很加分。5.5 通讯地址被误判成公司名或技能词现象简历尾部“北京市朝阳区”被技能词典里的“朝阳”命中导致技能列表出现奇怪内容。原因词典匹配没有上下文约束行政区名、学校名与技能词共用一套查表逻辑。解决给技能抽取加两个限制——只匹配简历的“技能/项目经历”相关区块或者维护一个停用词表把常见地址后缀“区、路、号、大厦”加入过滤。课程设计阶段更简单的方案是不解析公司名只用技能和学历避免这类歧义进入匹配环节。5.6 手机号被 86 截断正则匹配只拿到后半段现象简历里写“86 138 8888 6666”解析结果却只匹配到“13888886666”或漏掉前导 86。原因正则里[- ]?只允许单个连字符或空格而真实简历可能是“86-138-8888-6666”这种带多个分隔符的写法。解决先做文本归一化去掉常见分隔符再跑正则。import re def normalize_phone(text: str) - list: # 把 86、空格、连字符统一替换后再匹配 cleaned re.sub(r[\s-], , text) return re.findall(r1[3-9]\d{9}, cleaned)逻辑说明这一步常见于简历解析的预处理流程。统一清洗后再入库还能避免同一个手机号因格式不同被统计成两条。课程设计阶段不要在一个正则里贪多先清洗再匹配逻辑和调试都会轻松很多。6. 验证解析质量用一份“脏简历”测出系统的真实水平与其面试现场临时找简历不如提前构造一个小测试集。这里给出最简基准一份正常简历、一份含全角数字/标点的简历、一份“本科在读”边界简历。# evaluate.py from parser import parse_contact, extract_skills cases [ { name: normal, text: 张伟 男 本科 电话13800138000 熟悉Java和Spring Boot, }, { name: fullwidth, text: 张伟 男 本科 电话 熟悉Java, }, ] expect { phone: [13800138000], education: 本科, skills: [Java], } for case in cases: text case[text] contact parse_contact(text) skills extract_skills(text) print(case[name], contact, skills)不用急着追求完美先用这个脚本跑一遍你会发现两件事全角数字不会被正则命中、分词结果里技能词没进词典。全角数字的解决是在解析前做text text.replace(, 0)这类映射技能词没命中就补词典。匹配阈值建议先收集五份真实简历跑出相似度分布再取中位数作为默认值。我第一版做这类系统时就是本地用例全过、换真实简历就翻车后来养成的习惯是每次改完解析规则都跑一遍这个测试脚本再继续。希望这篇笔记能帮你在做课程设计时少走这段路。本文还有配套的精品资源点击获取