
简介一款基于Spark与ECharts搭建的前程无忧招聘网站数据大屏分析项目面向大数据课程设计、期末大作业及可视化入门学习者。项目完整覆盖招聘数据爬虫采集、清洗统计、Spark处理到ECharts大屏展示的典型链路包含城市薪酬、岗位关键词、学历经验要求、公司类型分布等分析视角适合作为课程实践或毕业设计的参考骨架。压缩包共48个文件、约10.14MB核心包含Python爬虫脚本、Spark分析代码、JS可视化模块与HTML页面另外附有csv数据集、地图坐标文本、CSS样式及使用说明、项目说明文档整体结构清楚便于按模块查阅与二次开发。目前已有169人学习下载源码经过本地编译可正常运行评审分达95分以上难度适中且经助教老师审定配套的项目说明能辅助理解设计思路与实现细节可放心选用。1. 招聘数据大屏从爬虫到 Spark 到 ECharts 的一条完整链路几周前我需要快速验证一套数据可视化流程手里正好有一份基于 Spark ECharts 的前程无忧招聘数据分析项目。原本以为只是个普通的课程设计拆开才发现它在数据清洗、聚合统计和前端渲染之间做了非常清晰的切分非常适合用来理解一个完整的数据大屏项目是怎么落地的。项目里既包含了基于 PySpark 的分析脚本也有完整的 ECharts 大屏页面数据源来自前程无忧的岗位信息爬虫最终呈现为城市分布、薪资梯度、公司类型、经验要求等多个维度的可视化面板。对于做数据开发、想接触可视化层、或者正在准备课程设计的工程师来说这套源码的价值不在于图表有多炫而在于它展示了一套「爬取 — 清洗 — 聚合 — 可视化」的完整方法论每一层都有实际代码可以对照着跑通。如果你把这份源码当作期末作业来交它属于「本地能编译、数据能跑通、图表能展示」的完成态不需要再补功能。但如果想真正吸收建议按照本文的顺序先从数据链路入手理解再逐模块拆解 ECharts 图表的配置逻辑最后再回到爬虫和数据保鲜上。下面直接进入正题。2. Spark 数据预处理visual.ipynb 中的数据清洗与聚合逻辑2.1 任务拆解为什么招聘数据需要 Spark 来洗拿到一份招聘网站的岗位数据直接丢给 ECharts 显然不行。原始 CSV 里经常存在薪资字段格式混乱、城市写法不统一、经验年限混杂文本、公司规模缺失等情况如果不在后端做预处理前端图表就会被脏数据带偏。项目在这里选择了 Spark 作为清洗和聚合引擎并没有引入复杂的集群环境而是用 PySpark 在本地跑通全流程这是课程设计场景里非常合理的一种选择因为单机 DataFrame 操作已经能覆盖几万条岗位数据的计算需求。首先打开visual.ipynb可以看到任务的整体脉络读取数据、清洗字段、按维度聚合、导出为前端可用的 JSON 文件。下面是读取 CSV 并初始化 SparkSession 的典型代码from pyspark.sql import SparkSession from pyspark.sql.functions import col, when, regexp_replace, split spark SparkSession.builder \ .appName(QcwyAnalysis) \ .config(spark.sql.session.timeZone, Asia/Shanghai) \ .master(local[*]) \ .getOrCreate() df spark.read.csv(./data/job.csv, headerTrue, inferSchemaTrue, encodingutf-8) df.printSchema()这里master(local[*])表示使用本地全部 CPU 核心来跑对单机调试足够inferSchemaTrue让 Spark 自动推导每列类型但后续你大概率会发现某些列被推成了 string这就是数据本身的问题需要靠清洗步骤修正。读取完成后先打印 schema确认字段名与实际内容一致再决定清洗策略。2.2 薪资字段拆分与基准化处理招聘网站的薪资写法往往长这样15-20万/年、1-1.5万/月、200-300/天甚至有的只写面议。如果直接把这些字符串丢给前端做图表ECharts 的折线图或者散点图根本无法处理。项目里通常会先把薪资区间拆成下限和上限再统一成月薪这样才能进行均值计算和分段统计。df_clean df.filter(~col(salary).contains(面议)) \ .withColumn(salary_clean, regexp_replace(col(salary), 万/月, )) \ .withColumn(salary_low, split(col(salary_clean), -).getItem(0).cast(double)) \ .withColumn(salary_high, split(col(salary_clean), -).getItem(1).cast(double)) \ .withColumn(salary_avg, (col(salary_low) col(salary_high)) / 2) df_clean.select(salary, salary_low, salary_high, salary_avg).show(5, truncateFalse)这段代码的核心逻辑是把15-20万/年这类字符串先做正则替换把万/月单位标记剥掉再用 split 按-切分成上下限。乘以 10000 的操作可以根据数据实际情况补上如果原始数据本身就是以元为单位则不需要。计算出的salary_avg就是后续薪资分布图表的统计基础如果某些行 salary_avg 为 null说明原始格式超出了正则预期的范围需要回到数据源头检查。2.3 按城市、公司类型、经验维度聚合拿到干净的薪资字段后就要按大屏展示的需求做多维度聚合。项目里cityJobNum.js、cityCompanytype.js、ExperienceDegreeSalary.js这三个文件对应了三个不同的聚合结果在 notebook 中的实现思路基本一致先用 groupBy 分组再用 agg 聚合最后 toPandas 转成 Python 对象并导出为 json。下面是按城市统计岗位数量并关联城市坐标信息的典型做法from pyspark.sql.functions import count, avg city_job df_clean.groupBy(city) \ .agg(count(*).alias(job_num), avg(salary_avg).alias(avg_salary)) \ .orderBy(col(job_num).desc()) city_job_pd city_job.toPandas() city_job_pd.to_json(./visual/js/cityJobNum.js, orientrecords, force_asciiFalse)这里需要注意的是to_json输出的格式是否能直接被前端script标签引用。如果直接输出 JSON 数组前端拿到的是纯数据页面里需要用 fetch 请求读取或者手动包装成const data [...]的形式。项目里的 js 文件都是后者这种做法的好处是双击 index.html 就能打开不依赖任何本地服务这在课程设计的演示场景里非常讨巧。2.4 代码逻辑说明与参数调整建议上面两步的核心参数要注意两个地方。第一regexp_replace的正则表达式只处理了万/月一种单位如果数据里混入千/月或者万/年建议先写一个 when 条件做单位换算。第二groupBy(city)之前大概率需要做城市名归一化处理比如北京和北京市应该归为一类否则地图上的数据会分散导致某些城市显示为 0。项目里提供了cityCode.txt和BaiduMap_cityCenter.txt两个辅助文件前者用于匹配城市编码后者用于给 ECharts 地图提供城市中心点坐标这两个文件通常不需要改动只有当你要扩展新的城市时才需要追加坐标。聚合完成后建议先show()一下结果检查是否有 null 分组或者数量级异常的数据。比如avg_salary如果出现了300000这种值说明薪资单位处理环节出了问题。这个步骤虽然简单但能避免后面 ECharts 页面渲染出离谱的图表。3. ECharts 大屏搭建index.html 与多个图表组件的联动逻辑3.1 页面骨架与模块划分思路打开visual/index.html页面布局采用了典型的数据大屏网格设计顶部是标题栏中部由多个图表容器构成每个容器对应一个独立的功能模块城市岗位分布地图、薪资与经验交叉分析、公司类型占比、关键词热度等。这种布局的优点是每个图表互不干扰各自渲染时不会因为某个图表的数据加载失败而导致整个页面白屏。项目中用到了daySalary.js、ExperienceDegreeSalary.js、cityCompanytype.js、cityJobNum.js、keywordNum.js、description.js这些数据文件它们的职责边界非常清晰。页面加载数据的机制值得注意不是用 fetch 请求后端接口而是通过script标签直接把 JSON 数据文件引入到全局作用域里。这样做的好处是部署时不依赖 Tomcat 或 Nginx 这类静态服务器打开 HTML 文件就能看到效果。缺点是数据文件更新后必须刷新页面才生效无法做到实时推送但作为课程设计和数据展示项目来说这种取舍完全可以接受。3.2 中国地图与散点图的坐标绑定项目地图模块使用了 ECharts 的地图组件展示岗位在全国主要城市的数量分布。地图数据源来自cityJobNum.js其中既包含岗位数也包含薪资均值。地图的渲染依赖城市名称与地理坐标的映射ECharts 内置了 geo 坐标系但对北京市和北京这类写法特别敏感要么用注册地图时统一的城市名要么在数据层面就做归一化。项目里BaiduMap_cityCenter.txt起到的作用就是在数据层面帮 ECharts 补齐缺失的城市坐标。const cityJobData window.cityJobNum || []; const geoCoordMap {}; // BaiduMap_cityCenter 数据按行读取后存入 geoCoordMap cityJobData.forEach(item { geoCoordMap[item.city] [item.longitude, item.latitude]; }); option { tooltip: { trigger: item }, geo: { map: china, roam: true, itemStyle: { areaColor: #1a2a4a, borderColor: #3a5f8a } }, series: [{ type: effectScatter, coordinateSystem: geo, data: cityJobData.map(item ({ name: item.city, value: [item.longitude, item.latitude, item.job_num] })), symbolSize: function(val) { return Math.max(6, Math.min(30, val[2] / 50)); }, rippleEffect: { brushType: stroke } }] };这段代码的核心是把 job_num 映射成散点的大小symbolSize函数里val[2]是数值数组的第三位即岗位数量。通过Math.max和Math.min限制了散点的大小范围避免数据差距过大导致小城市点几乎看不见大城市点占满整个屏幕。effectScatter类型是带涟漪特效的动态散点视觉效果上比普通scatter更适合大屏展示。roam: true允许用户滚轮缩放和平移地图这在现场演示时非常实用可以放大某个城市重点说明。3.3 薪资与经验交叉分析柱状图加折线图双 Y 轴ExperienceDegreeSalary.js对应的是薪资和经验年限的交叉分析这类数据用双 Y 轴图表来呈现最适合左边是平均薪资右边是岗位发布数量X 轴是经验区间。ECharts 对双 Y 轴的支持靠 yAxis 数组来实现下面是一个标准的配置模式option { tooltip: { trigger: axis }, legend: { data: [平均薪资, 岗位数量], top: 10 }, grid: { left: 15%, right: 15%, bottom: 15% }, xAxis: { type: category, data: expData.map(item item.experience) }, yAxis: [ { type: value, name: 平均薪资(元), axisLine: { lineStyle: { color: #7ec7ff } } }, { type: value, name: 岗位数量, splitLine: { show: false }, axisLine: { lineStyle: { color: #ffd37a } } } ], series: [ { name: 平均薪资, type: bar, data: expData.map(item item.avg_salary), itemStyle: { color: #37a2da } }, { name: 岗位数量, type: line, yAxisIndex: 1, data: expData.map(item item.job_num), smooth: true } ] };yAxis数组中第一个对象是左轴第二个是右轴series中通过yAxisIndex: 1指定折线图使用右轴如果不指定默认用左轴。这种配置的关键点在于两个 Y 轴的量纲不能差太远如果岗位数量是百级别而薪资是万级别折线就会被压成一条直线那就要考虑使用对数轴或者把其中一个维度拆分成独立图表。项目里grid的 left 和 right 都设成了15%是为了给左右两个 Y 轴的名称留出空间。3.4 饼图和词云的数据格式与渲染cityCompanytype.js和keywordNum.js分别服务公司类型占比饼图和关键词热度图。饼图的数据格式非常固定[{ name: 民营, value: 1234 }, { name: 国企, value: 890 }]这种结构在 Spark 聚合阶段就可以直接构造不需要前端做二次处理。关键词模块用的是keywordNum.js数据格式类似前端拿到后可以配合词云插件或者柱状图来渲染如果用纯 ECharts 实现通常采用bar横向排列的方式因为词云插件需要额外引入第三方库增加页面体积和依赖风险。饼图的配置要点在每个 series 的label和labelLine上大屏场景建议把 label 显示在外部并配上引导线不然重叠严重时图表完全没法读。同时radius参数可以根据页面空间调整[30%, 70%]会渲染成环形图比实心饼图更贴合大屏视觉效果。3.5 图表间联动点击地图高亮柱状图的实现页面上图表不只是独立展示还实现了简单的联动逻辑。当地图上某个城市被点击时其他图表会同步过滤出该城市的数据。具体的做法是给地图实例绑定click事件事件回调里拿到城市名称再调用其他图表的dispatchAction或直接更新其option数据重绘。myMap.on(click, function(params) { const cityName params.name; cityBarChart.setOption({ series: [{ data: barData.filter(item item.city cityName) }] }); cityPieChart.setOption({ series: [{ data: pieData.filter(item item.city cityName) }] }); });这种联动模式的原理是每个图表实例独立维护自己的状态事件源只负责触发更新动作目标图表通过setOption全量替换局部数据。这里要注意setOption第二个参数要传true表示采用 merge 模式而不是完全替换整个 option这样才能保留样式配置只更新数据部分。项目里如果事件绑定不生效大概率是地图实例初始化时机不对需要在setOption之后再去注册事件。4. 爬虫模块拆解qcwy.py 的数据采集策略与字段设计4.1 采集策略与页面解析逻辑crawler/qcwy.py是一个面向前程无忧搜索页面的数据采集脚本目标是把搜索结果中的岗位名称、公司名称、城市、薪资、经验要求、学历要求、公司类型、发布时间等字段结构化落地到 CSV 文件。爬虫的核心技术选型是 requests 正则或者 requests BeautifulSoup项目里采用的方式更偏向正则提取和字符串切割因为前程无忧的页面结构相对固定用正则处理反而更直接。一个典型的采集过程是先构造搜索 URL携带关键词和城市参数发起 GET 请求拿到 HTML 后用正则或者选择器提取岗位列表区域分页循环直到抓满指定页数。由于这类网站基本都有反爬机制代码里往往需要设置 User-Agent 和合理的请求间隔项目里如果没有加建议自己补上一段import requests import time import csv import re import random def get_html(url): session requests.Session() session.headers.update({ 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: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Referer: https://www.51job.com/ }) resp session.get(url, timeout10) resp.encoding gbk return resp.text4.2 URL 构造与分页参数说明前程无忧的搜索 URL 结构有一定的规律不同的城市对应不同的 jobarea 参数关键词对应 keyword 参数。分页通过页码参数控制每一页的岗位数据可以通过正则表达式逐条提取。下面是构造搜索 URL 和解析列表页的代码示例def build_url(keyword, city_code, page): return ( fhttps://we.51job.com/api/job/search-pc? fapi_key51jobtimestamp0keyword{quote(keyword)} fsearchType2functionindustryjobArea{city_code} fpageIndex{page}pageSize50 )字段的提取逻辑决定了后续 Spark 清洗的复杂度。比如薪资字段可能直接存在于列表页的 HTML 属性中也可能需要进入详情页才能拿到。项目里为了控制爬取量大概率是只抓列表页因此某些字段可能存在缺失后端的 Spark 就要兜底处理 null 值。另外分页循环中设定一个合理的 sleep 间隔非常重要比如time.sleep(random.uniform(1, 3))这是用时间换稳定性的现实考量过度频繁请求会直接触发封禁。4.3 增量爬取与数据去重策略这类课程设计项目一般不会考虑增量爬取跑一次全量抓取导出 CSV 就结束了。但在实际工程场景中如果想把大屏做成有温度的展示需要隔几天更新一次数据那就必须在爬虫里加去重逻辑。一种做法是文件级去重每次爬完后读取旧 CSV以岗位的某些唯一标识为 key 做差集只把新增数据追加进去。用 pandas 处理这种场景比较高效import pandas as pd def deduplicate(old_csv, new_list, key_field): old_df pd.read_csv(old_csv, dtypestr) old_keys set(old_df[key_field].tolist()) new_df pd.DataFrame(new_list) filtered new_df[~new_df[key_field].isin(old_keys)] result pd.concat([left_df, filtered], ignore_indexTrue) result.to_csv(old_csv, indexFalse, encodingutf-8-sig) return len(filtered)去重逻辑里要特别注意编码问题CSV 文件用utf-8-sig而不是utf-8保存否则用 Excel 打开时中文会乱码。项目里的job.csv文件名比较简单但如果你是接手后做二次开发建议在 schema 里加上crawl_date字段这样数据更新的时间线就能在图表上呈现也算是一个加分项。5. 数据文件体系与前后端 json 对接的约定5.1 数据字典与字段血缘关系项目里job.csv、cityInfo.csv、data.csv、data.csv.bask等数据文件构成了完整的数据血缘链条。job.csv是爬虫产出的原始数据表cityInfo.csv是城市维表data.csv可能是中间聚合结果的备份data.csv.bask是临时备份文件这些文件的字段设计直接决定了 Spark 脚本里的清洗逻辑和 ECharts 图表能展示的维度。为了清晰地了解数据的来龙去脉可以在分析前先写一个小脚本遍历一下 CSV 每个字段的非空值比例和 unique 数量import pandas as pd df pd.read_csv(./data/job.csv, encodingutf-8-sig) for col in df.columns: non_null df[col].notna().sum() unique df[col].nunique() print(f{col}: non_null{non_null}, unique{unique}, dtype{df[col].dtype})这个检查的意义在于快速发现哪些字段适合做聚合维度哪些字段只适合做文本展示。比如城市字段的 unique 数量如果只有十几个那地图散点图就是自然的呈现方式如果经验字段有七八种写法那就需要在 Spark 阶段做 mapping 归并。字段级别的记录对后续调试和写项目文档都很有帮助。5.2 坐标数据与地图渲染的协同关系cityCode.txt和BaiduMap_cityCenter.txt这两份文件在地图渲染中扮演的角色不同。cityCode.txt提供的是城市与数字编码的映射通常用于爬虫 URL 中 jobArea 参数的构造BaiduMap_cityCenter.txt提供的是城市名与经纬度的映射用于 ECharts 图表中散点坐标的定位。两者之间的城市名必须以validCity.js作为统一的命名空间否则会出现数据里有城市名地图上却找不到对应坐标的 bug。在二次开发时如果要新增一个大屏中没有的城市需要同时修改三处爬虫的 city_code 映射、BaiduMap_cityCenter.txt的坐标以及validCity.js中的校验名单。漏掉任何一处数据要么爬不下来要么地图上渲染不出来要么前端控制台报 cant find series 的错。这种多文件协同的场景在课程设计中往往被忽视但在真实的数仓项目中其实对应了维表一致性的问题值得理解。6. 项目部署验证与踩坑手记拿到源码后建议按顺序执行这套验证流程先跑通 Spark 脚本确认聚合结果正常输出再启动静态服务加载大屏页面确认每个图表都有数据渲染最后检查爬虫入口确保后续数据更新链路是完整的。如果环境是 WindowsPySpark 启动时可能会报 winutils 相关的错误那是因为本地没有 Hadoop 运行时环境解决办法是下载对应版本的 winutils.exe 并配置HADOOP_HOME环境变量或者直接在代码里设置spark.conf.set(spark.hadoop.fs.defaultFS, file:///)绕开 HDFS 地址解析。关于 ECharts 地图不显示的问题最常见的原因是地图 JSON 没有正确注册。下面是一个兼容 ECharts 5 的注册写法// 在 html 中引入 china.js 后执行 echarts.registerMap(china, chinaJson); const chart echarts.init(document.getElementById(mapContainer)); chart.setOption(option);如果控制台报BMap is not defined那是因为页面引入了百度地图的依赖而本地没有联网。这种情况下可以把地图底图换成 ECharts 内置的 geo component完全不依赖外部地图 API。另一个高频问题是本地双击打开 HTML 时图表空白但浏览器控制台无报错这时建议用python -m http.server 8080起一个本地静态服务再访问因为部分浏览器对 file:// 协议的模块加载限制很严格。最后这份源码的价值不在于图表数量多而在于它每一个图表的数据都有真实的清洗链路做支撑。你完全可以替换爬虫的数据源把整套 Spark ECharts 的逻辑复用到其他行业的数据大屏上比如电商、教育、内容平台需要改的只是字段映射和图表类型选择。按上述流程跑一遍之后建议自己动手把其中一个 JS 数据文件替换成新的聚合结果观察图表的变化这个实验做完这套项目基本就吃透了。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。