简介:这是一份基于Python的招聘数据分析可视化系统毕业设计资料包,面向计算机相关专业毕业生及需要完成数据类课题设计的同学。资源围绕招聘数据的采集、处理与可视化展示,构建了从爬虫脚本、数据清洗分析到前端图表展示的完整闭环,可帮助读者快速理解项目结构并作为毕业设计或课程设计直接参考。压缩包共54个文件,包含14个Python源码文件、10个HTML页面、8张PNG与6张JPG图片,另有配置文件、JS/CSS前端样式、CSV数据样例和说明文档等,整体大小8.59MB,目录分工清晰,便于按模块查阅。目前已有1544人学习。资源内附爬虫主程序、分析模块、交互式可视化页面、数据样例及环境配置脚本,并包含文档说明,既可用于毕设演示,也能帮助初学者掌握招聘数据分析可视化的常见实现路径。
1. 招聘数据可视化:从爬虫抓取到图表展示的三层结构
这套毕业设计源码给我的第一印象是:它的重点并不在 ECharts 画图本身,而在于整条链路是配置驱动的。你从conf.ini/my.ini里改一个城市名或关键词,spider_main.py就会重新抓取招聘页面,落成test.csv;接着process_data.py负责清洗,analyze_data.py做聚合统计,需要批量处理时还能切到analyze_spark.py走 Spark 的 local 模式;最后server.py用 Flask 起服务,把统计结果渲染进show.html、us.html和interaction.html三个页面。它很适合两类人:一类是拿 python 数据分析与可视化做课程设计或毕业设计的在校生,另一类是没完整跑过"爬虫→清洗→分析→展示"全链路、想看看 pandas 和 Spark 各自边界的开发者。下文我按每一层拆开讲,包括参数语义、代码逻辑和常见坑位。
2. 爬虫层:conf.ini 配置驱动与 spider_main 任务编排
2.1 conf.ini 到底在配置什么
这个项目的爬虫入口并不复杂,但它的配置项设计值得先看清楚。根目录下有conf.ini、my.ini和一万.ini,本质上都是同一类配置文件,区别只是针对不同招聘网站或不同关键词组合的预置方案。我用conf.ini举例,它的典型结构如下:
[crawl] crawl_url = https://search.xx.com/jobs?keyword={keyword}&city={city} keyword = Python数据分析 city = 北京 max_page = 5 delay_seconds = 1.5 timeout = 10 output_file = data/test.csv [request] user_agent = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36这段配置的逻辑很直白:crawl_url是搜索页模板,{keyword}和{city}是占位符,运行时被真实值替换;max_page控制翻页深度,delay_seconds控制每次请求之间的间隔。项目里用configparser读取这段配置,代码大概是这样的:
import configparser config = configparser.ConfigParser() config.read("conf.ini", encoding="utf-8") crawl_url = config.get("crawl", "crawl_url") keyword = config.get("crawl", "keyword") city = config.get("crawl", "city") max_page = config.getint("crawl", "max_page") delay = config.getfloat("crawl", "delay_seconds") timeout = config.getint("crawl", "timeout") output_file = config.get("crawl", "output_file")这里我说明三个参数选择的理由:第一,用configparser而不是 YAML,是因为它属于 Python 标准库,毕设环境里不需要额外pip install,出问题的概率最低;第二,getint/getfloat能自动做类型转换,避免手写int()时因为空值抛异常;第三,delay_seconds不要设成 0,哪怕是最基础的演示,连续高频请求招聘网站也容易被限流,后面我会单独讲这个坑。
2.2 spider_main.py 的任务拼接与请求逻辑
配置文件负责"要抓什么",spider_main.py负责"怎么抓"。这个文件的流程可以拆成四步:拼接 URL、发起请求、解析页面、写 CSV。核心代码是下面这段:
import requests from bs4 import BeautifulSoup import csv import time headers = { "User-Agent": config.get("request", "user_agent"), "Referer": "https://search.xx.com/jobs", } all_rows = [] for page in range(1, max_page + 1): url = crawl_url.format(keyword=keyword, city=city) + f"&page={page}" try: resp = requests.get(url, headers=headers, timeout=timeout) resp.raise_for_status() except requests.RequestException as e: print(f"[ERROR] page {page} failed: {e}") continue soup = BeautifulSoup(resp.text, "html.parser") for item in soup.select(".job-list-item"): row = { "position": item.select_one(".job-title").text.strip(), "company": item.select_one(".company-name").text.strip(), "salary": item.select_one(".salary").text.strip(), "city": city, "experience": item.select_one(".exp").text.strip(), "education": item.select_one(".edu").text.strip(), } all_rows.append(row) time.sleep(delay) with open(output_file, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=row.keys()) writer.writeheader() writer.writerows(all_rows)这段代码有三个细节值得展开:resp.raise_for_status()会在 HTTP 状态码非 200 时主动抛异常,这样翻页失败不会静默地写入半截数据;encoding="utf-8-sig"会在 CSV 头部写入 BOM,保证 Excel 直接打开不乱码,这是 Windows 演示环境里很实用的处理;time.sleep(delay)放在每页请求之后,配合配置项里的delay_seconds,是控制请求频率的主要手段。
初次跑通时,我建议先把max_page改成 1,只抓一页验证选择器对不对,再放开翻页深度。选择器匹配不到数据时,soup.select(".job-list-item")会返回空列表,程序不会报错,但 CSV 里只有表头——这种情况先检查页面结构,而不是检查代码。
2.3 CSV 字段设计与落盘格式
爬虫层产出的test.csv字段顺序很重要,因为后续process_data.py是按列名读取的。这个项目约定的是七列:position、company、salary、city、experience、education,再加上我建议补充的publish_time。
一个容易踩的点是salary字段。爬虫层不要在这里做薪资解析,比如把"15-20K·14薪"拆成数字,这是分析层process_data.py的职责。爬虫保持原始字符串,好处是数据可回溯——如果清洗逻辑写错了,还能从 CSV 重新处理,不用重新抓网页。同样的原则也适用于experience字段,原始文本如"3-5年"、"经验不限"都原样保留。
2.4 请求失败的兜底策略
招聘网站的结构变化比想象中频繁,这个项目里爬虫的容错设计是:单页失败只打印错误日志,continue进入下一页,而不是中断整个流程。实际运行中你会遇到ConnectionError、Timeout或HTTP 403,前两个是网络波动,403基本是请求头被识别了。处理时先确认user_agent是否是真实浏览器的 UA,再看有没有必填的 Cookie,这两个检查能解决大多数403问题。
3. 数据分析层:pandas 与 Spark 双轨清洗策略
3.1 为什么同一份数据要准备两条分析路径
这是这套源码和普通 Flask 展示项目最大的区别——它把pandas路径和Spark路径同时放在了工程里。analyze_data.py走的是 pandas 的read_csv+groupby,analyze_spark.py走的是spark.read.csv+groupBy。对于几百条到几千条的招聘数据,pandas 完全够用,单线程也能在几百毫秒内完成聚合;但换个场景,比如关键词扩到几十个、城市扩到十几个,爬虫落盘的文件从一个变成十几个,Spark 的批处理价值就出来了。
毕设答辩时"数据量变大怎么办"是最容易被问到的问题,analyze_spark.py的存在就是回答这个问题的材料:它能直接读入多个 CSV 做同样的清洗和聚合,而 pandas 那边需要手动pd.concat合并所有文件。Spark 在这里用local[*]模式启动,不需要额外搭集群,演示时只是启动慢一点。
3.2 process_data.py 的字段归一化
清洗层是这套系统里代码最核心的部分。process_data.py要解决三件事:去重、薪资文本转数值、城市和学历字段归一化。下面这段是典型的清洗逻辑:
import pandas as pd df = pd.read_csv("data/test.csv", encoding="utf-8-sig") df = df.drop_duplicates(subset=["position", "company", "city"]) def parse_salary(s): if "K" not in s: return None, None low, high = s.replace("K", "").split("-")[:2] return float(low), float(high) df["salary_low"] = df["salary"].apply(lambda x: parse_salary(x)[0]) df["salary_high"] = df["salary"].apply(lambda x: parse_salary(x)[1]) df["avg_salary"] = (df["salary_low"] + df["salary_high"]) / 2 df["city"] = df["city"].str.replace("·海淀", "").replace("·朝阳", "")这里drop_duplicates的去重键是"职位+公司+城市",因为同一家公司可能在不同城市发相同职位,只看职位和公司会误删;parse_salary用先替换K再取前两段的写法,是因为部分数据会带"·14薪"这种后缀,split("-")[:2]能安全忽略后半段。清洗前后的对比可以看下表:
| 原始字段 | 清洗后字段 | 处理方式 |
|---|---|---|
| 北京·海淀 | 北京 | 正则去除区县后缀 |
| 15-20K·14薪 | salary_low=15, salary_high=20, avg=17.5 | 提取数字区间并计算均值 |
| 3-5年 | 3-5年 | 保留原文,统计时做区间映射 |
| 大专 / 本科 / 硕士 | 大专 / 本科 / 硕士 | 直接归一为标准名称 |
清洗完成后,df.to_csv("data/cleaned.csv", index=False)落一份中间结果,这样做的好处是后面调图表参数时不用反复跑爬虫和清洗,直接读这份 CSV 做调试。
3.3 analyze_data.py 的聚合口径
analyze_data.py的核心是三个统计口径:城市平均薪资、岗位数量 Top10、学历要求分布。这三个口径正好对应show.html上的三张基础图。它的聚合代码是典型的 pandas 操作:
import pandas as pd import json df = pd.read_csv("data/cleaned.csv", encoding="utf-8-sig") city_salary = ( df.groupby("city")["avg_salary"] .mean() .round(1) .sort_values(ascending=False) .reset_index() ) top_positions = ( df["position"].value_counts().head(10).reset_index() ) top_positions.columns = ["position", "count"] edu_dist = df["education"].value_counts().reset_index() edu_dist.columns = ["education", "count"] result = { "city_salary": city_salary.to_dict(orient="records"), "top_positions": top_positions.to_dict(orient="records"), "edu_dist": edu_dist.to_dict(orient="records"), } with open("data/analysis_result.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False)注意.round(1)的位置——先求均值再保留一位小数,如果先 round 再聚合,精度误差会累积。to_dict(orient="records")输出的是[{"city": "北京", "avg_salary": 18.2}]这种结构,前端拿到后几乎不需要做二次转换,可以直接塞进 ECharts 的dataset里。ensure_ascii=False保证 JSON 里中文正常显示,不转成\uXXXX转义序列。
3.4 analyze_spark.py 的批处理写法
analyze_spark.py的思路是把process_data.py里用 pandas 做的清洗,搬到 Spark DataFrame 上重做一遍:
from pyspark.sql import SparkSession from pyspark.sql.functions import col, split, avg, count spark = SparkSession.builder.appName("job_analysis").getOrCreate() df = spark.read.option("header", True).csv("data/*.csv") df = df.dropDuplicates(["position", "company", "city"]) df = df.withColumn("salary_low", split(split(col("salary"), "-")[0], "K")[0].cast("float")) df = df.withColumn("salary_high", split(split(col("salary"), "-")[1], "K")[0].cast("float")) result = df.groupBy("city").agg(avg("salary_low").alias("avg_low")) result_pd = result.toPandas() result_pd.to_json("data/spark_result.json", orient="records", force_ascii=False)这里的split嵌套比较绕,拆开看是两次切割:先按-切出薪资区间的前半段,再按K去掉单位后缀,最后cast("float")转数值。toPandas()这一步是刻意的,因为可视化层读的是 JSON 文件,统一由 pandas 结构输出,前端不直接连 Spark。演示时如果 Spark Session 启动失败,最常见的原因是本机没配JAVA_HOME,这个问题我会在最后一章单独讲。
4. 可视化层:Flask 路由、JSON 接口与 ECharts 联动
4.1 server.py 的路由设计
整个可视化层由server.py一个文件承载,用的是 Flask 而不是 Django,理由很明确:Flask 的路由到视图的映射足够直观,模板渲染语法简单,对没有 Web 开发经验的人来说几天就能上手改。它的路由规划如下表:
| 路由 | 模板 | 职责 |
|---|---|---|
/ | templates/index.html | 系统首页,项目介绍 |
/show | templates/show.html | 静态统计图表展示 |
/us | templates/us.html | 报告与说明 |
/interaction | templates/interaction.html | 交互式筛选图表 |
/spider | templates/spider.html | 爬虫运行状态展示 |
服务端读取上一章生成的analysis_result.json,并把它传给模板,核心代码大概是:
from flask import Flask, render_template, jsonify import json app = Flask(__name__) with open("data/analysis_result.json", encoding="utf-8") as f: analysis_data = json.load(f) @app.route("/show") def show(): return render_template("show.html", data=analysis_data) @app.route("/api/city_data") def city_data(): city = request.args.get("city", "北京") filtered = [x for x in analysis_data["city_salary"] if x["city"] == city] return jsonify({"status": 0, "data": filtered})这里有一个关键点:静态图表用render_template直接传数据,交互页面走/api/city_data接口重新查询。二者对比,render_template适合首屏渲染,数据量小、加载快;接口方式适合用户切换城市时局部刷新,不用重载整个页面。
4.2 JSON 接口的返回结构
/api/city_data返回的 JSON 结构对前端是透明的,硬编码为统一格式:
{ "status": 0, "data": [ { "city": "北京", "avg_salary": 18.2, "count": 120 } ] }status字段供前端判断请求是否成功,data才是业务数据。这个项目的页面模板里有不少fetch调用,JavaScript 拿到data后直接chart.setOption({...})更新图表。注意接口和页面同源,不存在跨域问题;如果后期把 Flask 分成前后端两个端口部署,才需要额外配置CORS。前端联动部分的逻辑大致是:
document.getElementById("city-select").addEventListener("change", async function (e) { const city = e.target.value; const resp = await fetch(`/api/city_data?city=${encodeURIComponent(city)}`); const result = await resp.json(); if (result.status === 0) { salaryChart.setOption({ series: [{ data: result.data.map(d => d.avg_salary) }] }); } });encodeURIComponent对中文城市名做编码是这里最容易漏的一步,如果直接拼 URL,城市名为"北京"时可能在某些浏览器下请求异常。这个交互逻辑在答辩演示时很加分,它让页面从"静态图表"变成了"可以操作的分析工具"。
4.3 interaction.html 与 show.html 的职责边界
这套源码里interaction.html是核心演示页,show.html是备选方案。一个常见的误区是试图把所有图表都塞进同一个页面,结果加载慢、渲染乱。这个项目的做法是:静态组图放show.html,交互式筛选放interaction.html,职责分离。interaction.html里除了城市筛选,还有按学历、经验年限两个下拉框,每个下拉框变化都会重新请求接口。这样做还有一层好处:如果某个图表接口报错,报错范围被限制在单页内,不会影响其他页面的正常展示。
5. 部署验证与常见坑:把毕业设计系统完整跑起来
5.1 一键与环境准备
压缩包里的install_package.bat做了三件事:安装requirements.txt里的 Python 依赖、创建data目录、按顺序执行爬虫和分析脚本。手动执行时对应的命令是:
pip install -r requirements.txt python spider_main.py python process_data.py python analyze_data.py python server.py然后浏览器访问http://127.0.0.1:5000。如果只做可视化演示而跳过爬虫,可以先用压缩包自带的test.csv直接跑process_data.py,数据已经存在,不用重复抓取。
5.2 高频踩坑排查表
| 现象 | 原因 | 处理 |
|---|---|---|
ModuleNotFoundError: pandas | 依赖没装全 | 对比requirements.txt逐个安装 |
conf.ini读取乱码 | 文件编码不是 UTF-8 | config.read("conf.ini", encoding="utf-8") |
| Flask 端口 5000 被占用 | 其他服务占用了端口 | app.run(port=5050)换端口启动 |
| 图表区域空白 | 前端拿到空数组或 key 不匹配 | 打开浏览器控制台查看接口返回值 |
| Spark Session 启动失败 | 没装 Java 或JAVA_HOME未配置 | 安装 JDK 并配置环境变量,或者跳过 Spark 路径只跑 pandas |
5.3 把这些参数改成你自己的
如果想把系统复用到其他城市或其他岗位,改三个地方就够:conf.ini里的keyword和city、爬虫页面的 CSS 选择器、server.py里的初始城市默认值。关键词改成"Java开发"、城市改成"上海"后,重新执行爬虫到分析的四条命令,图表数据会自动更新,不需要动任何前端代码。如果爬虫目标站结构变化太大,也可以把 CSV 换成手动整理的 Excel 数据,只要字段名保持一致,后面的分析展示链路完全不用改。这是这套毕业设计最大的可扩展点——爬虫、分析、展示三段解耦,改任何一段都不影响另外两段运行。
本文还有配套的精品资源,点击获取