Python招聘数据分析可视化系统:从爬虫到图表完整实现
2026/9/13 14:02:08 网站建设 项目流程

简介:一套基于Python的毕业设计招聘数据分析可视化系统源码,附带数据库脚本与文档说明,面向计算机、通信、人工智能、自动化等相关专业的学生、老师和从业者。该项目为个人毕业设计,答辩评审分达98分,所有代码均经过调试测试,确保可直接运行,既适合作为毕业设计或课程设计参考,也适合数据分析与可视化方向的入门学习和进阶提升。压缩包内共161个文件,主要包含Java后端代码、Vue前端页面、JavaScript脚本、SVG图标、SCSS样式以及SQL数据库文件、Python脚本、Maven配置等,整体仅7.11MB,目录结构清晰,便于按模块检索。内容涉及招聘数据采集、处理、分析与可视化展示,覆盖任务调度、Redis缓存、接口控制、后台管理等核心模块,并附带chromedriver等运行辅助工具。目前已有335人学习下载,学习借鉴价值较高,基础较强的开发者可在原基础上修改扩展,实现更多定制化功能。

1. Python招聘数据分析可视化是毕设的高频选题,但大多数网盘项目跑不起来

招聘数据分析可视化这几年几乎成了计算机类毕业设计的“标配方向”,原因很直接:爬虫、数据库、数据分析、前端图表一条链路覆盖了本科阶段大部分核心课程,既能挂算法又能挂工程。但真正打开一个网盘下载的“源码+数据库+文档”压缩包,常见情况是两种:数据只有几百条,画出来的饼图和地图撑不起一场答辩;或者图表全部写死,老师问一句“把筛选条件改一下重新跑”就当场卡住。这套系统要解决的,正是从原始网页文本到结构化职位表、再到可演示图表的完整闭环。适用对象主要是准备拿这个方向做毕设的学生,也适合想快速验证某个垂直岗位薪酬分布的数据分析师。整条链路没有引入重框架,一台普通笔记本就能跑通,关键是把数据质量、图表逻辑和交付文档三条线同时做扎实。

2. 招聘数据分析可视化系统的拆分与技术选型

2.1 四层结构:数据源、存储、分析、展示各司其职

先把系统按数据流拆成四层,这是做可视化项目前必须完成的设计动作。采集层负责从招聘网站拿到原始页面,存储层负责把半结构化的职位信息转成规整的二维表,分析层用 pandas 做聚合和清洗,展示层再把分析结果渲染成图表页面。四层之间只通过数据库表传递数据,爬虫崩了不影响画图,画图崩了不影响入库。

层级职责常见选型毕设为什么这么选
采集层拉取职位列表页与详情页requests + BeautifulSoupScrapy 的分布式调度对单机题目过度设计
存储层持久化职位、公司、分析快照MySQL 8.x数据高度结构化,关系查询方便
分析层清洗、聚合、统计口径计算pandas + numpy处理几千条数据内存足够
展示层图表生成与 Web 预览Flask + pyecharts比 Django 轻,pyecharts 输出 HTML 免去前端开发

这个选型对应到招聘数据分析与可视化这个题目上,原则是“够用、好解释”。答辩时最怕被问“你为什么不用 Hadoop/Spark”,直接回答“当前数据量在万级以内,单机 pandas 是成本最低的解”,比硬上分布式更能体现工程判断力。

2.2 源码目录怎么组织才像个能交付的项目

网上流传的毕设源码压缩包,多数把几十个 Python 文件平铺在一个目录里,run.py 调 models.py 再调 utils.py,路经绕得人头晕。一个合格的毕业设计源码,至少要有入口、配置、模块包、SQL 脚本、模板和文档的明确分区。

recruit-analysis/ ├── requirements.txt # 依赖清单,锁定版本 ├── config.yaml # 数据库连接与爬虫开关 ├── app.py # Flask 入口,注册路由与蓝图 ├── /analysis │ ├── etl.py # 清洗、薪资解析与聚合计算 │ └── charts.py # pyecharts 图形封装 ├── /crawler │ ├── collector.py # 请求调度与页面下载 │ └── parser.py # 从 HTML 中抽取结构化字段 ├── /sql │ └── init.sql # 建库建表脚本,可重复执行 ├── /templates │ ├── index.html # 概览页 │ └── charts.html # 图表嵌入页 ├── /static │ └── charts/ # pyecharts 渲染出的 HTML 文件 └── /doc ├── 需求文档.md ├── 数据库设计.md └── 运行说明.md

目录结构里值得注意的一点:/static/charts放的是 pyecharts 渲染结果。这个设计的好处是图表生成为静态文件后,Flask 只需要用send_from_directory就能展示,不需要在每次请求时重新计算数据,演示现场即使断网图表也能打开。

2.3 环境准备与依赖版本:requirements.txt 必须锁成能复现的

毕设答辩时老师会拿你的代码在自己电脑上跑,环境对不上是最常见的拒跑原因。requirements.txt 里写flaskpandaspyecharts而不写版本号,几个月后新版本接口一变,代码直接报错。

flask==3.0.0 pandas==2.1.4 pyecharts==2.0.5 pymysql==1.1.0 requests==2.31.0 beautifulsoup4==4.12.3 pyyaml==6.0.1

Python 版本建议锁定 3.10 或 3.11。pyecharts 2.x 系列要求 Python 3.7 以上,pandas 2.x 在 Python 3.9-3.12 都能跑,但 3.8 以下会有隐性问题。安装时用pip install -r requirements.txt一条命令装齐,比在运行说明里写“缺什么装什么”专业得多。配置数据库密码时,我一般用.env文件配合python-dotenv读取,避免把账号密码硬编码进源码。

3. 数据库设计与数据清洗:招聘信息从半结构化变成可分析的表

3.1 招聘数据怎么建模:职位表、公司表、快照表

招聘数据的核心实体是职位和公司。职位有标题、城市、薪资、学历、经验、发布时间等属性,公司有名称、行业、规模,两者是多对一关系。单独建公司表不是过度设计,因为后面做“行业薪资对比”和“公司发布量排行”时,一张扁平表会导致大量冗余。

CREATE DATABASE IF NOT EXISTS recruit_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE recruit_db; CREATE TABLE company ( id INT AUTO_INCREMENT PRIMARY KEY, company_name VARCHAR(128) NOT NULL UNIQUE, industry VARCHAR(64), scale VARCHAR(32), city VARCHAR(32), updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_company_city (city) ) ENGINE=InnoDB; CREATE TABLE job ( id INT AUTO_INCREMENT PRIMARY KEY, url_md5 CHAR(32) NOT NULL UNIQUE, title VARCHAR(128) NOT NULL, company_id INT NOT NULL, city VARCHAR(32), district VARCHAR(32), salary_min INT, salary_max INT, salary_unit ENUM('月','日','小时') DEFAULT '月', education VARCHAR(16), experience VARCHAR(32), tags VARCHAR(255), publish_date DATE, crawled_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (company_id) REFERENCES company(id), INDEX idx_job_city (city), INDEX idx_job_publish (publish_date) ) ENGINE=InnoDB;

这里有几个字段设计值得给答辩老师讲清楚。url_md5是 URL 的 MD5 哈希,用唯一索引天然去重,比每次查原始 URL 快得多。salary_minsalary_max分开存整型数值,而不是存 “10-15K·13薪” 这种原始字符串,是因为后续所有薪资聚合操作都依赖数值比较,存字符串做不了中位数和分位数。utf8mb4字符集是中文项目必须的,它能存四字节 Emoji,避免公司名里带特殊符号入库报错。

3.2 薪资文本清洗:从“10-15K·13薪”到两个整数

爬虫拿到的薪资字段五花八门:“10-15K·13薪”、“8千-1.2万/月”、“薪资面议”都混在一起。清洗逻辑要能够兼容区间、单位和附加薪资说明。

import pandas as pd import re def parse_salary_range(text: str) -> pd.Series: """把 '10-15K·13薪' 解析成 (10000, 15000) 数值对""" if pd.isna(text): return pd.Series({"salary_min": None, "salary_max": None, "salary_unit": "月"}) text = str(text).replace(",", "").replace("·", "").strip() # 匹配区间写法:数字(可含小数) + 分隔符 + 数字 + 单位(K/万) match = re.search( r"(\d+(?:\.\d+)?)\s*[-~至到]\s*(\d+(?:\.\d+)?)\s*([Kk万]?)", text, ) if not match: return pd.Series({"salary_min": None, "salary_max": None, "salary_unit": "月"}) low_str, high_str, unit = match.groups() factor = {"k": 1000, "K": 1000, "万": 10000}.get(unit, 1) low = int(float(low_str) * factor) high = int(float(high_str) * factor) # 个别页面把单位写在前面,比如 "月薪8千-1.2万" if high < low: low, high = high, low return pd.Series({"salary_min": low, "salary_max": high, "salary_unit": "月"})

该函数的正则分三段理解:(\d+(?:\.\d+)?)匹配整数或小数,[-~至到]兼容不同分隔符,([Kk万]?)捕获单位。单位转换因子是K乘 1000、乘 10000,没单位默认乘 1。对“薪资面议”这类无法解析的文本,函数返回 None,在后续聚合中统一dropna(),不要用 0 填充,否则会把薪资中位数拉偏。

3.3 数据质量的隐藏问题:重复发布与公司别名合并

招聘网站同一职位会被 HR 反复刷新置顶,爬虫抓三次可能拿到三个不同 URL 的同一条职位。只靠url_md5去重并不够,因为刷新后 URL 带上了不同的时间戳参数,需要组合去重。

def dedup_jobs(df: pd.DataFrame) -> pd.DataFrame: # 第一层:URL 完全相同的记录,保留最新抓取的一条 df = df.sort_values("crawled_at", ascending=False) df = df.drop_duplicates(subset=["url_md5"], keep="first") # 第二层:标题、城市、薪资区间都相同,判定为重复发布 before = len(df) df = df.drop_duplicates( subset=["title", "city", "salary_min", "salary_max"], keep="last", ) print(f"去重前 {before} 条,去重后 {len(df)} 条") return df

第二层去重keep="last"保留的是最后爬取的一条,这是有意为之。同一条职位在不同时间发布,薪资可能调整过,保留最新版本更接近市场真实情况。公司名别名合并也容易忽略,“阿里巴巴”、“阿里集团”、“淘宝中国”在数据库里是三家公司,简单方案是维护一张手动映射表放到config.yaml里,不追求自动化。

数据量方面,毕设答辩有一个不成文的底线:有效职位记录不低于 3000 条,否则“数据分析”这六个字立不住。低于这个量级,薪资中位数和城市分布都缺乏统计意义。

4. 可视化图表的实现:城市分布、薪资中位数与词云一次讲清

4.1 选图逻辑:什么维度配什么图,答辩老师认这套表

可视化最容易犯的错是把所有能画的图都画一遍,堆出一面花花绿绿的图表墙。选图表的依据是“问题需要展示趋势、分布、占比还是关系”。招聘数据四个高频分析点有明确对应关系:城市岗位量用地图,薪资水平用箱线图,学历要求用饼图,技能关键词用词云。

分析需求推荐图表选图理由常见反面做法
岗位城市分布中国地图地域差异一目了然用柱状图列 30 个城市,文字挤在一起
各岗位薪资水平箱线图能同时看中位数、四分位和离群值用平均薪资柱状图,掩盖分布形态
学历门槛占比环形饼图比例结构清晰用雷达图,维度太少没有几何意义
JD 技能词频词云关键词权重直观用折线图表达无时间关系的词频

箱线图是这组图表里的亮点,它能告诉老师“算法工程师平均薪资高,但低分位和高分位差距也大”,比单一平均值信息量大得多。pyecharts 的 Boxplot 要求传入数据格式是“每个序列是一组原始数值列表”,直接用 MySQL 查询结果时需要先按岗位分组聚合。

4.2 pyecharts 输出地图与箱线图的完整代码

pyecharts 2.x 的 API 和 1.x 有差异,关键在于Mapdata_pair接收[名称, 数值]二元组列表,maptype="china"需要地图文件支持。以下是岗位城市分布图的实现。

from pyecharts import options as opts from pyecharts.charts import Map from pyecharts.globals import ThemeType def make_city_map(city_count: dict) -> Map: top_items = sorted(city_count.items(), key=lambda x: x[1], reverse=True)[:15] c = ( Map(init_opts=opts.InitOpts( theme=ThemeType.LIGHT, width="900px", height="600px", )) .add( series_name="岗位数量", data_pair=top_items, maptype="china", is_map_symbol_show=False, label_opts=opts.LabelOpts(is_show=False), ) .set_global_opts( title_opts=opts.TitleOpts(title="招聘岗位城市分布 Top15"), visualmap_opts=opts.VisualMapOpts( max_=max(v for _, v in top_items), is_piecewise=True, ), tooltip_opts=opts.TooltipOpts( trigger="item", formatter="{b}: {c} 个岗位", ), ) ) return c

关键参数逐一说明:is_piecewise=True让图例变成分段色块,而不是连续渐变色,小数据量下颜色区分更明显;label_opts=opts.LabelOpts(is_show=False)隐藏所有城市名,否则广东省的“深圳/广州/东莞”等标签会叠在一起;formatter="{b}: {c} 个岗位"是 ECharts 模板字符串,{b}代表城市名,{c}代表数值。图表渲染后用c.render("static/charts/city_map.html")输出独立 HTML 文件,双击即可在浏览器打开,这也方便直接截图放进毕业论文。

箱线图的实现复杂一点,因为需要先对原始薪资数据做分组分位数计算。

from pyecharts.charts import Boxplot def make_salary_box(grouped: dict) -> Boxplot: # grouped: {"Python开发": [8, 10, 12, ...], "Java开发": [9, 11, 15, ...]} names = list(grouped.keys()) values = [v for v in grouped.values()] c = ( Boxplot(init_opts=opts.InitOpts(theme=ThemeType.INFOGRAPHIC)) .add_xaxis(names) .add_yaxis("薪资(千元/月)", values) .set_global_opts( title_opts=opts.TitleOpts(title="各岗位薪资分布箱线图"), yaxis_opts=opts.AxisOpts(name="单位:K"), legend_opts=opts.LegendOpts(is_show=False), ) ) return c

pyecharts 的 Boxplot 实际上会自动对传入的原始值列表计算 Q1、Q2、Q3、IQR 和离群值,不需要手动调用prepare_data,这一点比早期版本更省事。原始薪资建议统一除以 1000 转成 K 为单位,轴线数字更干净。

4.3 参数微调:让图表放进答辩 PPT 不糊,做成可视化大屏也有层次

到这里跑通单张图不难,真正拉开差距的是图表的呈现细节。用于答辩的整块可视化大屏通常采用“地图居左、薪资箱线图居右、学历饼图和词云放下方”的三区块布局,每张图统一宽度 900px、高度 600px,主题色一致。pyecharts 的ThemeType内置十来套主题,选定后全项目不要混用。

嵌入 Flask 模板时注意路径问题。pyecharts 生成的 HTML 依赖 ECharts 的 JS 资源,有 CDN 和本地两种模式。答辩现场如果断网,CDN 模式图表会白屏,我一般用c.render_embed()返回完整 HTML 内嵌串,再传给模板变量。Flask 侧只需要在app.py里加一行:

@app.route("/charts/city") def city_chart(): chart_html = chart_repo.get_city_map_html() # 读取静态 HTML 或动态渲染 return render_template_string( f"<!DOCTYPE html><html><body>{chart_html}</body></html>" )

render_template_string适合集成测试,正式答辩我建议把所有图表先预渲染成static/charts/下的静态 HTML,再做一个index.html用 iframe 把它们嵌到同一张大屏上,这样实现页面的路由和数据解耦,切换图表时也不用重新计算。

5. 文档说明与系统交付:从运行截图到毕业论文素材

5.1 文档说明里哪些部分不能省,否则老师默认你造数据

毕设文档直接决定评分档位,代码写得再漂亮,文档里没有数据来源说明,就会被质疑“是不是造假”。一套能站住脚的文档说明通常包含六块内容,每块对应可验证的交付物。

文档章节必需内容代码里对应物
需求分析用户角色、功能列表、数据流图爬虫任务清单、页面原型
总体设计系统架构图、技术选型理由四层架构代码结构
数据库设计ER 图、表结构说明、索引策略sql/init.sql
功能模块说明爬虫模块、分析模块、可视化模块的输入输出各模块docstring
测试记录爬虫成功率、清洗前后数据量对比、页面响应耗时日志目录里的运行日志
运行说明环境要求、启动步骤、配置项释义README.md

运行说明这一块最容易被敷衍。只用一段话写“先装依赖再跑 app.py”是不够的,老师会拿到一台新电脑按步骤执行,任何缺失的中间步骤都会导致失败。运行说明要把“创建虚拟环境——安装依赖——导入 SQL——修改 .env 数据库密码——启动 Flask——浏览器访问”每一步的命令行原样贴出来。

5.2 源码组织规范:模块解耦与可演示性

源码质量在答辩演示阶段体现得最明显。演示时老师最常提的需求是“换一个城市看看图怎么变”,如果图表代码里城市是硬编码,当场改代码重启服务就很狼狈。把筛选维度做成函数参数是底线要求,例如make_city_map(city_count)里的city_count由外层从数据库动态查询得到,老师随便说一个城市都能现场生成。

模块解耦还要注意一个反模式:在爬虫模块里直接import pandas做清洗。职责上,爬虫只负责采集和解析,清洗分析归属analysis/etl.py。耦合的代码在答辩被追问架构时会露出破绽,而分层清晰的代码可以用一句话解释边界:“采集层不关心字段含义,分析层不关心数据来源”。

5.3 一键启动脚本与 README:让老师 2 分钟内跑起来

给项目配备一键启动能力,体验感提升立竿见影。项目根目录放一个run.sh,按顺序完成建库、装依赖、启动服务三步。

#!/usr/bin/env bash set -e echo "[1/3] 初始化数据库" if [ -f ".env" ]; then export $(grep -v '^#' .env | xargs) fi mysql -h"${DB_HOST:-127.0.0.1}" -u"${DB_USER:-root}" -p"${DB_PASSWORD}" < sql/init.sql echo "[2/3] 安装 Python 依赖" python3 -m venv venv source venv/bin/activate pip install -r requirements.txt echo "[3/3] 启动服务,访问 http://127.0.0.1:5000" python app.py

脚本里的set -e表示任意命令失败立即退出,避免后面步骤用错误状态继续执行。export $(grep -v '^#' .env | xargs)是把.env文件读取为环境变量,DB_HOST:-127.0.0.1是 Shell 参数展开语法,表示该变量未设置时使用默认值。这段脚本本身就可以写进文档的“自动部署”小节,作为测试环境搭建的证据。

README 则应控制在一屏以内,包含项目简介、核心功能、技术栈、启动四步、目录结构、一份运行截图。截图必须标上拍摄日期和数据量,这是证明系统真实运行过的最直接证据。

6. 进阶:给可视化加 URL 筛选参数,用一次现场演示证明系统可动态运行

6.1 用 PATH 参数替代表单提交,让图表切换快过老师提问

表单筛选在毕设演示时有个致命问题:浏览器提交表单后如果图表重算超过 3 秒,场面会很尴尬。更稳的方案是把筛选条件直接放进 URL 路径,Flask 路由动态解析,页面秒级切换。

from flask import Flask, render_template_string app = Flask(__name__) @app.route("/charts/<city>") def charts_by_city(city): # 从数据库按城市读取聚合结果 data = query_aggregate_by_city(city) if data.empty: return f"城市 {city} 暂无数据", 404 chart_html = render_city_dashboard(data) return render_template_string(f"<html><body>{chart_html}</body></html>")

老师提到的任何一个城市,只要把 URL 从/charts/北京改成/charts/上海,刷新即可看到整组图表变化。这个技巧在答辩现场往往能换来一句实质性好评,因为它体现了系统不是写死的 demo,而是具备参数化查询能力。

6.2 数据自动更新的 crontab 调度与常见故障自查

系统轮询式更新用系统定时任务即可,不需要引入 Celery。每天凌晨两点重跑爬虫和图表生成脚本,流程写在scheduler.sh里,由 crontab 调用。

0 2 * * * cd /home/user/recruit-analysis && ./scheduler.sh >> logs/scheduler.log 2>&1

0 2 * * *的五个字段分别是分钟、小时、日、月、星期几,这里表示每天凌晨 2 点执行。日志重定向2>&1必须写,否则 cron 进程会在没有终端的环境下丢失错误输出。更新完成后用diff对比前一天生成的图表文件大小,就可以判断爬虫是否抓到了新数据。

最后给出几个高频故障的自查项:连接数据库报Access denied时优先检查.env里的密码是否和 MySQL 实际密码一致;图表中文乱码时确认 HTML 头部是否声明<meta charset="utf-8">Map地图区域空白多半是缺少地图 JSON 文件,可以手动下载到pyecharts/datasets/map/目录;pyecharts 渲染多个图表到同一页面时,每个图表实例必须调用独立的render方法,否则后面的图会覆盖前面的图。启动服务后用浏览器多刷新两次,对比数据库行数是否随爬虫日志增长,数字在变化就说明整条链路真实跑通了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询