news 2026/9/17 3:51:53

招聘数据可视化:Python爬虫到Flask图表展示的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
招聘数据可视化:Python爬虫到Flask图表展示的完整实现

简介:这是一份基于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.htmlus.htmlinteraction.html三个页面。它很适合两类人:一类是拿 python 数据分析与可视化做课程设计或毕业设计的在校生,另一类是没完整跑过"爬虫→清洗→分析→展示"全链路、想看看 pandas 和 Spark 各自边界的开发者。下文我按每一层拆开讲,包括参数语义、代码逻辑和常见坑位。

2. 爬虫层:conf.ini 配置驱动与 spider_main 任务编排

2.1 conf.ini 到底在配置什么

这个项目的爬虫入口并不复杂,但它的配置项设计值得先看清楚。根目录下有conf.inimy.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是按列名读取的。这个项目约定的是七列:positioncompanysalarycityexperienceeducation,再加上我建议补充的publish_time

一个容易踩的点是salary字段。爬虫层不要在这里做薪资解析,比如把"15-20K·14薪"拆成数字,这是分析层process_data.py的职责。爬虫保持原始字符串,好处是数据可回溯——如果清洗逻辑写错了,还能从 CSV 重新处理,不用重新抓网页。同样的原则也适用于experience字段,原始文本如"3-5年"、"经验不限"都原样保留。

2.4 请求失败的兜底策略

招聘网站的结构变化比想象中频繁,这个项目里爬虫的容错设计是:单页失败只打印错误日志,continue进入下一页,而不是中断整个流程。实际运行中你会遇到ConnectionErrorTimeoutHTTP 403,前两个是网络波动,403基本是请求头被识别了。处理时先确认user_agent是否是真实浏览器的 UA,再看有没有必填的 Cookie,这两个检查能解决大多数403问题。

3. 数据分析层:pandas 与 Spark 双轨清洗策略

3.1 为什么同一份数据要准备两条分析路径

这是这套源码和普通 Flask 展示项目最大的区别——它把pandas路径和Spark路径同时放在了工程里。analyze_data.py走的是 pandas 的read_csv+groupbyanalyze_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系统首页,项目介绍
/showtemplates/show.html静态统计图表展示
/ustemplates/us.html报告与说明
/interactiontemplates/interaction.html交互式筛选图表
/spidertemplates/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-8config.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里的keywordcity、爬虫页面的 CSS 选择器、server.py里的初始城市默认值。关键词改成"Java开发"、城市改成"上海"后,重新执行爬虫到分析的四条命令,图表数据会自动更新,不需要动任何前端代码。如果爬虫目标站结构变化太大,也可以把 CSV 换成手动整理的 Excel 数据,只要字段名保持一致,后面的分析展示链路完全不用改。这是这套毕业设计最大的可扩展点——爬虫、分析、展示三段解耦,改任何一段都不影响另外两段运行。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 3:51:05

OpenClaw配置实战手册:从文件路径到模型技能全解析

装好 OpenClaw 只是把事情做完了一半,真正的分水岭在配置。很多人启动成功后,卡在“不知道去哪改模型”“skill 装了没反应”“微信一接入就报错”这类问题上,翻遍文档也找不到靠谱答案。这篇手册不打算复述官方文档,就按我实际部…

作者头像 李华
网站建设 2026/9/17 3:50:58

SpringBoot+Vue网上点餐系统实战:从需求拆解到部署避坑全记录

网上点餐系统这个题目,在Java毕设和课设里算是常客了。但说实话,十份作品里能称得上“完整可用”的,我见过的不超过三份。大多数不是卡在CRUD写不完,就是前后端联调直接摆烂,最后搭个半成品上去答辩。这次我基于Spring…

作者头像 李华
网站建设 2026/9/17 3:50:55

CentOS 8上Redis从编译安装到彻底卸载:踩坑总结与完整操作指南

CentOS 8装Redis,按理说是特别基础的活儿。但我在公司帮同事处理过好几回Redis环境问题,发现真正折腾人的根本不是安装本身,而是yum源失效、编译版本选错、卸载不干净、卸完端口还被残留进程占着这一连串破事。今天这篇文章就专门把CentOS 8上…

作者头像 李华
网站建设 2026/9/17 3:50:19

【 ‌infrastructure】第一篇 大型互联网基础设施知识体系02

一、大禹AI柜 AC-DC PSU 基线(公开事实) 母线:48V 直流(ORV3 对照运行区约 47.5–50.5V,47.5V 可作 BBU/备电触发参考) 输入:整机柜 PowerShelf 接 AC 配电;具体单相/三相、相数按 Shelf 型号定。ORV3 1OU Shelf 可配 3φ Delta/Wye 或 3单相 冗余:N+M,模组数量可扩展…

作者头像 李华
网站建设 2026/9/17 3:50:16

【 ‌infrastructure】第二篇 云数据中心基础设施中的基础知识01

编号 一级类型 二级类型 学科 学科中的知识列表(含有软件、算法与硬件设备工程融合) 知识工程和知识建模方法 在工业界/企业界/产业界的实践和研究 关联知识和规定和标准 1 计算资源与架构 Scaling 理论 计算机体系结构、分布式系统、数学物理、复杂科学 Amdahl定…

作者头像 李华