Python招聘数据分析可视化系统:从爬虫到图表完整实现 简介一套基于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 里写flask、pandas、pyecharts而不写版本号几个月后新版本接口一变代码直接报错。flask3.0.0 pandas2.1.4 pyecharts2.0.5 pymysql1.1.0 requests2.31.0 beautifulsoup44.12.3 pyyaml6.0.1Python 版本建议锁定 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) ) ENGINEInnoDB; 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) ) ENGINEInnoDB;这里有几个字段设计值得给答辩老师讲清楚。url_md5是 URL 的 MD5 哈希用唯一索引天然去重比每次查原始 URL 快得多。salary_min和salary_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, ascendingFalse) df df.drop_duplicates(subset[url_md5], keepfirst) # 第二层标题、城市、薪资区间都相同判定为重复发布 before len(df) df df.drop_duplicates( subset[title, city, salary_min, salary_max], keeplast, ) print(f去重前 {before} 条去重后 {len(df)} 条) return df第二层去重keeplast保留的是最后爬取的一条这是有意为之。同一条职位在不同时间发布薪资可能调整过保留最新版本更接近市场真实情况。公司名别名合并也容易忽略“阿里巴巴”、“阿里集团”、“淘宝中国”在数据库里是三家公司简单方案是维护一张手动映射表放到config.yaml里不追求自动化。数据量方面毕设答辩有一个不成文的底线有效职位记录不低于 3000 条否则“数据分析”这六个字立不住。低于这个量级薪资中位数和城市分布都缺乏统计意义。4. 可视化图表的实现城市分布、薪资中位数与词云一次讲清4.1 选图逻辑什么维度配什么图答辩老师认这套表可视化最容易犯的错是把所有能画的图都画一遍堆出一面花花绿绿的图表墙。选图表的依据是“问题需要展示趋势、分布、占比还是关系”。招聘数据四个高频分析点有明确对应关系城市岗位量用地图薪资水平用箱线图学历要求用饼图技能关键词用词云。分析需求推荐图表选图理由常见反面做法岗位城市分布中国地图地域差异一目了然用柱状图列 30 个城市文字挤在一起各岗位薪资水平箱线图能同时看中位数、四分位和离群值用平均薪资柱状图掩盖分布形态学历门槛占比环形饼图比例结构清晰用雷达图维度太少没有几何意义JD 技能词频词云关键词权重直观用折线图表达无时间关系的词频箱线图是这组图表里的亮点它能告诉老师“算法工程师平均薪资高但低分位和高分位差距也大”比单一平均值信息量大得多。pyecharts 的 Boxplot 要求传入数据格式是“每个序列是一组原始数值列表”直接用 MySQL 查询结果时需要先按岗位分组聚合。4.2 pyecharts 输出地图与箱线图的完整代码pyecharts 2.x 的 API 和 1.x 有差异关键在于Map的data_pair接收[名称, 数值]二元组列表maptypechina需要地图文件支持。以下是岗位城市分布图的实现。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(), keylambda x: x[1], reverseTrue)[:15] c ( Map(init_optsopts.InitOpts( themeThemeType.LIGHT, width900px, height600px, )) .add( series_name岗位数量, data_pairtop_items, maptypechina, is_map_symbol_showFalse, label_optsopts.LabelOpts(is_showFalse), ) .set_global_opts( title_optsopts.TitleOpts(title招聘岗位城市分布 Top15), visualmap_optsopts.VisualMapOpts( max_max(v for _, v in top_items), is_piecewiseTrue, ), tooltip_optsopts.TooltipOpts( triggeritem, formatter{b}: {c} 个岗位, ), ) ) return c关键参数逐一说明is_piecewiseTrue让图例变成分段色块而不是连续渐变色小数据量下颜色区分更明显label_optsopts.LabelOpts(is_showFalse)隐藏所有城市名否则广东省的“深圳/广州/东莞”等标签会叠在一起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_optsopts.InitOpts(themeThemeType.INFOGRAPHIC)) .add_xaxis(names) .add_yaxis(薪资(千元/月), values) .set_global_opts( title_optsopts.TitleOpts(title各岗位薪资分布箱线图), yaxis_optsopts.AxisOpts(name单位K), legend_optsopts.LegendOpts(is_showFalse), ) ) return cpyecharts 的 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 htmlhtmlbody{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(fhtmlbody{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 210 2 * * *的五个字段分别是分钟、小时、日、月、星期几这里表示每天凌晨 2 点执行。日志重定向21必须写否则 cron 进程会在没有终端的环境下丢失错误输出。更新完成后用diff对比前一天生成的图表文件大小就可以判断爬虫是否抓到了新数据。最后给出几个高频故障的自查项连接数据库报Access denied时优先检查.env里的密码是否和 MySQL 实际密码一致图表中文乱码时确认 HTML 头部是否声明meta charsetutf-8Map地图区域空白多半是缺少地图 JSON 文件可以手动下载到pyecharts/datasets/map/目录pyecharts 渲染多个图表到同一页面时每个图表实例必须调用独立的render方法否则后面的图会覆盖前面的图。启动服务后用浏览器多刷新两次对比数据库行数是否随爬虫日志增长数字在变化就说明整条链路真实跑通了。本文还有配套的精品资源点击获取