最近半年大模型岗位的讨论热度一直居高不下打开脉脉、猎聘、BOSS直聘铺天盖地都是大模型算法工程师、大模型应用开发、LLM推理优化这类职位。我身边不少朋友都在纠结要不要转行做AI也有人担心这是泡沫期的高薪幻觉。与其靠感觉判断不如直接把招聘数据拉下来做一次彻底的量化分析。所以前阵子我做了一个Python全链路的项目从爬虫采集招聘网站数据到清洗加工再到可视化大屏展示最终产出了一套大模型岗位人才需求的实时分析报告。这篇文章就完整拆解这个项目的设计思路、实现细节和最终数据结论适合正在做数据分析、想了解大模型就业行情、或者打算做类似爬虫可视化项目的朋友参考。1. 项目定位一次从爬虫到可视化大屏的完整链路1.1 大模型人才需求画像靠直觉说和靠数据说的区别大模型岗位热不热这个话题很多人都在讨论但大多数人的判断依据是朋友圈里的新闻、融资消息、某个大厂开了多少薪资的截图。这种信息有两个问题第一是幸存者偏差高薪案例被反复传播普通岗位的真实水平反而没人关注。第二是没有全局视角北京上海需求旺盛不代表全国都是这样算法岗火也不代表数据标注、模型部署、AI产品经理这些周边岗位同样火。做这个项目的核心目标就是把“大模型岗位人才需求”这个模糊的概念拆成一组可以量化、可以对比、可以追踪的指标再通过可视化的方式让规律自己浮现出来。当时我在定方案的时候列了三个核心问题大模型岗位集中分布在哪些城市薪资段位如何技能要求有什么变化趋势。这三个问题直接影响求职者的决策、企业的薪资定价以及培训机构课程方向的判断所以做出来之后的应用价值是真实的不是自嗨。数据来源选的是国内主流的几家招聘平台公开页面。之所以没有用现成的数据API是因为这些接口大多需要企业授权个人开发者拿不到稳定的权限。而网页端的关键字段——岗位名称、薪资区间、城市、经验要求、学历门槛、技能标签、发布日期——是公开可见的用爬虫采集在技术上是完全可行的。1.2 核心指标与可视化指标体系设计在设计指标体系的时候不能想到什么图表就做什么图表而是要从分析目标倒推。最终我确定了七个核心维度每一个维度对应一种可视化形态分析维度关键字段可视化形式解决什么问题需求规模岗位数量、发布公司数折线图时间趋势岗位是持续增长还是阶段性爆发地域分布城市、城区地图气泡图哪些城市是需求高地薪资水平月薪下限、月薪上限箱线图条形图不同层级、不同城市的薪资差异经验门槛经验要求堆叠柱状图初中高级岗位的结构比例学历门槛学历要求饼图是否有学历歧视技能需求技能标签词云横向条形图企业最看重的技术栈公司类型公司规模、融资阶段散点图大厂和创业公司的需求差异这个指标体系不是一次性定死的。在第一版跑完数据之后我发现“公司融资阶段”这个字段缺失严重真正拿到有效数据不到四成也直接影响了结论的可信度所以后期砍掉这个维度换成了“岗位细分方向”也就是把算法岗、开发岗、测试岗、产品岗拆开来看。这个调整背后的逻辑很简单数据质量比指标数量重要得多宁可少一张图也不能让一张图误导判断。2. Python爬虫采集主流招聘平台的字段设计与反爬应对2.1 技术选型Requests多线程配合Aiohttp为什么不直接上Scrapy爬虫方案选型阶段很多新手第一反应是上Scrapy但其实这个项目的体量完全没必要。大模型岗位虽然热门但整体岗位量级也就是几万个分布在几次分页请求里单线程爬也能在四五个小时内跑完。用Scrapy需要额外维护中间件、Item Pipeline、Twisted异步环境项目结构复杂了反而拖慢开发速度。我最终用的是Requests lxml Aiohttp的组合。Requests负责基础的HTTP请求lxml负责XPath解析HTMLAiohttp用来做并发请求加速。技术要点是先单线程把某个城市某个关键词的搜索页跑通确认解析逻辑没问题再改成并发模式。一上来就开二十个协程一旦解析逻辑有Bug错误信息会在几十个请求里重复刷屏排查难度直接翻倍。核心请求代码大致长这样import requests from lxml import etree headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.zhipin.com/, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(url, retry3): for i in range(retry): try: resp requests.get(url, headersheaders, timeout8) if resp.status_code 200: return resp.text except requests.RequestException: time.sleep(2 ** i) return None def parse_jobs(html): root etree.HTML(html) job_items [] for li in root.xpath(//li[contains(class,job-card)]): title li.xpath(.//span[classjob-name]/text()) salary li.xpath(.//span[classsalary]/text()) company li.xpath(.//div[classcompany-name]/text()) job_items.append({ title: title[0] if title else , salary: salary[0] if salary else , company: company[0] if company else , }) return job_itemsXPath的写法以目标站点的实际HTML结构为准不同平台差异很大我的经验是先在浏览器开发者工具里定位到元素右键复制完整的XPath再用代码跑一遍看能不能取到值不要凭感觉写路径会浪费很多时间。2.2 请求伪装、限速和断点续采反爬是爬虫项目绕不开的坎。招聘网站的防护策略不算顶尖但也不是裸奔常见的反爬机制有IP频率检测、User-Agent指纹识别、Cookie校验、WebDriver检测这几类。针对这些机制我的应对方案是请求头伪装。除了User-AgentReferer和Accept-Language也要设置正确否则服务端日志能直接看出来是脚本请求。UA池至少备五六个不同的UA轮换。访问频率控制。每请求一次之后随机等待1到3秒避免固定间隔的机械感。并发数控制在8到10个不要贪多。断点续采。爬虫断掉是常态内存里维护一个已采集URL的集合每成功解析一条就写入本地CSV同时把当前页码记录下来。重启之后从断点继续不用每次从头开始。代理池备用。如果IP被封需要切换到代理IP重试。我实际用到的是免费代理池稳定性一般但胜在零成本。真要跑大规模采集还是建议用付费代理。断点续采的代码不复杂核心是维护状态import json, os STATE_FILE crawl_state.json def save_state(state): with open(STATE_FILE, w, encodingutf-8) as f: json.dump(state, f, ensure_asciiFalse, indent2) def load_state(): if os.path.exists(STATE_FILE): with open(STATE_FILE, r, encodingutf-8) as f: return json.load(f) return {finished_urls: [], current_page: 1}2.3 原始数据合规底线爬虫写起来一时爽但法律法规的红线一定要知道。我做这个项目只采集公开页面上可以直接看到的招聘信息不碰用户个人隐私数据包括简历、联系方式、实名信息。采集频率控制在“接近人工浏览”的水平不对目标服务器造成压力。数据只用作文本分析不做商业化使用不批量导出扩散。这里特别提醒一句如果只是自己做学习研究爬取公开信息用于统计分析风险是相对可控的。但如果要大规模商用请务必走平台方的开放API或者商务合作渠道。技术能力不等于使用权限这个边界要守住。3. 数据清洗与特征工程从“25-35K·14薪”到结构化指标3.1 岗位名称的归类与语义归一化原始数据里的岗位名称五花八门“大模型算法工程师”“NLP算法工程师大模型方向”“LLM应用开发”“AI算法研究员”“高级大模型训练工程师”……如果直接用原始文本做统计会被拆分出几百个细分名称完全没法看趋势。所以清洗阶段最重要的一步是做岗位归类和方向映射。分类逻辑我用了两层第一层是硬编码关键词表把“大模型”“LLM”“GPT”“AIGC”“生成式AI”“多模态”这些词命中到“大模型相关岗位”主类别。第二层是在主类别内部再做方向拆分规则是命中“训练”“预训练”“微调”“调优”“SFT”→ 模型训练方向命中“推理”“加速”“部署”“vLLM”“TensorRT”→ 推理部署方向命中“RAG”“Agent”“应用开发”“Prompt”→ 应用开发方向命中“产品”“运营”“解决方案”→ 非技术岗位方向这个分类规则是迭代出来的。第一版跑完发现很多岗位同时命中多个方向比如“大模型算法工程师RAG方向”既命中算法又命中应用开发需要定义优先级算法方向优先于应用方向训练方向优先于推理方向。跟现实情况对照一下这种方式基本能反映岗位的核心职责。3.2 薪水、学历、经验字段的解析策略薪资字段是清洗阶段最头疼的。“25-35K·14薪”这种格式还算友好正则表达式一行搞定。但还有“30-60K·16薪”“4-6万·15薪”“面议”“200-400/天”“1000-2000/日”等等。统一处理的策略是全部换算成月薪单位按“月薪下限月薪上限”拆分为两个字段遇到“万”单位乘以10转成K遇到“/天”或“/日”的按22个工作日换算成月薪遇到“面议”直接置空。下面这段代码是我清洗薪资的核心逻辑import re def parse_salary(salary_str): if not salary_str or 面议 in salary_str: return None, None # 提取两个数字 nums re.findall(r(\d\.?\d*), salary_str) if len(nums) 2: return None, None low, high float(nums[0]), float(nums[1]) # 判断单位 if 万 in salary_str: low, high low * 10, high * 10 # 日薪换算 if (/ in salary_str or 日 in salary_str) and 月 not in salary_str: low, high low * 22 / 10, high * 22 / 10 return low, high这个函数有个小坑就是“/日”的岗位通常是实习岗月薪看起来很低但时薪可能并不低所以在做分析时要把日薪岗位单独标记出来不能跟正常月薪岗位混在一起算统计值。学历和经验字段相对简单直接用映射字典处理edu_map { 学历不限: 不限, 大专: 大专, 本科: 本科, 硕士: 硕士, 博士: 博士, } exp_map { 经验不限: 0-3年, 在校/应届: 应届, 1-3年: 1-3年, 3-5年: 3-5年, 5-10年: 5-10年, 10年以上: 10年以上, }3.3 去重策略与数据质量校验招聘平台的同一个岗位可能会在多个搜索关键词下重复出现比如搜“大模型”和搜“AIGC”都能看到同一个算法工程师职位。去重不能只看岗位名称因为“高级算法工程师”在不同搜索结果里会出现多次。我采用的组合去重键是“公司名岗位名城市薪资下限”四个维度同时相同才判定为重复。这个策略实测下来误杀率很低因为同一公司同一岗位在不同渠道开的薪资通常是一致的不同岗位即使名称相同薪资也会有差异。数据质量校验放在最后用简单的逻辑规则检查清洗后的数据是否符合基本常识薪资下限大于0且上限大于下限上下限差值不超过50K发布日期不晚于采集日期城市字段属于中国大陆城市列表学历字段在映射字典范围内不满足规则的数据直接剔除宁可样本少一点也不能让脏数据污染分析结果。这轮清洗下来原始采集的两万多条记录最后剩下约一万五千条有效数据去重率大概在25%左右这个比例跟招聘网站多关键词搜索的机制基本吻合。4. 可视化实现从静态图表到动态大屏的分阶段落地4.1 第一阶段PandasMatplotlib快速验证可视化的第一步不是直接做大屏而是先用Matplotlib画几张静态图验证数据规律。这个阶段的核心目标是确认数据有没有异常值、分类逻辑是否正确、初步趋势是否符合预期。如果这个阶段就画出一张“城市分布图”发现北上广深之外还有一个城市数据异常高那大概率是爬虫解析时把某个页面的城市字段解析错了这时候去检查数据比后面再做交互图发现问题要省力得多。我当时先跑了几张图验证方向按城市聚合岗位数量的柱状图、按薪资区间分组的直方图、按技能关键词排序的条形图。这些图证实了三个基本判断大模型岗位需求确实集中在头部城市、薪资分布呈现明显的右偏特征、技能需求集中在PyTorch、Transformer、微调、RAG这些方向。数据逻辑没问题之后再进入交互可视化阶段。4.2 第二阶段ECharts交互式图表的核心页面设计交互可视化我选了ECharts而不是Python系的Plotly。原因有两个ECharts在国内前端生态里的成熟度更高地图、词云、热力图等图表类型都支持另一个是中文文档完善、社区例子丰富遇到问题搜一下就能找到解决方案。大屏页面的核心是五个图表模块中国地图用地图气泡图展示各城市岗位数量。气泡大小映射岗位数量颜色映射平均薪资鼠标悬停显示城市详情。薪资箱线图按经验层级分组的薪资分布。能直观看到高薪岗位集中在哪个经验段以及薪资的离散程度。技能词云展示岗位描述中高频出现的技能关键词。词云是最直观的技术栈需求图谱。趋势折线图按发布时间统计的岗位新增数量趋势。这个能帮助判断市场是持续热还是短期拥挤。学历经验堆叠图展示不同岗位方向对学历和经验的要求结构。ECharts的配置项核心是option对象下面这个示例是城市气泡图的关键配置option { tooltip: { trigger: item, formatter: function(params) { return params.name br/岗位数量: params.value[2] br/平均薪资: params.value[3] K; } }, geo: { map: china, roam: true, itemStyle: { areaColor: #e0e7f1, borderColor: #8ca3bd } }, series: [{ type: scatter, coordinateSystem: geo, data: cityData.map(item ({ name: item.city, value: [item.lng, item.lat, item.count, item.avgSalary] })), symbolSize: function(val) { return Math.max(8, Math.sqrt(val[2]) * 2); }, itemStyle: { color: #3b82f6, opacity: 0.7 } }] };这里有个细节值得注意气泡大小的映射用平方根而不是线性映射。因为城市之间岗位数量的差异是倍数的比如北京3000个岗位成都500个线性映射会导致成都的气泡小到看不见平方根映射能保留小数值的可视化差异同时不至于让大数值占满整个屏幕。4.3 第三阶段Flask动态大屏整合静态HTML页面的问题是数据是写死的每次更新都需要重新生成。所以我用Flask做了一层薄薄的后端服务把清洗好的数据以JSON接口形式暴露给前端前端ECharts通过Ajax拉取数据后动态渲染。这样做的最大好处是后续只需要跑一遍爬虫和清洗脚本刷新页面就能看到最新数据不需要重新改前端代码。整个大屏项目的技术栈就是Python爬虫数据入库→Pandas清洗→导出JSON→Flask提供API→ECharts渲染。Flask端的代码非常简洁from flask import Flask, jsonify import pandas as pd app Flask(__name__) df pd.read_csv(cleaned_jobs.csv) app.route(/api/summary) def summary(): result { total_jobs: len(df), total_companies: df[company].nunique(), avg_salary: round(df[salary_low].mean(), 1), city_data: city_agg(), skill_data: skill_agg(), salary_trend: salary_trend(), } return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000)需要注意的一点是接口返回的JSON不能直接拿DataFrame.to_dict()来用因为Pandas会把numpy类型的数据带进去JSON序列化时会报错。稳妥的方式是手动转换成Python原生类型或者显式调用astype(float)处理。5. 数据里的结论大模型岗位需求到底呈现什么特征5.1 城市分布招聘需求高度聚集但第二梯队明显加速可视化大屏渲染出来之后第一张让我盯了很久的图是城市分布气泡图。结论很清晰北京、上海、深圳、杭州四个城市贡献了超过60%的大模型岗位需求。北京一枝独秀岗位数量几乎是上海的两倍这跟大模型公司总部集中在中关村、五道口、望京这些区域有直接关系。上海排在第二优势产业在金融大模型和企业服务大模型。深圳和杭州属于第二列的头部深圳偏硬件和具身智能方向杭州则是阿里系、DeepSeek等大模型公司拉动了需求。但更有意思的是第二梯队。苏州、南京、成都、武汉、西安这些城市的岗位量虽然绝对值不算大但在采样期间的增长速度比头部城市高出了不少。这说明大模型的人才需求正在从京沪深杭向外围城市扩散。对求职者来说如果愿意去第二梯队城市竞争压力会小很多生活成本也更友好。5.2 薪资与经验高薪不是标配经验断层更值得关注薪资分布这块的结论比较扎实。北京算法岗的薪资中位数在45K到55K之间上海稍低一些在40K到50K之间。但要注意这是中位数不是平均值意味着依然有大量岗位薪资只有20K到30K。把岗位方向拆分之后能看到更真实的图景做模型训练和算法研究的岗位薪资显著高于应用开发和部署岗位。原因并不复杂——能吃透模型原理、能调loss、能改模型结构的人本来就少供给稀缺导致价格走高。最让我意外的是经验门槛的数据。很多人以为大模型岗位都是高门槛实际统计下来“1-3年经验”的岗位占比是35%高于“5-10年”的22%。“经验不限”和“应届生”加起来超过了15%。这是一个典型的增量市场特征企业自己也不完全清楚需要什么样的人整个行业还在用“快速试错”的方式招人。但另一个角度也值得注意“5-10年”经验岗位的平均薪资最高说明行业真正缺的不是会用大模型API的人而是能把模型做深做扎实的资深工程师。5.3 技能需求演变微调、RAG、Agent成为新的关键词技能词云是最能反映大模型技术路线迭代的一张图。把15万个技能标签做词频统计之后技术关键词排序非常说明问题PyTorch、Transformer、Python排在头部这属于大模型的“基建技能”任何方向都需要。紧接着是微调、RAG、Agent、Prompt、LoRA、vLLM、分布式训练、CUDA、TensorRT。这个排序背后是技术热点的迁移。2023年讨论大模型还是“预训练微调”2024年上半年谈到RAG下半年Agent开始爆发到2025年几乎每三个岗位里就有一个提到Agent。企业在招人时写“熟悉Agent框架”已经是标配不再是加分项。另外vLLM和TensorRT的高频出现说明“部署和推理优化”已经脱离小众方向成为独立的高薪岗位这对那些不想卷算法但想进大模型赛道的开发者是一个可以考虑的切入口。6. 我把这套项目的完整架构和可复用部分拆给你看6.1 项目目录结构与关键依赖整个项目的代码结构做了模块化拆分目录大概长这样llm_job_analysis/ ├── crawler/ │ ├── __init__.py │ ├── fetch.py # 爬虫请求与解析 │ ├── state.py # 断点续采状态管理 │ └── config.py # 关键词、城市、URL配置 ├── analysis/ │ ├── clean.py # 数据清洗与字段解析 │ ├── classify.py # 岗位方向分类映射 │ └── aggregate.py # 聚合统计指标 ├── web/ │ ├── app.py # Flask API服务 │ ├── templates/ │ │ └── dashboard.html # 可视化大屏 │ └── static/ │ └── echarts.min.js ├── data/ │ ├── raw/ # 原始采集数据 │ └── cleaned/ # 清洗后的结构化数据 └── requirements.txtrequirements.txt里核心的依赖只有五个requests、lxml、pandas、flask、pyecharts生成地图JSON用。没有引入重型数据处理框架一来是这个体量的数据用Pandas完全够二来是项目跑在普通笔记本上也毫无压力。6.2 值得沉淀的三个可复用模块项目做完之后回头看有三块代码是完全可以复用到其他可视化分析项目的建议你直接抄去改第一块是字段解析工具集。parse_salary()、parse_edu()、parse_exp()这套函数核心就是把非结构化的文本转成结构化字段不仅适用于招聘数据任何带有“文本中间藏着数值”特征的数据都能用。第二块是聚合统计模块。aggregate.py里封装了按城市、按岗位方向、按时间维度做groupby的通用逻辑传入DataFrame和维度名就能返回聚合结果。实际开发的时候这套封装能省很多重复的pandas代码。第三块是JSON接口的标准化输出。Flask接口统一返回{code: 0, data: {...}, msg: ok}的结构前端处理逻辑变得非常统一。这个接口设计模式在写其他数据可视化项目时也能直接复用。6.3 我踩过的三个坑这个项目从开始到跑通完整链路大概花了一周多时间。过程中踩过的坑不少挑三个典型的说一下希望能帮你少走弯路。第一个坑是爬虫频率控制不当导致IP被封。当时为了追求速度把并发数调到20结果跑了不到十分钟就触发了网站的IP限制策略整批请求全部返回验证码页面。后面把并发数降到8每请求延迟2秒左右就稳定了很多。爬虫的“快”不是目标“稳”才是。第二个坑是薪资解析时忽略了英文大小写问题。有些JD里写的是“25k-35k”不是“25K-35K”如果用正则里的“K”去匹配会漏掉一半的英文格式。最后统一先做字符串大写转换再解析这个问题就消失了。做任何文本解析都要考虑数据格式的多样性和大小写变体。第三个坑是ECharts的中国地图在加载时特别慢。原因是地图JSON文件用的全量版包含省市县三级数据体积好几MB。换成只含省级边界和地级市坐标的精简版GeoJSON之后加载速度从七八秒降到一秒以内。这个优化对于做大屏项目来说非常重要用户不会有耐心等一张地图转半小时圈。最后说说我自己的体会。做完这个数据可视化项目之后我最大的感受是技术链路本身不算复杂难的是让每个环节的数据质量都不出问题。爬虫采集的时候觉得数据挺规整清洗阶段才发现有各种格式变体可视化阶段又发现聚合口径不够统一。每一步踩坑、排查、修正都是数据工程师日常工作的缩影。你要是也在做类似的项目不要只盯着最终的大屏效果把更多的耐心留给数据清洗和数据质量校验这部分的回报率是最高的。后续如果想在这个项目上继续扩展可以考虑接入招聘网站的历史数据做时间序列对比或者训练一个岗位薪资预测模型这些都是顺着现有数据资产能自然延伸的方向。
