恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Spark与ECharts的招聘数据大屏分析与可视化实现
首页
资讯中心
/
基于Spark与ECharts的招聘数据大屏分析与可视化实现
基于Spark与ECharts的招聘数据大屏分析与可视化实现
发布时间:2026/9/16 19:48:20
简介基于Spark与ECharts实现的前程无忧招聘网站数据大屏分析项目适合作为大数据课程设计或期末大作业面向已掌握Spark基础、需快速落地完整可视化方案的学习者。包内共48个文件以22个JavaScript前端脚本、7个图片、3个CSV数据文件为主并包含Python爬虫脚本、HTML页面、CSS样式、ECharts地图数据及ipynb分析笔记整体约10.14MB结构清晰便于按模块检索。已有169人学习下载源码均本地编译可运行评审分达到95分以上内容经助教审定难度适中。项目覆盖数据采集、清洗分析到可视化大屏展示的完整链路Python脚本负责前程无忧职位数据抓取与预处理JavaScript脚本承载ECharts大屏各图表交互配置CSV与地图文件提供基础数据集。随包附项目说明与使用说明可帮助理解爬虫逻辑、图表联动方式及目录结构适合用作课程设计参考或二次开发起点。1. 一张招聘数据大屏卡在数据链路上拿到“基于sparkecharts实现的前程无忧招聘网站数据大屏分析源码项目说明.zip”这个压缩包第一反应不是解压而是想清楚一个问题这项目最难的环节在哪儿很多人以为难在ECharts画图实际上真正耗时间的是数据链路——从前程无忧扒下来的职位数据是脏的薪资是1.5-2.5万/月这种字符串城市字段混杂着上海-浦东新区公司规模有的写1000-5000人有的写外资欧美。不经过Spark清洗这些数据直接进ECharts图表渲染出来的全是NaN和错位标签。这张数据大屏的完整链路是采集器爬职位数据-存入MongoDB-Spark读取并按城市、学历、薪资区间、技能标签做聚合统计-结果导出为JSON-前端用ECharts绘制地图、饼图、折线图和词云。适合谁来读想用Spark做离线分析、拿ECharts做可视化大屏交付的工程师或者准备拿这类项目做毕设、简历项目的在校生。下文按数据采集、Spark分析、大屏渲染、联调排错四个环节逐步展开每步都配可运行的代码和参数说明。2. 数据采集与存储从前程无忧到MongoDB2.1 采集器怎么写才不容易被反爬卡死前程无忧的职位搜索页在2024年前后改版后列表数据由接口返回JSON不再需要解析HTML。常见做法是直接请求接口拿result.body里的职位列表再逐个补抓详情页字段。注意Headers里必须带Referer和完整的User-Agent否则大概率拿到302跳转或验证码页。import requests import pymongo import time import random client pymongo.MongoClient(mongodb://127.0.0.1:27017/) db client[job51] collection db[position] headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.51job.com/, Accept: application/json, text/plain, */*, } def fetch_position_list(keyword, page): # 接口路径以实际抓包为准不同城市代码不同 url https://search.51job.com/list/000000,000000,0000,00,9,99,{},2,{}.json.format(keyword, page) resp requests.get(url, headersheaders, timeout10) data resp.json() return data.get(result, {}).get(body, []) for page in range(1, 30): items fetch_position_list(数据开发, page) if not items: break for item in items: collection.replace_one({jobid: item[jobid]}, item, upsertTrue) time.sleep(random.uniform(1, 3))这段代码的要点replace_one按jobid去重采集多次不会产生重复文档upsertTrue实现有则更新、无则插入。sleep必须加随机值固定间隔反而更容易触发频率限制。另外把keyword做成参数采集不同岗位时不用改函数脚本入口循环传入关键词列表即可。采集到的原始文档字段很乱包含job_name、job_area、provide_salary、company_name、company_type、degree、work_year等。这些字段名在不同接口版本里大小写不一致后面Spark读取时统一处理。2.2 存储选MongoDB还是MySQL这个阶段不需要纠结直接上MongoDB理由有三。第一职位详情里大量嵌套字段比如job_tags是数组、job_info是长文本MySQL要拆成多张表再JOIN写Spark分析时查询麻烦。第二MongoDB的BSON文档天然对应Python字典采集器写入时零转换。第三Spark官方提供了MongoDB Connectorspark.read.format(mongo)一行代码就把整个collection读成DataFrame省去写JDBC连接串的时间。MySQL不是不行但是要把薪资拆成区间、把技能标签拆出来存关联表这套动作放在采集阶段做还是放在Spark阶段做如果放在采集阶段爬虫脚本里塞一堆清洗逻辑后期调整口径得改爬虫再重跑路径太长。实践中建议采集器只做去重和最小清洗比如字段名统一成小写下划线复杂的口径转换全部丢给Spark职责单一排查问题也容易定位。2.3 采到的脏数据先做一次“入库清洗”即便采集器不做复杂清洗入库前仍要处理两类问题。一是空值职位接口里有些字段直接不返回Python端取不到键会抛KeyError二是类型错乱provide_salary有时候是字符串有时候是null。这里建议用dict.get()兜底把缺失字段统一置成空串。def clean_item(raw): item { jobid: raw.get(jobid), job_name: raw.get(job_name, ), city: raw.get(job_area, ).split(-)[0] if raw.get(job_area) else , salary: raw.get(provide_salary, ), education: raw.get(degree, ), work_year: raw.get(work_year, ), company_name: raw.get(company_name, ), company_type: raw.get(company_type, ), tags: raw.get(job_tags, ), job_info: raw.get(job_info, )[:500], update_time: time.strftime(%Y-%m-%d %H:%M:%S) } return itemcity字段的拆分尤其关键job_area返回格式是上海-浦东新区如果不拆开后续ECharts中国地图匹配省份时会全部落空。拆分之后上海-浦东新区变成上海跟地图JSON里的省份名称才能对上。这段清洗可以放进采集循环里也可以等服务跑完以后统一刷一遍推荐后者因为批量刷数可以用Spark做速度更快。3. Spark分析作业从原始职位到可视化指标3.1 用DataFrame清洗职位数据城市、薪资、学历三件套Spark版本建议用2.4以上推荐3.xDataset/DataFrame API比RDD写起来短一半代码。读取MongoDB用官方connector注意jar包版本要和Spark版本匹配否则运行时报NoSuchMethodError。maven坐标是org.mongodb.spark:mongo-spark-connector_2.12Spark 3.x对应connector 10.x版本。from pyspark.sql import SparkSession from pyspark.sql import functions as F from pyspark.sql.types import IntegerType import re spark SparkSession.builder \ .appName(job51 etl dashboard) \ .config(spark.mongodb.input.uri, mongodb://127.0.0.1:27017/job51.position) \ .config(spark.mongodb.output.uri, mongodb://127.0.0.1:27017/job51.position) \ .getOrCreate() df spark.read.format(mongo).load() df df.dropDuplicates([jobid])在读进来的DataFrame上先做去重防止采集脚本重复跑产生脏数据。dropDuplicates按jobid去重比distinct()更精准因为distinct会整行比较只要某个字段埋了脏值就可能漏掉。薪资字符串转数字是分析的重点。provide_salary的格式包括1.5-2.5万/月6-8千/月2-3.5万/年三种还有极少数面议。处理逻辑是识别单位再换算成月薪的千元数值方便后面做区间统计def parse_salary(s): if not s or 面议 in s: return 0, 0, 0 nums re.findall(r[\d.], s) if len(nums) 2: return 0, 0, 0 low, high float(nums[0]), float(nums[1]) if 万/月 in s: return int(low * 10), int(high * 10), 10000 elif 千/月 in s: return int(low), int(high), 1000 elif 万/年 in s: return int(low * 10 / 12), int(high * 10 / 12), 10000 else: return 0, 0, 0 parse_salary_udf F.udf(parse_salary, StructType([ StructField(salary_low, IntegerType()), StructField(salary_high, IntegerType()), StructField(salary_unit, IntegerType()) ])) df df.withColumn(salary_struct, parse_salary_udf(F.col(salary))) \ .withColumn(salary_low, F.col(salary_struct.salary_low)) \ .withColumn(salary_high, F.col(salary_struct.salary_high))用UDF比when().otherwise()链式判断更易读薪资格式复杂正则表达式能兜住大部分异常。单位换算成月薪千元即1.5-2.5万/月变成salary_low15, salary_high25。这样后面按区间分组时一笔15k-25k的职位可以同时算进10k-20k和20k-30k两个桶。3.2 技能词频统计与多维分组大屏上最受欢迎的是技能词云数据源是职位里的tags字段。这个字段在MongoDB里是数组Spark里读到后是一个数组列直接用explode把每个标签炸成一行再分组计数df_tag df.select( F.explode(F.split(F.col(tags), ,)).alias(tag) ).filter(F.col(tag) ! ) \ .groupBy(tag) \ .agg(F.count(*).alias(cnt)) \ .orderBy(F.desc(cnt)) \ .limit(50) df_tag.write.csv(/tmp/job51_output/tags, headerTrue)explode之前先split把tags里的逗号分隔字符串切成数组。limit(50)只保留热门词Top50词云上放太多词显示效果反而难看。结果写CSV前端再用Python或者Node起个静态服务读文件转JSON。城市维度的统计用groupBy后接pivot把学历和岗位类型放在一行里这样ECharts做堆叠柱状图时数据结构更扁平df_city df.filter(F.col(city) ! ) \ .groupBy(city, education) \ .agg(F.count(*).alias(count)) \ .groupBy(city) \ .pivot(education) \ .sum(count) \ .fillna(0)pivot会把学历字段转成多列输出格式变成city, 本科, 大专, 硕士, 学历不限前端不用嵌套遍历把列名直接映射到ECharts series里。fillna(0)把空学位组合补齐避免图表缺列。3.3 Spark提交参数和集群环境配置开发时用local模式就能跑master(local[*])但大屏项目最终要落在Yarn上定期更新数据提交命令里几个参数要注意。这里给一套常规配置单机4核8G内存十个以内的executor够用spark-submit \ --master yarn \ --deploy-mode cluster \ --executor-memory 4G \ --executor-cores 2 \ --num-executors 8 \ --driver-memory 2G \ --jars /opt/mongo-spark-connector_2.12-10.0.5.jar \ --conf spark.executor.memoryOverhead512M \ job51_etl.py参数里最容易出问题的是executor-memory和spark.executor.memoryOverhead。默认的overhead只有executor内存的10%MongoDB JDBC驱动和Spark Connector在序列化时消耗的堆外内存经常超限报Container killed by YARN for exceeding memory limits手动加512M能规避大多数情况。大数据分析的项目里处理几百MB的职位数据不需要调spark.sql.shuffle.partitions默认200个分区够调度但要注意输出文件数量会被分区数影响本地联调时建议设--conf spark.sql.shuffle.partitions8小数据量下减少小文件个数。参数取值说明--masteryarn生产环境必须走集群调度local模式只能开发调试--executor-memory4Gexecutor堆内存上限需求大时可到8G--executor-cores2每个executor的CPU核数太高会争抢内存带宽--num-executors8executor数量受集群队列资源上限约束--conf spark.sql.shuffle.partitions8小数据集下减少shuffle文件数避免大量空小文件--jarsmongo-connector...jarSpark读MongoDB的驱动本地跑也要加到--packages里跑完Spark作业检查输出目录hdfs dfs -ls /tmp/job51_output/tags确认生成part-00000文件然后再导出到前端能读取的路径。这一步不要省Spark写文件是分布式落盘的经常出现部分文件丢失但任务显示成功的情况必须检查文件数总和。4. ECharts数据大屏渲染把Spark结果变成可交互图表4.1 大屏布局分区块、分指标拿到Spark输出的JSON之后前端开始组装大屏。整体布局推荐用CSS Grid分三行顶部放标题和更新时间中间一行左右两侧放中国地图和词云中间放核心指标卡片底部一行放折线图和学历分布饼图。指标卡不要超过五个放总职位数、平均薪资、企业数量、最大需求城市、本科以上占比。大屏的核心要求是数据驱动图表初始化时请求一次数据后续通过定时器轮询或WebSocket推送更新。常规项目里定时轮询就够10分钟一次刷新逻辑放在setInterval里。ECharts的setOption是合并配置的切页签或切换城市时要注意主动清空旧数据的残留。4.2 中国地图与饼图用geoJSON源注册城市分布中国地图是这类项目最常用的图表。这里的坑在echarts 5.0以后移除了内置的地图数据必须自己引入geoJSON文件。常见做法是把项目的china.json包含中国各省份的边界坐标和名称放在src/assets/json/目录下运行时注册import * as echarts from echarts; import chinaJson from /assets/json/china.json; import axios from axios; echarts.registerMap(china, chinaJson); const chart echarts.init(document.getElementById(map)); axios.get(/api/city_distribution).then(res { const data res.data.map(item ({ name: item.city.replace(省, ).replace(市, ), value: item.count })); chart.setOption({ tooltip: { trigger: item, formatter: function (params) { return params.name : params.value 个职位; } }, visualMap: { min: 0, max: 500, inRange: { color: [#e0f3f8, #4e9aaf, #274b6d] } }, series: [{ type: map, map: china, roam: true, // 标记全国热门城市 markPoint: { data: res.data.slice(0, 10).map(item ({ name: item.city, coord: getCoord(item.city) })) }, label: { show: false }, data: data }] }); });参数说明visualMap的max值根据Spark聚合结果动态调整写死会导致北京、上海颜色过深其余省份全白。markPoint用来标注Top10城市但要注意地图坐标来源于geoJSON里的properties.cp如果没有这个字段需要额外维护一份城市经纬度映射表。label.show设成false因为省份名称渲染出来重叠严重图例用tooltip展示即可。饼图用ECharts的pie类型配置学历分布。这里有个技巧把学历字段的颜色映射做成数组让本科占比高的城市和整体学历饼图颜色一致用户对比两个图表时视觉上更好找规律。legend在饼图下方横排selectedMode: single允许单选某个学历查看占比大屏演示时点选效果比静态图有说服力。4.3 折线图与词云让数据动起来的关键配置职位数量随月份变化用折线图X轴是月份字符串Y轴是职位数量。数据来自Spark里按发布时间字段聚合的结果发布时间的清洗规则简单粗暴取字符串前7位作为月份例如2024-05-12变成2024-05。const trendChart echarts.init(document.getElementById(trend)); trendChart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: res.data.trend.map(item item.month) }, yAxis: { type: value, name: 职位数量 }, series: [{ name: 职位发布趋势, type: line, smooth: true, data: res.data.trend.map(item item.count) }] });setOption有两个参数notMerge默认是false即新旧数据做合并合并。如果新数据里缺少某些字段旧数据会残留常见案例是筛选完城市后折线图上旧城市的数据还在。解决办法是把第二参数显式传truetrendChart.setOption({...}, true)完全替换配置保证图表演示时数据一致。词云需要第三方库echarts-wordcloud不是ECharts官方自带。安装后注册方式import wordCloud from echarts-wordcloud; echarts.use(wordCloud); wordCloudChart.setOption({ series: [{ type: wordCloud, shape: circle, width: 100%, height: 100%, sizeRange: [12, 60], rotationRange: [0, 0], textStyle: { color: function () { return rgb( [randomRange(64, 224), randomRange(80, 160), 200].join(,) ); } }, data: res.data.tags.map(item ({ name: item.tag, value: item.cnt })) }] });wordcloud词云图在ECharts 5.x里性能一般超过200个词渲染会卡这也是前面Spark分析里limit(50)的原因。shape: circle是默认形状要改成三角形或菱形就传对应字符串但低频词会被挤到边缘且字体极小大屏上可读性差直接用圆形容错率最高。5. 联调验证与性能排错大屏上线前的最后三公里5.1 先验证Spark再验证ECharts顺序别搞反大屏调试时最常见的问题是浏览器F12看到图表空白或数值异常第一反应是改前端代码其实多半是Spark输出的JSON结构不对。建议按数据流方向排查先看Spark输出目录用hdfs dfs -cat /tmp/job51_output/tags/part-00000检查前几行数据格式再把JSON读到浏览器里手动用console.log打印一遍确认ECharts的data字段是数组而不是字符串最后才看图表配置。配一个简单的校验脚本用Python起HTTP服务检查所有接口返回码和字段数量curl http://localhost:8081/api/city_distribution | python3 -m json.tool | head -20这一步能拦截九成以上的图表渲染问题——通常是字段类型对不上ECharts要数字却传了字符串图里全显为NaN。5.2 地图不显示和数值错位的定位技巧地图空白优先检查registerMap是否执行过因为echarts全局注册一次即可放进组件生命周期里反复执行会覆盖。地图上对应省份没有高亮检查geoJSON里省份名称和Spark输出里的city字段是否完全一致例如内蒙古在地图JSON里叫内蒙古自治区需要统一做一次映射。数值错位多半是薪资字段没解析成功回到MongoDB里捅一条脏数据看parse_salary是否返回0这里建议把Spark清洗后salary_low0的行单独统计如果总数超过5%得回头调整UDF的格式匹配逻辑。5.3 大屏性能优化的三个可落地操作首屏渲染慢大屏图表多都是实例化后同时请求接口最直接的办法是改成按区块懒加载地图组件进入视口后再axios.get利用IntersectionObserver监听容器可见性。大数据量下的交互卡顿把地图的geo和series-map拆开用geo做底图series挂数据切换省份时geo不动只更新series数据省去重绘边界坐标。轮询刷新时不要chart.dispose()重建实例复用实例只调setOption参数变化时注意清理visualMap的max值动态计算数据最大值再回填配置。最后给一个验证技巧把Spark作业的调度时间和大屏数据更新时间做成同一个标识符比如用日期字符串拼在接口URL后面/api/tags?date2024-06-01前端F12看到日期变了就证明新数据已经上线不需要再人肉确认数据新旧。这样改完Spark代码重新跑一遍作业接口链路的校验全部靠URL参数自动捕获省心不少。本文还有配套的精品资源点击获取