简介这是一份基于Python的北京市大数据岗位招聘数据分析与可视化完整工程包面向数据采集、清洗、分析与可视化初学者也适合希望提升爬虫与数据分析全流程能力的求职者和在校生。资源共111个文件压缩包约6.58MB除了py爬虫与处理脚本、csv/xls数据文件外还包含html/js/css可视化页面、jpg/png图表截图及地图相关资源目录结构完整便于对照源码与报表学习。项目基于Boss直聘真实职位数据覆盖requests/Scrapy抓取、pandas清洗聚合、numpy数值计算、matplotlib/seaborn可视化等关键环节并利用bootstrap等前端技术呈现可交互图表。借助此包可快速搭建同类招聘分析项目理解岗位地域分布、薪资区间与经验要求等洞察并迁移到其他城市、行业的数据实战中也可作为课程设计与个人项目参考。目前已有727人学习下载。1. 北京市大数据岗位招聘数据值得你亲手搭一个采集到可视化的完整工程一个很反直觉的结论你在招聘网站上搜“大数据”翻到的几百页岗位卡片真正能回答“现在市场上哪个方向最缺人、给钱最多、要什么技能”的从来不是网站自带的筛选器而是你自己清洗、统计过的那份数据。原始JD里全是“15k-25k·14薪”“面议”“弹性工作、扁平管理”这类非结构化文本直接看是噪声可一旦转成薪资区间、技能词频、经验分层它就是一份城市级岗位需求画像。这篇文章讲的是一条 Python 全链路方案爬虫采集北京市大数据岗位数据pandas 做清洗和薪资解析pyecharts 做可视化展示最终落成一个你拿得到数据、跑得通代码、讲得清逻辑的工程。适合正在求职的大数据方向新人也适合拿来做课程设计或简历项目、需要自证“从采集到展示”完整能力的开发者。读完这条线你能复现结果也能自己往深扩展。2. 招聘爬虫怎么写先定字段再写单页抓取最后控反爬2.1 字段设计一次抓取要存哪些列才够后续分析和可视化用写爬虫最容易犯的错是一边抓一边改字段。Python 爬虫改起来成本看起来低实际上字段一旦变更历史数据全废清洗代码、图表代码都要跟着换字段名等于整个项目返工。我一般在写第一行 requests 之前先定数据表结构拿张纸列出要存哪些列、什么类型、从页面哪里取。招聘类字段表按我多次做类似项目的经验下面这些列已经足够支撑后续分析字段名类型来源说明岗位名称str岗位卡片标题例如“大数据开发工程师”公司名称str卡片中的公司名或企业标识薪资文本str原始薪资描述例如“15k-25k·14薪”保留原文工作地点str通常是“北京·某区”格式经验要求str“1-3年”或“经验不限”学历要求str“本科及以上”“大专”等技能标签list从职位描述里抽取的 Python、Spark、Hadoop 等关键词职位描述strJD 全文留作后续深度文本分析发布日期str用于判断数据新鲜度详情页 URLstr去重和溯源用避免数据来源不可查为什么一定要保留薪资原文、JD 全文、URL 这三个字段因为解析代码会迭代。第一次写的正则“15k-25k”能解析但第二周你发现还有“15K-25K·14薪”“底薪提成”“面议”这些写法此时只有原文还在才能重新解析。URL 则是去重和反查的关键没有它重复岗位很难判定。字段定好了后面所有环节——薪资解析、技能分析、区域统计——都在这个结构上做增量。2.2 单页抓取最小实现requests BeautifulSoup 从列表页提取岗位卡片字段定完先写一个能单页跑通的爬虫。很多人一上来就上 Scrapy 框架结果反爬、中间件、Pipeline 概念还没理顺就卡在配置上。对于“北京市大数据岗位”这种单城市、单关键词、数据量几万条以内的场景requests BeautifulSoup 足够没必要用框架。下面是最小实现抓一页列表里的岗位卡片。import requests import time from bs4 import BeautifulSoup 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-Language: zh-CN,zh;q0.9, } BASE_URL https://example-job-site.example/jobs?keyword大数据page{page} def fetch_list_page(page): 抓取列表页 HTML返回解析后的岗位卡片列表 url BASE_URL.format(pagepage) resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() # 关键手动修正编码避免中文乱码 if resp.apparent_encoding: resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) cards [] # 行业惯例岗位卡片通常按 li 或 div[class*job-card] 组织 for item in soup.select(li.job-card, div.job-card): title_tag item.select_one(.job-title) company_tag item.select_one(.company-name) salary_tag item.select_one(.salary) location_tag item.select_one(.location) link_tag item.select_one(a[href]) if title_tag is None: continue cards.append({ 岗位名称: title_tag.get_text(stripTrue), 公司名称: company_tag.get_text(stripTrue) if company_tag else , 薪资文本: salary_tag.get_text(stripTrue) if salary_tag else , 工作地点: location_tag.get_text(stripTrue) if location_tag else , 详情页URL: link_tag.get(href) if link_tag else , }) return cards if __name__ __main__: page_cards fetch_list_page(1) print(f第 1 页抓取到 {len(page_cards)} 条岗位) for c in page_cards[:3]: print(c)逻辑说明代码先请求列表页用apparent_encoding修正页面编码这是中文招聘页面最常见的乱码源头然后通过 CSS 选择器定位岗位卡片容器逐条提取标题、公司、薪资、地点和详情链接。注意我用了get_text(stripTrue)面试官最爱的“爬虫拿到的字符串全是空格换行”问题这一行就能解决。参数说明timeout10是请求超时设太短容易因网络波动误判失败设太长翻页慢10 秒是招聘网站这类响应稳定页面的常见取值。title_tag is None的跳过逻辑很重要因为第一页顶部往往有置顶推荐位结构跟普通岗位卡片不同解析不到标题就该放弃而不是报错。这版代码只抓了列表页字段职位描述、技能标签这类详情字段需要进详情页。但从工程代价看先跑通列表页比一次到位更稳列表页抓完确认字段对得上再写详情页解析函数复用同一个请求头和编码处理逻辑成本很低。2.3 反爬对策与频率控制三个必调参数和一个保底策略招聘网站对爬虫的容忍度不算高但也不像电商那种强反爬。以合规抓取公开页面为前提重点做三件事请求头伪装、抓取频率控制、失败重试。下面这段代码是把 2.2 的方案翻页并加上频率控制的完整形态。import random import time from urllib.parse import urljoin USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/119.0.0.0 Safari/537.36, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Edge/118.0.0.0, ] def fetch_with_retry(page, max_retries3): headers dict(HEADERS) headers[User-Agent] random.choice(USER_AGENTS) for attempt in range(max_retries): try: return fetch_list_page_with_headers(page, headers) except requests.RequestException as e: print(f第 {page} 页第 {attempt1} 次请求失败: {e}) time.sleep(3) def crawl_pages(start_page, end_page): all_cards [] for page in range(start_page, end_page 1): cards fetch_with_retry(page) if not cards: print(f第 {page} 页无数据可能触发了验证或页面改版) break all_cards.extend(cards) print(f已抓取第 {page} 页累计 {len(all_cards)} 条) # 频率控制核心参数随机 sleep 1.5~3 秒 time.sleep(random.uniform(1.5, 3.0)) return all_cards参数说明max_retries3是失败重试次数的经验值超过 3 次还在失败说明不是网络抖动而是被限制或页面结构变了再重试也是浪费时间。time.sleep(random.uniform(1.5, 3.0))是频率控制的关键固定 sleep 会被对方统计出请求特征随机区间更接近人工浏览1.5 到 3 秒的区间保证单城市几万条数据能在合理时间内抓完又不至于对目标站点造成压力。保底策略是当连续两页返回空列表立刻停止并打印当前 URL把页面 HTML 保存到本地debug.html再手动打开检查是验证码还是页面改版。不要对着黑匣子猜这是我从多次爬虫“神秘翻车”里总结出的最值钱习惯。3. 岗位数据清洗与薪资解析把“15k-25k·14薪”变成可统计的数值区间3.1 薪资文本解析正则处理“K/k/千/面议”四种写法招聘薪资文本是整个项目最脏的字段。常见写法有“15k-25k”“15K-25K·14薪”“15千-25千/月”“面议”四种还有少数“底薪提成”“30-60万/年”这类特殊格式。直接用float(15k)显然不行必须先做文本标准化。下面是我常用的解析函数处理高低区间和年薪、月薪单位。import re def parse_salary(text: str) - dict: 解析薪资文本返回区间和月薪中位值 if not text or 面议 in text: return {薪资下限: None, 薪资上限: None, 月薪中位值: None, 薪资原始文本: text} text text.lower() result {薪资原始文本: text} # 处理年薪描述30-60万/年 - 折算为月薪 /12 annual re.search(r(\d(?:\.\d)?)\s*-\s*(\d(?:\.\d)?)\s*万/年, text) if annual: result[薪资下限] float(annual.group(1)) * 10000 / 12 result[薪资上限] float(annual.group(2)) * 10000 / 12 result[月薪中位值] (result[薪资下限] result[薪资上限]) / 2 return result # 月薪描述15k-25k 或 15-25k兼容小数如 15.5k numbers re.findall(r(\d(?:\.\d)?)\s*k, text) if len(numbers) 2: x float(numbers[0]) * 1000 y float(numbers[1]) * 1000 result[薪资下限] x result[薪资上限] y result[月薪中位值] (x y) / 2 return result return {薪资下限: None, 薪资上限: None, 月薪中位值: None, 薪资原始文本: text}逻辑说明函数第一层先排除“面议”这类样本无法量化但不应直接丢弃而是要保留薪资原始文本后续统计时单独报告“面议占比”。第二层处理“30-60万/年”这是资深岗位和大厂常见的表述直接当成月薪会让薪资分布产生几千块的偏移。第三层处理常规月薪区间正则里\d(?:\.\d)?兼容“15.5k”这种带小数的写法否则15.5k-20k会被拆成“15”和“5”两个数字解析结果直接崩。参数说明最后统一转成元为单位即数字*1000这样后面画直方图和计算均值时不用再做单位换标。3.2 经验、学历、区域的标准化映射薪资解析完剩下的经验、学历、区域字段是枚举型但枚举值不统一。“1-3年”“三年以上”“经验不限”能直接看但“本科及以上”“本科”“统招本科”语义是同一个区域字段里“北京·朝阳”“北京朝阳”“朝阳区”也是同一个区。做分析前必须做归一否则透视表里一个朝阳能拆成三行。常见的做法是维护映射字典。import pandas as pd def normalize_exp(value: str) - str: 把经验描述归类为 不限/应届/1-3年/3-5年/5-10年/10年以上 if not value: return 未知 if 不限 in value: return 经验不限 if 应届 in value: return 应届 if 1年以下 in value or 在校 in value: return 1年以下 if 1-3 in value or 1-3年 in value: return 1-3年 if 3-5 in value or 3-5年 in value: return 3-5年 if 5-10 in value or 5-10年 in value: return 5-10年 if 10年 in value: return 10年以上 return value def normalize_edu(value: str) - str: if not value: return 未知 if 大专 in value: return 大专 if 本科 in value: return 本科 if 硕士 in value or 研究生 in value: return 硕士及以上 if 博士 in value: return 博士 return value # 区域处理去掉北京·前缀保留具体区名 def normalize_district(value: str) - str: if not value: return 未知区域 value value.replace(北京·, ).replace(北京, ).strip() return value if value else 北京全域 # 应用映射 df[经验分层] df[经验要求].apply(normalize_exp) df[学历分层] df[学历要求].apply(normalize_edu) df[区域] df[工作地点].apply(normalize_district)逻辑说明三组映射函数都采用“关键词命中优先”的策略因为招聘文本往往是复合写法比如“本科及以上”同时含“本科”和“以上”不能用相等判断。把“硕士及以上”里的“硕士”和“研究生”都归入同一类是为了后续groupby时类别稳定。参数说明normalize_district里连续两个 replace 是因为原始数据既有“北京·朝阳”也有“北京朝阳”甚至还有不带“北京”的“朝阳区”统一处理后才能聚合。这里我保留“北京全域”作为缺失或空区域的兜底值统计时你可以选择剔除它或单独看。3.3 清洗后的数据质量检查九成项目翻在“没做质检”清洗做完不要直接画图。我见过不少项目爬虫没翻车清洗代码没报错但最终图表里的“平均薪资 45k”一看就知道有问题——原因是没过滤实习岗或者重复岗位把统计翻倍了。清洗完成后我每次都会跑一遍质量检查这是整个流程里最像“玄学”的部分因为问题不出在语法层面而出在业务理解层面。# 去重公司岗位薪资下限 三列作为复合主键 df_dedup df.drop_duplicates(subset[公司名称, 岗位名称, 薪资下限], keepfirst) print(f原始数据 {len(df)} 条去重后 {len(df_dedup)} 条) # 质检1面议占比超过 20% 说明薪资字段采集可能不完整 mouth_only df_dedup[薪资原始文本].str.contains(面议, naFalse) print(f面议占比: {mouth_only.mean():.2%}) # 质检2关键字段缺失率 missing_rate df_dedup[[岗位名称, 公司名称, 薪资下限, 经验分层]].isna().mean() print(missing_rate) # 质检3看极端值正常的北京大数据岗位月薪中位值集中在 15k~40k extreme df_dedup[df_dedup[月薪中位值] 100000] print(f月薪中位值超过 10w 的异常样本: {len(extreme)} 条)逻辑说明去重时把薪资下限纳入主键而不是只用公司加岗位名是因为同一家公司可能同时挂“15-25k”和“20-35k”两个薪资区间去测试市场这两条数据是不同岗位不该被合并。质检 1 的面议占比是最容易被忽视的指标如果某个区域下面议特别多说明样本不具备薪资可比性画图时要单独剔除“面议”行并注明质检 3 过滤月薪 10 万以上的记录这类数据大多来自少数头部公司的高阶职位个体量少但会把均值拉高展示中位数比均值更稳。这三个检查做完数据才算是“能用来回答问题”的状态。4. 可视化看板怎么搭用 pyecharts 回答求职者最关心的四个问题4.1 先定义问题再选图表从“画什么图”到“回答什么问题”可视化最忌讳的是为了展示而展示把柱状图、饼图、词云全堆一页。招聘数据做出来给谁看给求职者或决策者看他们只关心四个问题哪些岗位需求量最大、薪资集中在什么区间、哪些技能被反复要求、学历和经验与薪资是什么关系。一个成熟的可视化方案应该反过来设计——先列问题再选择匹配的图表。业务问题推荐图表关键字段哪些岗位方向需求量最大横向条形图 Top20岗位名称计数薪资集中在什么范围直方图 均值线月薪中位值哪些技能词高频出现词云或条形图技能标签学历/经验与薪资关系分组柱状图或箱线图学历分层、经验分层、月薪中位值选 pyecharts 而不是 matplotlib核心原因是 pyecharts 生成的是交互式 HTML图表自带鼠标悬浮数据提示看板演示时可以逐条看数据点求职者也能自己拖着看这个体验差异对项目展示影响很大。配 Streamlit 做页面框架能把多张图拼成一个可筛选的仪表盘后面 4.3 会讲到。4.2 用 pyecharts 画岗位需求 Top20 与薪资分布直方图下面是两个最核心的图表一个横向条形图看岗位热度一个直方图看薪资分布。from pyecharts.charts import Bar, Histogram from pyecharts import options as opts # 图表1岗位需求 Top20 横向条形图 def get_top_jobs(df, top_n20): job_counts df[岗位名称].value_counts().head(top_n) job_names job_counts.index.tolist()[::-1] # 反转让最大的在顶部 job_nums job_counts.values.tolist()[::-1] return job_names, job_nums def render_top_jobs(df): job_names, job_nums get_top_jobs(df) bar ( Bar() .add_xaxis(job_names) .add_yaxis(岗位需求数, job_nums) .reversal_axis() # 横向条形图 .set_global_opts( title_optsopts.TitleOpts(title北京市大数据岗位需求 Top20), xaxis_optsopts.AxisOpts(name岗位数量), yaxis_optsopts.AxisOpts(name岗位名称), ) ) bar.render(output/top_jobs.html) # 图表2月薪中位值分布直方图 def render_salary_hist(df): salary_valid df[月薪中位值].dropna() salary_valid salary_valid[salary_valid 80000] # 剔除极端高值 hist ( Histogram() .add_xaxis(月薪中位值, salary_valid.tolist()) .add_yaxis(频率, None, opacity0.6) .set_global_opts( title_optsopts.TitleOpts(title北京市大数据岗位薪资分布), xaxis_optsopts.AxisOpts(name月薪中位值元), yaxis_optsopts.AxisOpts(name频数), ) ) hist.render(output/salary_dist.html)逻辑说明第一张图里[::-1]反转是因为 pyecharts 横向条形图默认从下往上排列不反转则最大值出现在底部视觉上不直观reversal_axis()把笛卡尔坐标系的 x 轴和 y 轴对调实现横向条形图效果。第二张直方图先剔除月薪 8 万以上的样本目的是避免少量高阶岗位把 x 轴拉得过大使大部分岗位的薪资区间在视觉上挤成一根柱子直方图不直接接受原生数值数组所以这里用了add_yaxis并传入 None让 pyecharts 自动完成分箱计算。参数说明output/目录需要提前建好pyecharts 不会自动创建目录否则render()会直接报 FileNotFoundError。4.3 把图表拼成可交互看板Streamlit 做筛选层多张 HTML 图表单独放着演示时得一个文件一个文件打开说服力差很多。我的做法是用 Streamlit 做一层筛选和拼装左侧选择区域或经验要求右侧动态展示对应图表。Streamlit 的数据流模型适合这个场景每次选择变更是重新跑一遍过滤再渲染数据量在万级完全扛得住。import streamlit as st import pandas as pd from pyecharts.charts import Bar from pyecharts import options as opts import streamlit.components.v1 as components st.cache_data def load_cleaned_data(): return pd.read_csv(data/cleaned_jobs.csv) df load_cleaned_data() st.title(北京市大数据岗位招聘分析看板) exp_list [全部] list(df[经验分层].unique()) exp_selected st.sidebar.selectbox(选择经验要求, exp_list) region_list [全部] list(df[区域].unique()) region_selected st.sidebar.selectbox(选择区域, region_list) filtered df.copy() if exp_selected ! 全部: filtered filtered[filtered[经验分层] exp_selected] if region_selected ! 全部: filtered filtered[filtered[区域] region_selected] job_counts filtered[岗位名称].value_counts().head(15) bar ( Bar() .add_xaxis(job_counts.index.tolist()) .add_yaxis(岗位需求数, job_counts.values.tolist()) .set_global_opts(title_optsopts.TitleOpts(titlef{exp_selected} 岗位需求 Top15)) ) html bar.render_embed() components.html(html, height500)逻辑说明st.cache_data让清洗后的数据只在首次加载时读一次之后的筛选操作不会反复读 CSV这一点对交互流畅度影响很大render_embed()把 pyecharts 图表嵌进 Streamlit 页面不需要生成临时 HTML 文件文件管理更干净。参数说明侧边栏的两个selectbox作为筛选条件筛选逻辑用布尔型 DataFrame 过滤代替query避免用户输入引号造成语法错误。整个看板到这一步已经从“有一堆数据”变成了“能回答问题”的独立交付物。5. 避坑招聘数据项目从爬虫到看板的 5 个常见翻车点5.1 爬虫只返回空列表但页面明明有岗位数据现象在浏览器里打开列表页能看到岗位卡片但爬虫执行后cards列表为空。原因目标页面是异步渲染列表数据由前端 JavaScript 发起第二层接口请求后写入页面requests 只拿得到初始 HTML里面根本没有岗位卡片节点。解决先用一个简单方法判断——把获取到的 HTML 字符串resp.text[:500]打印出来看页面骨架里有没有岗位关键词。异步渲染的页面骨架通常是空的。解决办法有两个一是找数据接口打开浏览器开发者工具的网络面板筛选 XHR 请求找到返回岗位 JSON 的接口直接用 requests 请求接口而不是页面二是换渲染工具比如用 Selenium 或 Playwright 驱动浏览器执行完 JS 再解析。实战中优先找接口成本最低灰度最稳。5.2 薪资解析被“面议”大量污染导致平均薪资严重失真现象清洗后算平均薪资结果出现 3k 或 80k 这种极端值。原因面议岗位的薪资被记成 0或被其他特殊文本干扰另外“底薪提成”这种文本里没有数字区间正则匹配失败返回空值。解决清洗逻辑中把“面议”“底薪提成”“薪资面议”统一归为“不可量化”类不参与均值和中位数计算最终统计时单独报告“面议占比”如果某个岗位方向下面议占比超过 30%这个方向的薪资分析结论就不该写进报告。我在 3.3 里已经加了这个质检逻辑实际使用时要关注占比值的变化趋势。5.3 关键词过滤误判搜“大数据”混入不相干岗位现象岗位榜单里出现“移动端开发”“UI 设计”这类明显非大数据岗位。原因搜索引擎是模糊匹配JD 正文里提到“负责大数据平台对接”的移动端岗位也会被召回如果只按岗位名称字段过滤「大数据销售」「大数据产品经理」又会被误判为技术岗位。解决清洗时增加岗位方向字段按“岗位名称 职位描述”双重匹配来分类。例如技术类岗位要求描述里同时命中“Spark/Hadoop/Flink”中的一个管理类岗位命中“团队/管理/带领”关键词。分类规则不用太复杂四到五个方向就能把纯技术、产品、销售、运维大致分开。5.4 图表中文乱码或中文方块字现象pyecharts 生成的 HTML 里中文标题正常但保存成图片后标题变成方框。原因pyecharts 渲染图片时依赖系统中文字体部分服务器或精简版系统没有安装中文字体字体渲染阶段找不到对应字形。解决本地开发机大多没问题如果要在服务器或 Docker 容器里跑需要安装中文字体常见做法是安装fonts-noto-cjk包并把 pyecharts 的全局字体设置为 Noto Sans CJK SC。做项目演示前先确认演示环境是否有中文字体否则现场出方框会很尴尬。5.5 重复岗位导致统计翻倍榜单顺序完全失真现象某个公司同一个岗位在列表里出现多次岗位需求 Top 榜里它排在第一位但真实招聘量并没有那么大。原因招聘网站会因为不同的招聘渠道、付费位、城市归属产生重复的岗位卡片它们 URL 可能不同但岗位名、公司名、薪资区间几乎一致。解决在 3.3 的去重逻辑里加入“公司名称岗位名称薪资下限”复合键。如果同一公司同一岗位出现不同薪资区间按照薪资区间不同视为两个岗位因为企业确实在招不同职级的定位。另外抓取详情页 URL 后可以用 URL 中的岗位 ID 做第二层去重这个 ID 在站内是唯一的往往比名称合并更准。6. 把源码数据跑成可复现工程目录组织与数据新鲜度验证6.1 代码目录怎么组织爬虫、清洗、可视化三模块强制分离拿到“源代码数据爬虫”类型的项目包第一件事不是运行而是先看目录结构。一套值得留下的工程目录往往是按阶段拆分的。我自己的项目习惯是下面这种组织方式每条数据从采集到展示责任边界都很清楚。project/ ├── requirements.txt # 依赖清单requests, pandas, pyecharts, streamlit ├── README.md # 第一步运行说明按顺序执行 ├── config/ │ └── settings.py # URL模板、请求头、采集关键词、翻页范围 ├── crawler/ │ ├── spider.py # 列表页抓取 详情页抓取 │ └── run_crawler.py # 入口脚本产出去重后的原始 CSV ├── analysis/ │ ├── clean.py # 清洗 薪资解析 归一化 │ └── quality_check.py # 数据质量报告 ├── visualization/ │ ├── charts.py # 生成各图表的 pyecharts 代码 │ └── dashboard.py # Streamlit 仪表盘入口 ├── data/ │ ├── raw_jobs.csv # 爬虫原始输出不做任何处理 │ └── cleaned_jobs.csv # 清洗后可直接分析的数据 └── output/ ├── top_jobs.html └── salary_dist.html参数说明raw_jobs.csv和cleaned_jobs.csv分开放是我一直坚持的习惯。原始数据是后悔药——清洗规则改了一版要重新解析时只需要重新跑clean.py不需要重新爬站。settings.py单独成文件是因为爬虫的频率、UA、关键词这些参数经常要调不要让爬虫脚本里到处散落 URL 和 User-Agent。6.2 数据新鲜度验证拿到项目数据后先重新跑一遍再信结论项目包里自带的数据集发布时间是未知的直接拿来做求职决策可能有风险。我拿到“源代码数据爬虫”这类包不信任数据只信任流程。验证数据新鲜度的方法是重新跑一遍爬虫把新数据与旧数据做三个维度的对比import pandas as pd new_data pd.read_csv(data/my_raw_jobs.csv) old_data pd.read_csv(data/provided_raw_jobs.csv) def freshness_report(new_df, old_df): print(f新数据 {len(new_df)} 条旧数据 {len(old_df)} 条) # 对比1总量变化 print(f数量变化率: {(len(new_df)-len(old_df))/len(old_df)*100:.1f}%) # 对比2薪资中位数是否漂移 for name, df in [(新, new_df), (旧, old_df)]: salary_median df[月薪中位值].dropna().median() print(f{name}数据月薪中位数: {salary_median/1000:.1f}k)逻辑说明数量变化率和薪资中位数漂移是判断数据可信度的关键。如果新数据的中位数和旧数据差异在 10% 以内说明旧数据集可信可以放心用于分析如果差异超过 30%说明期间市场结构变了或者旧数据的采集范围跟新爬虫不一致这时候用旧数据得谨慎。对比完数据的下一步才是开始清洗、画图、写报告。这份工程做到这里已经从“拿别人的数据”变成了“自己验证过的流水线”。我个人的教训是数据项目的可信度不取决于图表多漂亮而取决于每一步能不能复现、数据能不能溯源。养成拿到项目先看原始数据、先跑一遍爬虫的习惯能帮你避开大多数“图表好看但结论不可信”的坑。希望这篇总结能帮你把这个方向真正做透也少走几步我走过的弯路。本文还有配套的精品资源点击获取