资讯动态

Scrapy实战:Boss直聘AI岗位数据抓取与多维度分析

发布时间:2026/10/18 15:16:58 来源:尧图企业网站定制
简介这份实战包面向数据分析初学者、求职调研者与课程设计学生聚焦Boss直聘上大数据、数据分析、机器学习、人工智能等热门方向的岗位采集与统计。资源以Scrapy框架为核心通过纯HTTP请求高效抓取全国热门城市岗位信息输出结构化CSV文件字段涵盖公司名称、职位名称、薪资范围、学历要求、工作经验、工作地点、行业类别与技能关键词并已做基础去重与格式归一化可直接导入Excel或pandas、matplotlib、seaborn进行区域分布热力图、薪资中位数对比、学历需求饼图与高频技能词云等可视化分析。压缩包共34个文件约235KB以13个py脚本承载爬虫工程与解析逻辑12个js与html、css支撑辅助页面另含1个csv数据文件、1个cfg配置及png、txt等说明素材spiders、pipelines、middlewares、settings等模块齐备runspider.py可一键启动。目前已有73人学习适合快速上手岗位数据采集与多维统计的完整练手场景。1. 招聘数据里的AI岗位真相为什么值得自己爬一遍招聘平台上的 AI 岗位数据是当下技术就业市场最直接的体温计。但大多数人看招聘网站只是搜几个关键词、翻两页就关了得到的印象非常粗糙。真正有价值的问题——比如「AI 类岗位里算法和工程的比例到底是多少」「哪些城市在抢人」「薪资区间是怎么分布的」「经验要求集中在几年」——靠手翻是翻不出来的。这个实战包要解决的就是这件事用 Scrapy 把 Boss 直聘上 AI 类岗位的公开列表数据抓下来落成 CSV再做多维度统计和可视化。它适合有 Python 基础、想练一个完整数据管道的开发者也适合想用真实数据判断求职方向的从业者。整条链路不长但每一环都有坑下面按「先立住原理、再动手复现」的顺序拆开讲。2. 抓取目标拆解与 Scrapy 项目骨架搭建动手写爬虫之前最容易被跳过、也最不该跳过的一步是把「抓什么」定义清楚。很多人一上来就写start_requests结果爬到一半发现字段对不上、翻页逻辑错乱只能推倒重来。这一章先把数据模型和项目结构定下来再进入编码。2.1 先定字段再写代码AI 岗位的数据模型招聘列表页能拿到的信息通常比详情页少但胜在量大、翻页快。对「多维度统计」这个目标来说列表页字段已经够用。我一般会先列一张字段表明确每个字段的来源和类型再决定解析逻辑。字段名含义类型来源job_title岗位名称str列表卡片标题salary_raw薪资原始文本str列表卡片如「20-40K·14薪」city城市str列表卡片位置信息experience经验要求str列表卡片标签education学历要求str列表卡片标签company公司名str列表卡片tags技能标签list列表卡片标签区crawl_time抓取时间str程序生成这里有个关键决策薪资不要在原字段上直接清洗。原始文本「20-40K·14薪」里同时包含月薪区间和年终奖信息一旦在爬取阶段就拆成数字后面想按「年薪」维度统计时就没有后悔药了。正确做法是原样存salary_raw清洗放到分析阶段用单独的解析函数处理。同理tags存成列表落 CSV 时用分隔符拼接分析时再拆开。字段定完还要明确抓取范围。AI 类岗位的关键词不能只用一个「AI」那样会漏掉大量岗位。常见做法是用一组关键词算法工程师、机器学习、深度学习、NLP、计算机视觉、数据挖掘、大模型、推荐算法。每个关键词单独跑一轮最后合并去重。去重键用「岗位名 公司 城市」因为同一个岗位在不同关键词下可能重复出现。2.2 创建 Scrapy 项目与目录结构环境上Python 3.8 以上即可Scrapy 用当前稳定版。先建虚拟环境再装依赖避免污染全局。# 创建并激活虚拟环境 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装核心依赖 pip install scrapy pandas matplotlib # 创建 Scrapy 项目 scrapy startproject job_spider cd job_spider scrapy genspider ai_jobs example.com生成后的目录结构大致是这样我习惯在spiders同级加一个utils放清洗和解析工具job_spider/ ├── scrapy.cfg ├── job_spider/ │ ├── __init__.py │ ├── items.py # 字段定义 │ ├── middlewares.py # 中间件 │ ├── pipelines.py # 数据落盘 │ ├── settings.py # 全局配置 │ ├── utils/ │ │ └── parser.py # 薪资、标签解析 │ └── spiders/ │ └── ai_jobs.py # 主爬虫items.py里把 2.1 的字段表翻译成 Item 定义这一步别偷懒字段写全后面 pipeline 才好统一处理。import scrapy class JobItem(scrapy.Item): job_title scrapy.Field() salary_raw scrapy.Field() city scrapy.Field() experience scrapy.Field() education scrapy.Field() company scrapy.Field() tags scrapy.Field() keyword scrapy.Field() # 记录来自哪个搜索词便于后续分析 crawl_time scrapy.Field()keyword这个字段容易被忽略但它对多维度统计很有用——你可以对比不同关键词下的岗位数量判断哪个技术方向需求更旺。字段定义完成后项目骨架就算立住了接下来才是真正的解析和翻页。2.3 settings.py 里必须改的四个配置Scrapy 默认配置是给「礼貌爬取」设计的直接跑会非常慢但也不能无脑调快。下面四个参数是我每次都会动的改之前先理解每个参数在控制什么。# settings.py 关键配置 # 1. 遵守 robots 协议公开数据也保持克制 ROBOTSTXT_OBEY True # 2. 并发请求数默认 16招聘站建议降到 4-8 CONCURRENT_REQUESTS 6 # 3. 下载延迟默认 0必须设置否则容易被限流 DOWNLOAD_DELAY 1.5 RANDOMIZE_DOWNLOAD_DELAY True # 在 0.5~1.5 倍之间随机更像人 # 4. 自动限速根据服务器响应动态调整 AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 1.0 AUTOTHROTTLE_MAX_DELAY 10.0 AUTOTHROTTLE_TARGET_CONCURRENCY 1.0 # 默认请求头补一个正常的 UA DEFAULT_REQUEST_HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, }参数说明CONCURRENT_REQUESTS控制同时发出的请求数调太高会触发风控调太低效率差6 是个稳妥起点。DOWNLOAD_DELAY是每个请求之间的固定间隔配合RANDOMIZE_DOWNLOAD_DELAY变成随机间隔能显著降低被识别为脚本的概率。AUTOTHROTTLE是 Scrapy 自带的自动限速它会根据响应延迟动态调整TARGET_CONCURRENCY设为 1.0 表示「一次只稳定处理一个请求」对招聘站这种反爬较严的目标比较合适。这四个参数不是拍脑袋定的跑之前先用小批量比如 3 页试观察有没有 403 或验证码再决定是否放宽。3. 列表页解析与翻页把岗位数据稳定抓下来项目骨架搭好后核心工作就两件把列表页的每个岗位卡片解析成 Item以及让翻页逻辑稳定跑完。这两件事看着简单实际是翻车最集中的地方。3.1 用 CSS/XPath 定位岗位卡片招聘列表页的结构通常是「一个容器包多个卡片每个卡片里再分标题、薪资、标签」。解析的第一步是找到卡片的选择器而不是直接去抓单个字段——先定位卡片再在卡片内部取字段这样字段之间不会串位。import scrapy from datetime import datetime from job_spider.items import JobItem class AiJobsSpider(scrapy.Spider): name ai_jobs allowed_domains [example.com] # 一组 AI 相关关键词逐个搜索 keywords [算法工程师, 机器学习, 深度学习, NLP, 计算机视觉] def start_requests(self): base https://example.com/jobs?query{}page{} for kw in self.keywords: # 每个关键词从第 1 页开始 yield scrapy.Request( base.format(kw, 1), callbackself.parse, meta{keyword: kw, page: 1}, ) def parse(self, response): keyword response.meta[keyword] page response.meta[page] # 先定位所有岗位卡片 cards response.css(div.job-card-wrapper) for card in cards: item JobItem() item[job_title] card.css(span.job-name::text).get(default).strip() item[salary_raw] card.css(span.salary::text).get(default).strip() item[city] card.css(span.job-area::text).get(default).strip() item[experience] card.css(span.tag-exp::text).get(default).strip() item[education] card.css(span.tag-edu::text).get(default).strip() item[company] card.css(h3.company-name::text).get(default).strip() item[tags] card.css(div.tag-list span::text).getall() item[keyword] keyword item[crawl_time] datetime.now().strftime(%Y-%m-%d %H:%M:%S) yield item # 翻页找到下一页按钮存在就继续 next_page response.css(a.next-page::attr(href)).get() if next_page and page 30: # 硬上限防止无限翻页 yield scrapy.Request( response.urljoin(next_page), callbackself.parse, meta{keyword: keyword, page: page 1}, )逻辑说明start_requests为每个关键词生成第一页请求meta里带上关键词和页码这样在parse里能知道当前数据属于哪个搜索词、翻到第几页。parse先用response.css(div.job-card-wrapper)拿到所有卡片再对每张卡片单独取字段get(default)保证字段缺失时不会报错。翻页部分用response.urljoin把相对链接转成绝对链接page 30是硬上限避免因为页面结构问题陷入死循环。参数说明选择器里的类名job-card-wrapper、job-name等是示例实际要对着目标页面的 DOM 改。改的时候用浏览器开发者工具右键「检查」确认类名唯一且稳定。如果类名是随机生成的比如带哈希后缀就要改用结构选择器比如div[class*job-card]这种模糊匹配。3.2 翻页的三种失败模式与兜底翻页是爬虫最容易断的地方我踩过的坑基本归为三类。第一类是下一页按钮选择器失效。招聘站经常改版今天a.next-page能用明天可能变成button[aria-label下一页]。兜底做法是准备多个候选选择器依次尝试def get_next_url(self, response): candidates [ a.next-page::attr(href), button[aria-label下一页]::attr(data-url), a[class*next]::attr(href), ] for sel in candidates: url response.css(sel).get() if url: return url return None第二类是翻到最后一页后仍返回内容。有些站点翻到超出范围时不返回空页而是返回第一页或一个通用列表导致爬虫无限循环。解决办法是记录已抓取的岗位去重键如果连续两页没有新岗位就主动停止。第三类是翻页链接是 JavaScript 动态生成。这种情况href可能是javascript:void(0)真正的翻页靠 AJAX。此时要么分析 XHR 请求直接请求接口要么用scrapy-splash之类的渲染方案。对招聘列表页来说多数翻页仍是服务端渲染先按静态处理遇到动态再单独处理。提示翻页逻辑写完先用一个关键词跑 3 页把结果打印出来核对字段确认无误再放开全部关键词和页数。3.3 数据落成 CSVPipeline 与编码坑数据抓下来后用 Pipeline 统一落盘。CSV 是最通用的格式但中文编码和字段拼接是两个高频坑。import csv from itemadapter import ItemAdapter class CsvPipeline: def open_spider(self, spider): # utf-8-sig 带 BOMExcel 打开不乱码 self.file open(ai_jobs.csv, w, newline, encodingutf-8-sig) self.writer None def process_item(self, item, spider): adapter ItemAdapter(item) row adapter.asdict() # tags 是列表拼成字符串再写入 row[tags] |.join(row.get(tags, [])) if self.writer is None: self.writer csv.DictWriter(self.file, fieldnamesrow.keys()) self.writer.writeheader() self.writer.writerow(row) return item def close_spider(self, spider): self.file.close()逻辑说明open_spider在爬虫启动时打开文件encodingutf-8-sig是关键——普通utf-8写出的 CSV 用 Excel 打开会中文乱码加 BOM 头能解决。process_item里把tags列表用|拼接避免 CSV 里出现逗号导致列错位。fieldnames用第一行的 keys 动态生成前提是字段顺序稳定所以 Item 定义时字段顺序要固定。参数说明分隔符选|而不是逗号是因为技能标签里可能本身含逗号比如「Python, Go」。如果标签里也可能含|就换成更冷门的分隔符比如\x1f。落盘路径建议带上日期比如ai_jobs_20250101.csv方便多轮抓取对比。4. 多维度统计从 CSV 到可解释的结论数据落到 CSV 只是原料真正有价值的是统计。这一章用 pandas 做几个核心维度的分析每个维度都对应一个实际能回答的问题。4.1 薪资文本清洗把「20-40K·14薪」变成数字薪资清洗是整个分析里最需要小心的一步因为格式太杂。常见格式有「20-40K」「20-40K·14薪」「15-25K·13薪」「300-500元/天」。我一般写一个解析函数返回月薪下限、上限和年薪估算。import re import pandas as pd def parse_salary(text): 解析薪资文本返回 (月薪下限K, 月薪上限K, 年薪估算W) if not isinstance(text, str) or not text.strip(): return None, None, None # 匹配 20-40K 或 20-40K·14薪 m re.search(r(\d)-(\d)K(?:·(\d)薪)?, text) if m: low, high int(m.group(1)), int(m.group(2)) months int(m.group(3)) if m.group(3) else 12 # 年薪 月薪中位数 * 月数 / 10单位万 annual (low high) / 2 * months / 10 return low, high, round(annual, 1) # 匹配 300-500元/天按 22 天折算 m re.search(r(\d)-(\d)元/天, text) if m: low int(m.group(1)) * 22 / 1000 high int(m.group(2)) * 22 / 1000 return round(low, 1), round(high, 1), round((low high) / 2 * 12 / 10, 1) return None, None, None df pd.read_csv(ai_jobs.csv) df[[salary_low, salary_high, salary_annual]] df[salary_raw].apply( lambda x: pd.Series(parse_salary(x)) ) # 丢掉解析失败的记录并打印数量 before len(df) df df.dropna(subset[salary_low]) print(f薪资解析成功 {len(df)}/{before} 条)逻辑说明正则先尝试匹配「K·薪」格式再匹配「元/天」格式都不匹配就返回 None。年薪估算用月薪中位数乘以月数单位统一成「万」方便后续分箱。dropna之后打印成功率如果成功率低于 80%说明页面薪资格式和正则不匹配需要回去补规则。参数说明22是每月工作日估算/1000是把元转成 K/10是把 K 转成万。这些系数不是绝对的按自己的口径调整即可但要在分析结论里注明口径否则数字没法解释。4.2 城市、经验、学历三个维度的分布统计清洗完薪资就可以做分组统计。下面三个维度是最能反映市场结构的。# 城市分布取前 10 city_dist df[city].value_counts().head(10) print(城市分布 Top10) print(city_dist) # 经验要求分布 exp_dist df[experience].value_counts() print(\n经验要求分布) print(exp_dist) # 学历要求分布 edu_dist df[education].value_counts() print(\n学历要求分布) print(edu_dist) # 各城市的平均年薪 city_salary df.groupby(city)[salary_annual].agg([mean, count]) city_salary city_salary[city_salary[count] 5].sort_values(mean, ascendingFalse) print(\n各城市平均年薪样本5) print(city_salary.head(10).round(1))逻辑说明value_counts直接给出频次分布groupby加agg同时算均值和样本量。count 5这个过滤很重要——样本太少的城市均值没有统计意义一个只有 1 条记录的城市可能因为一条高薪岗位就排到第一那是噪声不是信号。参数说明head(10)只取前 10避免输出太长。round(1)保留一位小数。实际分析时经验字段可能有「经验不限」「1-3年」「3-5年」「5-10年」等值直接value_counts就能看出分布不需要额外处理。4.3 技能标签词频AI 岗位到底要什么技术tags字段里藏着技能要求把它拆开做词频统计能直接回答「AI 岗位最常要求哪些技术」。from collections import Counter # 把所有标签拆开、去空格、过滤空值 all_tags [] for tags in df[tags].dropna(): for t in str(tags).split(|): t t.strip() if t: all_tags.append(t) tag_freq Counter(all_tags).most_common(20) print(技能标签 Top20) for tag, cnt in tag_freq: print(f{tag}: {cnt})逻辑说明tags在 CSV 里是用|拼接的字符串先split(|)拆回列表再逐个累加。Counter直接给出词频most_common(20)取前 20。这一步能看出「Python」「PyTorch」「TensorFlow」「机器学习」这类词的出现频次从而判断哪些技能是硬通货。参数说明如果标签里混入了非技能词比如「五险一金」「弹性工作」可以在累加前用一个白名单或黑名单过滤。白名单维护成本高黑名单更实际把明显是福利的词列进去排除即可。5. 可视化与避坑让图表说话让爬虫活着统计数字有了接下来是可视化。但比画图更重要的是——爬虫得能稳定跑完数据得能对上。这一章先讲可视化再集中讲踩坑。5.1 用 matplotlib 画三张核心图可视化不需要花哨三张图足够说明问题城市分布柱状图、薪资分布直方图、技能词频横向柱状图。import matplotlib.pyplot as plt # 解决中文显示 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(1, 3, figsize(18, 5)) # 图1城市分布 city_dist.plot(kindbar, axaxes[0], color#4C72B0) axes[0].set_title(AI 岗位城市分布 Top10) axes[0].set_ylabel(岗位数) # 图2薪资分布 df[salary_annual].hist(bins20, axaxes[1], color#55A868) axes[1].set_title(年薪分布万) axes[1].set_xlabel(年薪) # 图3技能词频 tags, counts zip(*tag_freq[:15]) axes[2].barh(range(len(tags)), counts, color#C44E52) axes[2].set_yticks(range(len(tags))) axes[2].set_yticklabels(tags) axes[2].invert_yaxis() axes[2].set_title(技能标签 Top15) plt.tight_layout() plt.savefig(ai_jobs_report.png, dpi150)逻辑说明subplots(1, 3)一行三列分别放城市、薪资、技能三张图。font.sans-serif设成SimHei解决中文乱码axes.unicode_minus关掉负号显示问题。barh画横向柱状图invert_yaxis让词频最高的排在最上面。tight_layout自动调整间距savefig的dpi150保证清晰度。参数说明bins20是直方图分箱数太少看不出分布形状太多显得毛躁20 对几百条数据比较合适。颜色代码是 matplotlib 内置色板的十六进制值换成自己喜欢的即可。5.2 避坑清单五条血泪经验坑一请求头缺失导致 403。现象是前几个请求正常之后全部返回 403。原因是默认 UA 被识别为脚本。解决是在settings.py里补全DEFAULT_REQUEST_HEADERS至少包含User-Agent和Accept-Language并且开启AUTOTHROTTLE让请求节奏更像人。坑二翻页选择器写死改版即失效。现象是爬到第 2 页就停日志里没有报错。原因是a.next-page这个类名在新版页面里变了。解决是准备多个候选选择器用get_next_url依次尝试同时加「连续两页无新数据就停」的兜底。坑三CSV 中文乱码。现象是 Excel 打开 CSV 全是乱码但用文本编辑器看正常。原因是utf-8不带 BOMExcel 默认按 GBK 解析。解决是写文件时用encodingutf-8-sig多一个 BOM 头Excel 就认了。坑四薪资解析成功率低。现象是dropna之后数据少了一大半。原因是页面薪资格式比预想的多比如「20-40K」「20K-40K」「20-40K·14薪」「面议」。解决是先把所有解析失败的salary_raw打印出来人工看一遍再补正则规则。别指望一次写全。坑五标签字段列错位。现象是 CSV 里某些行的列数对不上pandas 读取时报错。原因是tags列表拼接时用了逗号而标签本身含逗号。解决是换用|或\x1f这类不常见分隔符并在DictWriter里确保字段顺序固定。注意爬取公开数据也要控制频率DOWNLOAD_DELAY不要低于 1 秒单次抓取页数设上限。数据只用于个人分析不要二次分发。6. 从能跑到好用增量抓取与数据校验的两个技巧爬到数据、画出图这个实战包的核心链路就通了。但「能跑一次」和「能持续用」之间还有距离。这一章讲两个让方案真正好用的技巧都是我在反复抓取中磨出来的。第一个技巧是增量抓取。招聘数据是动态的今天抓一次、下周再抓一次才能看出岗位数量的变化趋势。但每次全量重抓既慢又浪费。做法是在 Pipeline 里维护一个已抓取的去重键集合落盘时先读历史 CSV把「岗位名 公司 城市」作为键已存在的跳过。这样第二次抓取只写入新岗位CSV 自然变成累积数据集。实现上open_spider时读一次历史文件把键存进setprocess_item里判断即可。注意去重键不要用薪资因为薪资会变用岗位和公司更稳定。第二个技巧是数据校验。抓完一批数据别急着分析先跑几个断言总记录数是否大于预期下限、salary_raw非空比例是否高于 80%、city字段是否有异常值比如空字符串占比过高、crawl_time是否都在合理时间范围内。这些检查用 pandas 几行就能写完def validate(df): assert len(df) 100, 记录数过少可能抓取中断 assert df[salary_raw].notna().mean() 0.8, 薪资字段缺失过多 assert df[city].replace(, pd.NA).notna().mean() 0.9, 城市字段异常 assert df[job_title].duplicated().mean() 0.3, 重复率过高检查去重逻辑 print(校验通过可以进入分析)逻辑说明四个断言分别检查数据量、薪资完整性、城市完整性和重复率。任何一个不通过说明抓取环节有问题先回去修爬虫别拿脏数据做分析。参数说明0.8、0.9、0.3这些阈值按实际数据调整但要有明确口径不能因为不通过就随手改低。我自己的习惯是每次抓完先跑校验再跑统计最后画图。顺序不能反否则图上的异常你根本分不清是市场信号还是抓取 bug。这个实战包看着简单但把「抓取—清洗—统计—可视化」每一环都做扎实比堆一堆花哨模型有用得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑