
简介基于Python开发的北京市大数据岗位招聘数据分析与可视化项目是一套完整可运行的毕业设计源码资源囊括网络爬虫、数据清洗、分析与可视化全流程适合计算机、数据科学等专业学生用于课程设计、期末大作业或毕业设计参考。压缩包共包含110个文件以HTML页面、CSS样式、JavaScript脚本、Python源码及数据文件为主另有XML、图片与字体等支撑素材整体大小6.64MB目录结构清晰便于直接部署演示。项目从招聘网站采集北京市大数据岗位信息经过数据处理后以图表形式展示岗位分布、技能要求及薪资趋势可视化界面交互流畅功能逻辑完整在答辩中获得98分评价。已有88人学习下载适合注重实战的入门与进阶开发者可快速掌握爬虫与数据分析项目落地思路。1. 这是一个什么样的项目把北京的招聘数据从爬虫一路做到可视化把北京市大数据岗位的招聘信息抓下来、清洗成可分析的表格、算清楚薪资和学历分布、再做一张能交互的可视化看板——这个基于Python实现的招聘数据分析与可视化展示项目正好覆盖了爬虫、数据分析、可视化这三条最常被问到的技术线而且每条线都能跑通不是纸上谈兵。对求职者来说它的价值是能回答「北京的算法岗和数据开发岗到底差多少钱、学历卡在哪一档」对做毕业设计或找练手项目的工程师来说它是一套数据从采集到展示的全链路样板拿到之后把爬虫目标站点换掉就能复用到其他城市、其他岗位方向。整个项目可以按「爬虫采集 → Pandas 清洗 → 指标计算 → Flask 接口 → ECharts 图表」的顺序拆开看每一层都能独立验证也方便单独替换。2. 爬虫层用 requests 把职位数据变成结构化 CSV反爬参数这样设置2.1 先设计字段再写代码分析层要用的维度爬虫层必须提前留好很多人在写爬虫时习惯「先抓回来再说」结果到了分析阶段发现学历字段混在描述里、薪资是「面议」没法算、技能标签压根没抓。这个项目的正确顺序是先列出分析维度再倒推爬虫字段。以招聘数据为例分析层需要回答这几类问题岗位分类的分布算法、数据开发、数据分析各占多少、学历要求门槛、经验要求分布、薪资随学历和经验怎么变化、哪些技能关键词被提到最多。对应的字段设计如下字段类型采集来源分析用途title字符串职位名称岗位分类、关键词提取company字符串公司名称公司维度统计salary字符串原始薪资文本如 1.5-2.5万·14薪薪资归一化拆成数值education字符串学历要求本科/硕士/不限学历分布统计experience字符串经验要求3-5年/不限经验与薪资交叉分析tags字符串技能标签用竖线分隔技能词频统计industry字符串公司所属行业行业分布publish_time字符串发布时间时间维度去重与增量更新city字符串城市过滤北京市岗位一个关键点原始字段和清洗后字段要分开存。爬虫层把 salary 原样存成「1.5-2.5万·14薪」分析层再拆成数值字段。如果爬虫阶段就做转换一旦清洗逻辑出错原始数据已经丢了只能重新爬保留原始字段相当于给分析层留了后悔药。字段设计好后爬虫脚本的输出就是一份 CSV文件名带日期比如bj_bigdata_jobs_20250601.csv。每次全量抓取产出一个新文件而不是覆盖旧文件这样后续做趋势对比时历史数据还在。2.2 爬虫骨架requests 会话保持、解析、断点续爬的最小实现爬虫本体用requests就够了不需要上 Scrapy。原因很简单项目只抓一个城市一个方向的数据总量在几千条量级requests 加解析器已经能在一小时内跑完引入 Scrapy 反而增加学习成本和部署复杂度。下面是一个可以直接改目标地址的骨架import requests import pandas as pd import time import random from pathlib import Path HEADERS { 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: application/json, text/plain, */*, Referer: https://www.jobplatform.example/, } SEARCH_API https://api.jobplatform.example/search CITY_CODE 010 # 北京的城市编码按实际站点调整 KEYWORD 大数据岗位 PAGE_SIZE 50 MAX_PAGE 100 session requests.Session() session.headers.update(HEADERS) out_file Path(bj_bigdata_jobs.csv) # 断点续爬如果已有结果文件从已抓取的最大页继续 start_page out_file.exists() and len(pd.read_csv(out_file, encodingutf-8-sig)) 0 start_page 1 def fetch_jobs(page): params { city: CITY_CODE, query: KEYWORD, page: page, page_size: PAGE_SIZE, } resp session.get(SEARCH_API, paramsparams, timeout10) resp.raise_for_status() return resp.json() all_rows [] for page in range(start_page, MAX_PAGE 1): try: data fetch_jobs(page) jobs data.get(data, {}).get(jobs, []) if not jobs: print(fpage {page} 无数据提前终止) break for job in jobs: all_rows.append({ title: job.get(title, ), company: job.get(company, {}).get(name, ), salary: job.get(salary, ), education: job.get(education, ), experience: job.get(experience, ), tags: |.join(job.get(tags, [])) if job.get(tags) else , industry: job.get(industry, ), publish_time: job.get(publishTime, ), city: job.get(city, 北京), }) print(fpage {page} 累计 {len(all_rows)} 条) time.sleep(random.uniform(1.5, 3.5)) except requests.exceptions.RequestException as e: print(fpage {page} 请求失败: {e}继续下一页) time.sleep(5) continue逻辑说明session requests.Session()保持 Cookie 和连接池避免每个请求重新建立 TCP 连接对目标站的负载也更友好。params不直接拼在 URL 里requests 会做正确的 URL 编码避免中文关键词在有些网关上报错。参数说明timeout10单位是秒表示连接和读取一共最多等 10 秒防止单页卡死拖垮整个循环random.uniform(1.5, 3.5)是把每次请求的间隔控制在 1.5 到 3.5 秒之间。这个间隔不是越小越好小于 1 秒会触发频率限制大于 5 秒会让全量抓取拖到一小时以上。1.5~3.5 秒是兼顾速度和存活率的常见区间。MAX_PAGE100是保险丝防止某个搜索结果异常膨胀导致无限循环。2.3 处理页面结构变化和反爬解析库选择、UA 池、验证码的应对顺序招聘站点常见两种返回服务端渲染的 HTML 页面或者前端异步加载返回 JSON。判断方式很简单用浏览器打开页面按 F12 看 Network 面板搜索关键词看响应类型是json还是text/html。如果你的目标站点是 JSON 接口上面例子的写法直接适用如果是 HTML 页面需要把resp.json()换成 HTML 解析from bs4 import BeautifulSoup resp session.get(https://www.jobplatform.example/jobs?key大数据岗位, timeout10) soup BeautifulSoup(resp.text, html.parser) job_cards soup.select(.job-card) for card in job_cards: title card.select_one(.job-title).get_text(stripTrue) salary card.select_one(.salary).get_text(stripTrue) all_rows.append({title: title, salary: salary})CSS 选择器要跟实际页面结构走这里只是示例。解析层的血泪经验是先抓一页存成 HTML 文件在本地反复调试选择器确认无误后再上全量循环。直接在线调试一旦触发验证码你连页面的真实结构都看不到。反爬对策按从轻到重排列维护 2~3 个真实浏览器的 User-Agent 随机切换避免每次请求都暴露同一指纹。请求顺序加随机延时代码里已经实现。如果连续多页返回验证码页面最有效的办法不是硬刚而是停 10 分钟让频率降下来。写一个计数变量连续失败 5 次就中断脚本下次运行用断点续爬从失败点继续。不要用无头浏览器去绕验证码。这个项目的目的是拿数据做分析不是对抗目标站的风控绕过验证码的代价远超收益也不符合数据采集的边界。关于断点续爬我建议把「已抓到的最大页数」单独存到一个小文件里而不是依赖 CSV 行数判断——因为某些页面可能本来就返回 0 条。代码开头读取progress.txt每成功抓一页就写入一次。这是防中途断电最靠谱的做法。3. 分析层用 Pandas 把「1.5-2.5万·14薪」拆成可计算的指标3.1 薪资归一化正则拆分区间、单位换算、年薪折算的三个步骤招聘网站的薪资文本格式五花八门「1.5-2.5万·14薪」、「20-30K·13薪」、「面议」、「30-50万·16薪」。直接把它们当字符串存着没法做统计转成数值需要三个步骤拆出基本工资区间、识别单位、按发薪月数折算成年薪。import re import pandas as pd df pd.read_csv(bj_bigdata_jobs.csv, encodingutf-8-sig) def parse_salary(s): if not isinstance(s, str) or 面议 in s: return None, None, None if · in s: base, extra s.split(·, 1) else: base, extra s, nums re.findall(r[\d.], base) if len(nums) 1: low high float(nums[0]) elif len(nums) 2: low, high float(nums[0]), float(nums[1]) else: return None, None, None if 千 in base: low, high low * 1000, high * 1000 elif 万 in base: low, high low * 10000, high * 10000 months 12 if extra: m re.search(r(\d)薪, extra) if m: months int(m.group(1)) return low, high, low * months / 10000 df[salary_low], df[salary_high], df[salary_annual_wan] zip( *df[salary].map(parse_salary) ) df df.dropna(subset[salary_annual_wan])逻辑说明函数返回三个值——最低月薪、最高月薪、折算后的最低年薪。年薪用low * months / 10000计算单位换算成「万元」这样后续图表纵轴直接显示「万元」不用再做一次除法。拆·是在处理「1.5-2.5万·14薪」这种混合写法后半段单独提取月数。参数说明re.findall(r[\d.], base)会把「1.5」和「2.5」都提出来「20-30K」同理如果只有单值如「15万」就高低位都赋同一个数。这里的隐性问题在下一节讨论——区间下界和上界的口径。实际分析里我一般用区间下界salary_low做保守估计因为招聘网站的薪资区间上界常有注水下界更接近真实基准。3.2 岗位分类与关键词提取顺序敏感的分类函数和标签爆炸处理职位名称五花八门「大数据算法工程师」「数据分析师」「数据仓库开发」「ETL开发工程师」「BI工程师」直接 value_counts 会得到几十个零散类别得先归一化到几个大类。这一步的分类规则直接决定了后面所有分布图表的可信度。title_keywords { 算法: 算法岗, 机器学习: 算法岗, 数据挖掘: 算法岗, 数仓: 数据仓库岗, 仓库: 数据仓库岗, 数据开发: 数据开发岗, etl: 数据开发岗, 数据工程: 数据开发岗, 分析: 数据分析岗, bi: 数据分析岗, 报表: 数据分析岗, } def classify_title(title): t title.lower() for keyword, label in title_keywords.items(): if keyword in t: return label return 其他 df[position_category] df[title].apply(classify_title) print(df[position_category].value_counts())逻辑说明这是一个顺序敏感的字典——越具体的关键词越靠前。比如「大数据算法工程师」同时含「算法」和「数据开发」如果「数据开发」在前就会被错误分类到数据开发岗。把「算法」系列排在「数据开发」前面保证更精确的语义先匹配。技能标签处理上爬虫阶段用|拼接分析阶段用explode()展开skill_series ( df[tags] .dropna() .str.split(|) .explode() .str.strip() ) skill_series skill_series[skill_series ! ] skill_top skill_series.value_counts().head(20) print(skill_top)这里有一个常见坑explode()展开的是行一个职位带 5 个标签就变成 5 行统计词频时不会有影响但如果你要同时统计「学历 × 技能」交叉表需要先对 skill_series 去重再 join 回原表否则同一个职位会被重复计数。我一般会先df.explode(tags)再drop_duplicates(subset[_id, tags])来避免。3.3 从明细到结论学历分布、经验薪资交叉表、中位数兜底清洗完成后第一批结论用三次聚合就能出学历分布、经验要求分布、学历/经验与年薪的中位数交叉表。edu_dist df[education].value_counts(normalizeTrue).round(4) * 100 edu_dist.to_csv(agg_edu_dist.csv, encodingutf-8-sig) exp_salary ( df.groupby(experience)[salary_annual_wan] .agg([count, median]) .round(1) ) exp_salary.to_csv(agg_exp_salary.csv, encodingutf-8-sig) edu_salary ( df.groupby(education)[salary_annual_wan] .agg([count, median]) .round(1) )逻辑说明normalizeTrue返回占比而不是频数乘以 100 后直接是百分比。agg([count, median])一次性同时输出样本量和中位年薪方便判断每个类别的可信度——比如「博士」只有 5 条样本中位数再高也不具代表性看板里可以加标注。为什么用中位数而不是均值薪资分布是典型右偏态个别「算法专家 80万起」会把均值整体拉高中位数更贴近普通人的真实预期。数据分析项目里遇到这类偏态数据中位数是默认选择均值只作为参考指标放副标题。最终输出 CSV 而不是直接打印是为了让可视化层直接读文件不用重新跑一遍清洗流程。这三张聚合表就是可视化层的数据源。到此为止分析部分已经能回答「北京大数据岗位学历门槛多高、经验折溢价什么样、什么技能最常被要求」三个核心问题。4. 可视化层用 Flask ECharts 搭建本地可跑的招聘数据大屏4.1 技术选型为什么是 Flask 而不是 Jupyter 或纯 HTML可视化展示有两条常见路线一是 Jupyter Notebook 里用 Matplotlib 画静态图二是用 Flask 起服务、配 ECharts 做交互看板。前者适合个人探索后者才是项目标题里「可视化展示」该有的形态——能打开浏览器交互查看、能按学历筛选联动、能后期扩展成大屏。这个项目用 Flask 而不是 Django 的理由很实际整个后端只是读 CSV 返回 JSON代码控制在几十行Flask 一个文件就能起服务。Django 对这类小体量任务属于杀鸡用牛刀需要配置模板、Admin、路由规则学习成本高出一大截。目录结构建议这样组织project/ ├── bj_bigdata_jobs.csv # 爬虫输出 ├── agg_*.csv # 分析层输出的聚合表 ├── app.py # Flask 服务 └── static/ ├── index.html # 看板页面 └── echarts.min.js # 本地 ECharts 库注意 ECharts 要下载到本地不要用 CDN。可视化大屏场景下网络环境不可控而且毕设答辩现场经常没外网本地引入能避免页面白屏——这是很多人翻车后才懂的一步。4.2 后端接口设计CSV 到 JSON API 的最小 Flask 实现看板页面上有多个图表前端通过 fetch 拉取 JSON 数据。后端职责是「读文件、聚合、返回 JSON」每个图表一个接口命名清晰。from flask import Flask, jsonify, send_from_directory import pandas as pd app Flask(__name__) df pd.read_csv(bj_bigdata_jobs.csv, encodingutf-8-sig) app.route(/) def index(): return send_from_directory(static, index.html) app.route(/api/summary) def summary(): total len(df) edu df[education].value_counts().head(8).to_dict() exp df[experience].value_counts().head(8).to_dict() return jsonify({ total: total, education: edu, experience: exp, }) app.route(/api/salary_by_edu) def salary_by_edu(): g df.groupby(education)[salary_annual_wan].median().round(1) return jsonify(g.to_dict()) app.route(/api/skill_top) def skill_top(): from collections import Counter counter Counter() for tags in df[tags].dropna(): for tag in str(tags).split(|): counter[tag.strip()] 1 return jsonify(counter.most_common(20)) if __name__ __main__: app.run(debugTrue, host127.0.0.1, port5000)逻辑说明send_from_directory直接从 static 目录返回 HTML 文件省去模板渲染。jsonify负责把 dict 转成 JSON 响应并设置 Content-Type。分组聚合用 Pandas 的 groupby返回的 Series 用to_dict()转成前端友好格式。参数说明host127.0.0.1只允许本机访问安全性更好如果要在局域网内用手机看效果改成0.0.0.0即可但注意这时候任何能访问到你 IP 的人都能看到数据。port5000是 Flask 默认端口如果被占用会报错换成 8000 或 8080 都可以。新旧版本 Flask 的 jsonify 中文编码行为不同——新版默认ensure_asciiTrue返回的 JSON 里中文会变成\uXXXX不影响前端解析但直接在浏览器调试时看着难受。可以加一行app.json.ensure_ascii False新版或app.config[JSON_AS_ASCII] False旧版转成明文。4.3 前端看板ECharts 四个核心图表的配置要点看板放在static/index.html里用 ECharts 的fetch拉接口数据再渲染。四个核心图表分别是岗位分类饼图、学历分布环形图、薪资中位数柱状图、技能关键词条形图。布局上一张可视化大屏通常用 Grid 栅格把页面划分成 2×2 区域每个区域一个图表容器。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title北京市大数据岗位招聘分析看板/title script srcecharts.min.js/script style #grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 16px; } .chart { height: 360px; border: 1px solid #e5e7eb; border-radius: 8px; } /style /head body div idgrid div ideduChart classchart/div div idsalaryChart classchart/div div idskillChart classchart/div div idexpChart classchart/div /div script async function renderSalaryByEdu() { const res await fetch(/api/salary_by_edu); const data await res.json(); const entries Object.entries(data); const chart echarts.init(document.getElementById(salaryChart)); chart.setOption({ tooltip: { trigger: axis }, title: { text: 学历 vs 年薪中位数, left: center }, xAxis: { type: category, data: entries.map(e e[0]) }, yAxis: { type: value, name: 万元/年, nameTextStyle: { align: left } }, series: [{ type: bar, data: entries.map(e e[1]), itemStyle: { color: #3b82f6 }, label: { show: true, position: top, formatter: {c}万 } }] }); } renderSalaryByEdu(); /script /body /html逻辑说明fetch返回 Promiseawait res.json()把响应体解析成对象Object.entries(data)把{ 本科: 28.5 }转成[[本科, 28.5]]数组正好适配 ECharts 的数据格式。echarts.init绑定一个 DOM 元素容器必须有明确高度否则图表渲染出来是一个零像素的空白块——这是 ECharts 最常见的问题。参数说明trigger: axis表示鼠标悬停时整条轴线触发提示框适合柱状图对比formatter: {c}万让数值标签显示「28.5万」而不是裸数字。颜色#3b82f6是蓝色系大屏底色如果比较深可以换成#00e0ff一类亮色。ECharts 初始化完成后窗口大小变化时要调用chart.resize()否则图表会撑满容器或出现滚动条。页面跑起来后输入http://127.0.0.1:5000/看到四个图表后端实时从 CSV 读取数据。后续爬虫更新了 CSV刷新页面图表就跟着变不用改任何前端代码——这就是「接口和数据源分离」带来的好处。5. 避坑爬虫与分析阶段最容易翻车的 5 个问题5.1 现象爬到第 30 页开始全部返回验证码页但程序没报错这不是偶发问题是绝大多数请求频率过高的必然结果。现象很迷惑前面的代码没报错日志里每条都是 200 状态码但数据解析后发现 title 全是空的。原因通常是目标站在第 N 次请求后把响应悄悄换成了验证码 HTML 页面状态码照常 200但 JSON 解析拿不到字段。解决解析前加一道「数据形状校验」检查返回里是否含预期字段不含就直接抛异常连续 3 次校验失败就停 60 秒再重试重试 2 轮还失败就中断。爬虫里最危险的不是报错而是「返回 200 但数据结构变了」这种无声腐蚀。5.2 现象薪资中位数只有 8 万元明显低于真实行情排查时发现「面议」字段被解析成了 0而 0 参与了 median 计算——中位数直接被打下来。原因清洗函数里对「面议」的处理是返回 None但某个分支把空字符串也当成了合法薪资。解决排序检查最便宜的 20 条数据确认没有 0 或者极端小值参杂清洗时对salary_annual_wan设置下界过滤比如df df[df[salary_annual_wan] 3]年收入低于 3 万的数据直接剔除或标记为待人工审核。任何统计结论发布前先看最差和最好的 20 条数据是否存在逻辑矛盾。5.3 现象CSV 用 Excel 打开中文乱码但 Pandas 读取正常原因编码不匹配。爬虫写 CSV 用的是utf-8Excel 默认按GBK/ANSI打开。解决写文件时用encodingutf-8-sig这个编码会在文件开头写入 BOM 标记Excel 能识别并正确解码Pandas 读取时也统一用utf-8-sig两侧保持一致。如果你已经有了一批用纯utf-8写的文件批量转换的脚本用df.to_csv(..., encodingutf-8-sig)重新输出一份即可。5.4 现象可视化大屏页面所有图表都是空白控制台报宽度或高度为 0原因ECharts 的容器 div 没有明确高度。很多人的 HTML 里写了div idchart/divCSS 没设置 heightdiv 高度默认是 0图表自然不可见。解决给所有.chart容器设height: 360px或按百分比加min-height另一个常见情况是页面使用了display: none的 Tab 切换图表在隐藏状态下初始化宽度高度都算不出来需要在切到可见时手动调一次chart.resize()。经验做法是在window.onload之后再初始化图表而不是在 DOM 结构刚插入时就执行。5.5 现象分类统计里「其他」占比高达 60%图表基本没意义原因岗位分类关键词覆盖不全「大数据分析经理」「数据产品专家」这类职位没有落入任何分类。解决先跑df[title].value_counts().head(50)人工扫一遍高频职位名称对照分类字典补关键词再处理低频职位「其他」类别占比超过 20% 就说明分类规则需要迭代。这一步没有玄学套路本质是用人工先看数据再定规则别指望一个字典一次写完美。6. 进阶与验证怎么确认这套分析结果值得被外人相信数据项目最容易被质疑的就是「你这个数字准不准」。我一般用三个方法自检第一抽样人工核对——从清洗后的数据里随机抽 30 条打开原始招聘页面逐条比对薪资、学历、经验三个字段误差率超过 5% 就回头查清洗逻辑第二总量对比——把抓取到的北京大数据岗位总数和招聘平台搜索结果页显示的总数做对比差 10% 以内说明爬虫覆盖基本完整差太多就要检查是不是关键词漏了同义词第三稳定性测试——隔一周重跑一次爬虫对比两次抓到的岗位 ID 重叠度重叠率应该在 60% 以上否则说明数据缺口巨大。如果要做成长期观察的看板下一步是把 CSV 换成 SQLite爬虫每天增量写入再给 Flask 加一个last_updated字段供前端展示数据新鲜度。技能关键词部分也可以引入 Word2Vec 做同义词合并比如把「Hadoop」「Spark」归入「大数据框架」这个更上层的类别——但对于当前这个项目规模Pandas 的计数已经够用不必为了算法而加算法。我做这类项目有个习惯先拿 50 条数据跑通整条管道再放全量。50 条足够暴露清洗规则和图表配置的问题跑全量省下的是重写脚本的时间而不是爬虫时间。这套方案的真正价值在于——它把爬虫、数据分析、可视化三条技能线缝在了一起你替换掉数据源之后就能迁移到任何结构化招聘数据上继续用。希望帮到你。本文还有配套的精品资源点击获取