资讯动态

招聘JD结构化分析系统:Python+Streamlit轻量级毕设方案

发布时间:2026/10/3 2:48:02 来源:尧图企业网站定制
简介这是一套面向本科毕业设计与课程大作业的Python招聘数据分析可视化系统专为计算机、数据科学及相关专业学生设计解决招聘岗位数据采集、清洗、分析与多维可视化呈现的实际问题。资源包含54个文件以36个文本类说明文档含数据说明、部署指南、实验报告等、10个JavaScript前端交互脚本、5个核心Python模块如data_collection.py、data_clean.py、app.py、1个HTML主页面及配套CSS/JS静态资源为主整体压缩包仅366KB轻量易部署。已有65人学习下载项目经助教审定、本地实测可运行评审分达95分以上涵盖从爬虫采集、Flask后端服务搭建到ECharts图表渲染的完整技术链路目录结构清晰模块职责分明附详细README与分步说明特别适合初学者快速理解Web化数据分析项目的工程组织与落地逻辑。1. 这不是又一个“爬完拉倒”的岗位分析脚本它把招聘数据从“看一眼就关掉”变成可钻、可比、可推演的业务决策入口你手头有一份刚爬下来的5000条招聘JDExcel里密密麻麻堆着“Python”“Java”“3年经验”“本科以上”……但真正想问的问题——比如“长三角地区对PyTorch经验的要求是否在半年内陡增”“深圳AI算法岗的‘熟悉Transformer’出现频次是不是已超过‘掌握CNN’”——根本没法用筛选框回答。这个毕业设计标题里的“可视化系统”核心不是画几个柱状图而是构建一个带语义解析能力的数据探针它能把原始JD文本自动拆解成技术栈、经验门槛、学历偏好、地域分布、薪资区间、岗位职能等结构化维度并支持任意组合下钻、时间切片对比、关键词共现热力追踪。适合两类人一是计算机/信息管理专业学生做毕设——它不依赖服务器部署、不硬套高大上框架纯本地PythonSQLiteStreamlit跑通全流程二是HRBP或校招负责人拿过去改几行配置就能分析自家渠道数据。源码里没藏任何黑盒模型所有NLP逻辑都用spaCy正则词典规则实现你能看清每一条“要求”是怎么被识别、归类、加权的。这不是炫技项目是能真实塞进Excel替代方案里的轻量级分析中枢。2. 从原始JD到结构化数据库文本清洗、实体抽取与字段映射的三道硬坎2.1 原始数据预处理为什么不能直接用requestsBeautifulSoup存CSV招聘网站返回的HTML里混着大量噪声职位描述中夹杂公司宣传文案、福利列表、联系方式含手机号/邮箱、重复的“投递链接”按钮、甚至广告位JS代码。如果直接soup.find_all(div, class_job-detail)后粗暴.text会得到类似这样的脏数据“【腾讯】急聘高级算法工程师深圳南山科技园五险一金年度体检免费三餐要求1. 熟悉Python/PyTorch2. 3年以上CV方向经验3. 发表过顶会论文优先。简历投递hrtencent.com勿发附件”这段文本里“深圳南山科技园”要抽成city和district“五险一金年度体检免费三餐”是福利项而非技能要求“hrtencent.com”必须过滤。常见错误是写个万能正则re.findall(rPython|Java|C\\|TensorFlow, text)——结果把“Python工程师”和“熟悉Python的测试工程师”全算作“Python需求”却漏掉“用Python写自动化脚本”这类隐式表达。我一般会先做三步清洗HTML标签剥离 段落级切分用lxml.html.fromstring(html).text_content()代替.get_text()保留换行符再按\n\n或br分割成逻辑段落噪声段落剔除定义关键词黑名单如[投递邮箱, 联系电话, 公司地址, 福利待遇, 关于我们]对每段做Jaccard相似度匹配相似度0.6的整段丢弃敏感信息脱敏用re.sub(r\d{11}, [PHONE], text)替换手机号re.sub(r\b[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Z|a-z]{2,}\b, [EMAIL], text)替换邮箱——避免后续词频统计被干扰。import re from lxml import html def clean_job_description(raw_html: str) - list: 返回清洗后的段落列表每段为纯文本 try: tree html.fromstring(raw_html) text tree.text_content() # 保留换行便于后续按段落切分 text re.sub(r\s, , text).replace( , \n) paragraphs [p.strip() for p in text.split(\n) if len(p.strip()) 20] # 黑名单过滤 noise_keywords [投递邮箱, 联系电话, 公司地址, 福利待遇, 关于我们, 工作地点, 薪资范围] cleaned [] for para in paragraphs: # 计算与黑名单词的字符重合率 overlap_ratio max([len(set(para.lower()) set(kw.lower())) / len(kw) for kw in noise_keywords], default0) if overlap_ratio 0.6: # 脱敏 para re.sub(r\d{11}, [PHONE], para) para re.sub(r\b[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Z|a-z]{2,}\b, [EMAIL], para) cleaned.append(para) return cleaned except Exception as e: return [fERROR: {str(e)}]注意len(p.strip()) 20是关键阈值——太短的段落如“岗位职责”“任职要求”往往是标题不含实质内容直接过滤能减少80%的无效实体抽取。2.2 技术栈与经验要求的双轨抽取为什么用spaCy而不用jiebaJD里“熟悉Spring Boot”和“精通Spring Cloud”语义权重不同“3年经验”和“应届生可投”是互斥条件。用jieba分词会把“Spring Boot”切成[Spring, Boot]丢失框架完整性而spaCy的en_core_web_sm模型能识别Spring Boot为PROPN专有名词且支持自定义术语词典注入。更关键的是spaCy的Matcher能写规则匹配“动词名词”结构如“掌握TensorFlow”“熟悉PyTorch”→ 归为tech_skill匹配“数字年经验”如“3年以上”“2年左右”→ 提取数字存入exp_years匹配“应届”“不限经验”“1-3年”→ 存入exp_level枚举字段Entry/Mid/Senior/Unlimited。import spacy from spacy.matcher import Matcher nlp spacy.load(en_core_web_sm) # 注入招聘领域专有词典需提前准备 custom_terms [PyTorch, TensorFlow, Spring Boot, Kubernetes, Docker, Flink] for term in custom_terms: nlp.vocab[term].is_stop False # 防止被过滤 matcher Matcher(nlp.vocab) # 规则1动词技术名词掌握/熟悉/精通/了解 框架名 pattern_skill [ {POS: VERB, LEMMA: {IN: [know, master, familiar, understand]}}, {POS: ADP, LOWER: with}, {POS: PROPN, OP: } # 匹配连续多个专有名词 ] matcher.add(TECH_SKILL, [pattern_skill]) # 规则2数字年经验 pattern_exp [ {SHAPE: d}, # 数字 {LOWER: {IN: [year, years, yr, yrs]}}, {LOWER: {IN: [experience, exp, 相关经验]}, OP: ?} ] matcher.add(EXP_REQUIREMENT, [pattern_exp]) def extract_entities(text: str) - dict: doc nlp(text) matches matcher(doc) skills, exps [], [] for match_id, start, end in matches: span doc[start:end] if nlp.vocab.strings[match_id] TECH_SKILL: # 提取动词后的技术名词跳过介词 tech_part .join([t.text for t in span if t.pos_ PROPN]) if tech_part and len(tech_part) 3: skills.append(tech_part.strip()) elif nlp.vocab.strings[match_id] EXP_REQUIREMENT: # 提取数字部分 num_str .join([t.text for t in span if t.like_num]) if num_str.isdigit(): exps.append(int(num_str)) return { tech_skills: list(set(skills)), # 去重 exp_years: max(exps) if exps else None, exp_level: classify_exp_level(exps) # 自定义函数见下文 } def classify_exp_level(exps: list) - str: if not exps: return Unlimited avg sum(exps) / len(exps) if avg 1.5: return Entry elif avg 4: return Mid else: return Senior参数说明OP: 表示匹配一个或多个连续PROPN解决“Spring Boot”“React Native”这类复合名词LEMMA用词根匹配避免“familiar/familiarity”漏判like_num比is_digit更鲁棒能捕获“3”“2-5”中的数字。3. 可视化层的设计逻辑为什么放弃Matplotlib而选PlotlyStreamlit3.1 交互式热力图用Plotly实现“点击城市→展开该市技术栈TOP10”Matplotlib静态图无法响应点击事件而招聘分析的核心诉求是下钻看到“北京岗位数最多”后必须能立刻点进去看“北京哪些技术最抢手”。Plotly的FigureWidget支持on_click回调配合Streamlit的st.session_state可实现状态联动。关键不是画图而是数据绑定逻辑当用户点击地图上的“上海”前端触发plotly_click_event后端从SQLite查出WHERE city上海的所有tech_skills再用Counter统计频次生成新柱状图。import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots def create_city_heatmap(df: pd.DataFrame): # 地理坐标映射简化版实际需geopandas city_coords { 北京: (39.90, 116.40), 上海: (31.23, 121.47), 深圳: (22.54, 114.05), 杭州: (30.27, 120.15), 成都: (30.58, 103.91), 武汉: (30.59, 114.31) } # 统计各城市岗位数 city_count df[city].value_counts().reset_index(namecount) city_count[lat] city_count[index].map(lambda x: city_coords.get(x, (0,0))[0]) city_count[lon] city_count[index].map(lambda x: city_coords.get(x, (0,0))[1]) fig px.scatter_geo( city_count, latlat, lonlon, sizecount, hover_nameindex, title全国招聘岗位地理分布气泡大小岗位数, projectionnatural earth ) fig.update_layout(height500, margin{r:0,t:50,l:0,b:0}) return fig def create_skill_bar_chart(df: pd.DataFrame, city: str None): 根据城市筛选后生成技术栈TOP10 if city: filtered df[df[city] city] else: filtered df # 展开tech_skills列表一行变多行 skills_list [] for _, row in filtered.iterrows(): for skill in row[tech_skills]: skills_list.append({skill: skill.strip(), city: row[city]}) skills_df pd.DataFrame(skills_list) top_skills skills_df[skill].value_counts().head(10).reset_index(namecount) fig px.bar( top_skills, xcount, yindex, orientationh, titlef{city or 全国}技术栈需求TOP10, labels{index: 技术栈, count: 出现频次} ) fig.update_layout(height400, yaxis{categoryorder:total ascending}) return fig逻辑说明skills_df的构造是关键——tech_skills字段存的是[Python, PyTorch, SQL]这样的列表必须用循环展开成三行才能用value_counts()准确统计。若用df.explode(tech_skills)在Streamlit中可能因缓存机制导致状态错乱显式循环更可控。3.2 Streamlit状态管理如何让“选城市→刷技能图→切时间轴”不丢上下文Streamlit默认每次交互都重跑整个脚本st.selectbox选城市后st.slider的时间选择会被重置。解决方案是用st.session_state持久化参数import streamlit as st # 初始化session state if selected_city not in st.session_state: st.session_state.selected_city 全部 if time_range not in st.session_state: st.session_state.time_range (2023, 2024) # 城市选择器绑定state cities [全部] sorted(df[city].unique().tolist()) selected_city st.selectbox( 选择城市, cities, indexcities.index(st.session_state.selected_city) ) st.session_state.selected_city selected_city # 时间范围滑块 year_range st.slider( 选择年份范围, min_value2020, max_value2024, valuest.session_state.time_range, keytime_slider ) st.session_state.time_range year_range # 主图渲染使用state中的参数 if selected_city 全部: filtered_df df[(df[year] year_range[0]) (df[year] year_range[1])] else: filtered_df df[ (df[city] selected_city) (df[year] year_range[0]) (df[year] year_range[1]) ] st.plotly_chart(create_skill_bar_chart(filtered_df, selected_city), use_container_widthTrue)避坑提示keytime_slider必须唯一否则多个slider会互相覆盖st.session_state变量名要与UI组件key一致否则赋值失效st.selectbox的index参数必须用cities.index(...)动态计算硬编码index0会导致首次加载时state未生效。4. 避坑招聘数据可视化里最常翻车的5个血泪现场4.1 现象技术栈词频统计中“Python”出现1200次“Java”仅800次但实际岗位中Java岗薪资中位数高35%原因未区分“技能要求”和“工具描述”。JD中“使用Python进行数据分析”是工具“Python后端开发”才是岗位核心技能。单纯统计所有Python出现次数会把运维、测试、数据分析岗的Python使用频次计入开发岗需求。解决在实体抽取阶段增加上下文判断。对每个匹配到的Python检查其前后3个词是否包含backend、server、django、flask等后端关键词或data、analysis、pandas等数据分析关键词分别打标role_type字段Backend/Data/DevOps。统计时按role_type分组聚合。4.2 现象地图气泡图中“西安”显示岗位数为0但原始数据里有23条西安JD原因城市名称标准化失败。“西安市”“陕西西安”“西安高新区”被当作不同城市。SQLite中WHERE city西安无法匹配city西安市。解决建立城市别名映射表。在清洗阶段调用city_alias {西安市: 西安, 陕西西安: 西安, 西安高新区: 西安, 深圳市: 深圳}统一转为标准简称。映射表存为JSON文件随源码分发方便用户自行增补。4.3 现象Streamlit本地运行报错ModuleNotFoundError: No module named plotly.graph_objects原因pip install plotly安装的是精简版缺少graph_objects子模块。常见于conda环境或旧版本pip。解决强制安装完整版pip install --force-reinstall plotly5.18.0指定稳定版本或改用conda install -c conda-forge plotly。4.4 现象爬取BOSS直聘时页面返回403 Forbidden但浏览器能正常访问原因BOSS直聘检测到非浏览器请求头。requests.get(url)默认User-Agent是python-requests/2.x被反爬拦截。解决伪造浏览器头。在headers中加入User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36并添加Accept-Language: zh-CN,zh;q0.9,en;q0.8。注意毕业设计中建议用公开数据集如Kaggle的job_postings.csv替代爬虫规避法律风险。4.5 现象SQLite数据库写入时抛出sqlite3.DatabaseError: database disk image is malformed原因程序异常退出如CtrlC导致数据库文件损坏或多个进程同时写入同一.db文件。解决① 使用with sqlite3.connect(jobs.db) as conn:确保连接自动关闭② 写入前加锁import threading; lock threading.Lock()在conn.execute()前lock.acquire()执行后lock.release()③ 定期备份shutil.copy2(jobs.db, fjobs_backup_{int(time.time())}.db)。5. 毕业设计答辩加分项用“岗位需求变迁”功能讲出业务洞察而不是罗列图表5.1 构建时间切片对比模块证明你理解“数据会说话”答辩时最容易被问“你这系统除了好看到底能解决什么问题”答案不是“能画图”而是用数据验证假设。例如针对“AI岗位是否正在向工程化下沉”这一命题设计如下对比时间轴选取2022Q1、2023Q1、2024Q1三个季度维度统计每个季度tech_skills中PyTorch与Docker的共现率即同一条JD同时含这两个词的比例结论若共现率从2022年的12%升至2024年的38%佐证“模型训练PyTorch服务部署Docker”已成为AI工程师标配印证工程化趋势。实现代码只需扩展create_skill_bar_chart函数增加time_slice参数def compare_skill_cooccurrence(df: pd.DataFrame, skill1: str, skill2: str, quarters: list): 计算两个技能在指定季度的共现率 results [] for q in quarters: # 筛选该季度数据假设df有quarter列 q_df df[df[quarter] q] # 统计同时含skill1和skill2的JD数 both_count q_df[q_df[tech_skills].apply( lambda x: skill1.lower() in [s.lower() for s in x] and skill2.lower() in [s.lower() for s in x] )].shape[0] total_count q_df.shape[0] cooccur_rate both_count / total_count if total_count 0 else 0 results.append({quarter: q, cooccur_rate: cooccur_rate}) result_df pd.DataFrame(results) fig px.line(result_df, xquarter, ycooccur_rate, titlef{skill1} {skill2} 共现率趋势, markersTrue) fig.update_yaxes(tickformat.0%) return fig # 在Streamlit中调用 st.subheader(AI工程化趋势验证) st.plotly_chart(compare_skill_cooccurrence( df, pytorch, docker, [2022Q1, 2023Q1, 2024Q1] ), use_container_widthTrue)5.2 设计“岗位需求健康度”评分卡把分析结果翻译成HR语言技术同学容易陷入“我能算什么”而答辩评委尤其是企业导师更关心“这对我有什么用”。设计一个health_score指标计算逻辑对某城市某技术栈健康度 (该技术岗位数 / 全市总岗位数) × (该技术平均薪资 / 全市平均薪资) × (该技术应届生占比 0.5)业务解读分数1.2表示“供不应求”0.8表示“人才过剩”0.8~1.2为健康区间。例如“杭州Go语言健康度1.35”结论是“杭州Go岗缺口大建议校招加大宣传”。def calculate_health_score(df: pd.DataFrame, city: str, skill: str) - float: city_total df[df[city] city].shape[0] if city_total 0: return 0 city_skill df[ (df[city] city) (df[tech_skills].apply(lambda x: skill.lower() in [s.lower() for s in x])) ] if city_skill.empty: return 0 # 岗位占比 ratio city_skill.shape[0] / city_total # 薪资溢价 city_avg_salary df[df[city] city][salary_mid].mean() skill_avg_salary city_skill[salary_mid].mean() premium skill_avg_salary / city_avg_salary if city_avg_salary 0 else 1 # 应届生友好度加0.5避免负向惩罚 fresh_ratio city_skill[city_skill[exp_level] Entry].shape[0] / city_skill.shape[0] return round(ratio * premium * (fresh_ratio 0.5), 2) # 示例杭州Go语言健康度 score calculate_health_score(df, 杭州, Go) st.metric(杭州Go语言岗位健康度, f{score}分, 1.2为紧缺0.8为饱和)我的习惯答辩PPT里只放这张评分卡截图然后说“老师您看这个0.65分——它告诉我杭州Java岗现在人才供给充足但如果我们把‘微服务架构’作为子技能单独算健康度是1.42这意味着基础Java开发不缺人但能做Spring Cloud的高级人才依然稀缺。所以校招策略不该砍Java而该强化微服务培训。”——把技术动作翻译成业务决策这才是毕设该有的深度。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑