简介这份资源面向具备Python基础、希望入门网络爬虫的开发者与数据分析初学者以猎聘网为例完整演示如何抓取招聘岗位信息并落地为CSV文件。内容围绕Requests发送GET请求、Parsel结合CSS选择器解析页面、请求头伪装、字典组织字段、csv模块追加写入等环节展开同时涉及异常处理与robots协议等合规要点帮助读者理解从请求到存储的完整链路。资源包共1个docx文档约18KB以图文笔记形式呈现关键代码与解析思路便于对照练习。目前已有4903人学习下载说明该案例在爬虫入门场景中具有较高的参考价值。读者可借此掌握职位标题、城市、薪资、经验学历、公司标签等字段的提取方法并理解反爬与数据清洗的常见处理方向为后续采集其他站点或做岗位数据分析打下基础。1. 从一份招聘 CSV 说起为什么我还在用 requests parsel 这套老组合上周帮一个做数据分析的朋友跑岗位薪酬分布他丢过来一份 CSV字段整整齐齐标题、城市、薪资、经验要求、学历要求、职位标签、公司名称、公司标签、详情页。我问他怎么弄的他说照着一段 Python 爬虫代码改的核心就三样东西——requests 发请求、parsel 解析页面、csv 落盘。这套组合放到今天不算新甚至有人觉得过时但真到要快速抓一批结构化岗位数据、还要能直接喂给 Excel 或 pandas 的场景它依然是最短路径。这份资源解决的就是这件事以招聘网站为例用 GET 请求拿到搜索页 HTML用 CSS 选择器把列表项里的字段一个个抠出来再以追加模式写进 CSV。适合两类人——刚学完 requests 和选择器、想找一个完整闭环练手的以及手头有临时数据需求、不想上 Scrapy 这种重框架的从业者。下面我按能跑起来的顺序拆一遍重点讲参数怎么定、选择器为什么这么写、以及那些不跑一次根本不知道的坑。2. 请求层拆解URL、params 与 headers 到底怎么定2.1 先搞清楚请求方式和参数从哪来很多人卡在第一步不是不会写代码而是不知道该往params里塞什么。正确做法是打开目标搜索页按 F12 进开发者工具切到 Network 面板筛选 Doc 类型请求然后刷新页面。在请求列表里找那个返回 HTML 的文档请求点进去看两个地方Headers 里的 Request Method以及 Payload 里的查询参数。以招聘搜索页为例请求方式是 GET参数挂在 URL 问号后面形如?inputFromwww_indexworkYearCode0keypythonsceneinputckIdxxx。这些就是你要放进params字典的东西。注意key就是搜索关键词改它就能换岗位ckId这类看着像会话标识的字段有时不带也能返回数据有时会触发校验先原样带上最稳。import requests url https://www.liepin.com/zhaopin/ # params 对应 URL 问号后的查询参数key 控制搜索的岗位关键词 data { inputFrom: www_index, workYearCode: 0, key: python, scene: input, ckId: kfpt1emhvkrshc9o4h2xquxd74pfihmb, } # User-Agent 必须伪装成真实浏览器否则容易被直接拒绝 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.46 } response requests.get(urlurl, headersheaders, paramsdata) print(response.status_code)逻辑说明requests.get会把params自动拼接到 URL 上等价于你手动拼字符串但更安全会自动做 URL 编码。headers里最关键的是User-Agent它告诉服务器我是浏览器。参数说明workYearCode0一般表示不限工作年限sceneinput表示来自搜索框输入这些值直接照抄抓包结果即可不要自己臆造。2.2 判断请求是否真的成功写完上面几行别急着往下解析。先看response.status_code是不是 200再看len(response.text)有没有几万字符。如果返回的是几百字符的短页面八成是被拦了或者参数不对。我一般会顺手把response.text存成debug.html用浏览器打开肉眼确认列表项在不在。# 快速自检状态码 页面长度 是否含目标关键词 print(response.status_code) print(len(response.text)) print(job-list-item in response.text) # 返回 True 说明列表结构在如果job-list-item不在返回内容里先别改选择器问题多半在请求层——要么 UA 被识别要么参数缺了关键项要么该页面本身需要先访问首页拿 Cookie。常见做法是先用requests.Session()访问一次首页再带着同一个 session 去请求搜索页。3. 解析层实战CSS 选择器逐字段提取与容错3.1 用 parsel 定位列表容器拿到 HTML 后用parsel.Selector包一层就能用 CSS 或 XPath 选择器提取。核心思路是先抓大容器再在容器内逐字段抠这样比全局选择器稳得多因为列表项之间的结构是重复的。import parsel selector parsel.Selector(response.text) # 先定位每一个职位卡片这是最外层重复单元 lis selector.css(.left-list-box .job-list-item) print(抓到卡片数, len(lis))逻辑说明.left-list-box .job-list-item是后代选择器意思是left-list-box 里面所有 job-list-item。参数说明类名必须和抓包看到的 DOM 完全一致差一个字符就是空列表。如果len(lis)为 0回到上一步确认 HTML 里到底有没有这个类名。3.2 逐字段提取与列表拼接每个卡片内部再分别取标题、城市、薪资、标签、公司信息、详情页链接。这里有个高频坑标签是一个列表前两个是经验和学历剩下的才是技能标签需要用切片和join处理。for li in lis: title li.css(.job-title-box div:nth-child(1)::attr(title)).get() city li.css(.job-dq-box .ellipsis-1::text).get() money li.css(.job-detail-header-box .job-salary::text).get() # 标签是列表前两项固定为经验和学历其余合并成技能标签 tag_list li.css(.job-labels-box .labels-tag::text).getall() exp tag_list[0] if len(tag_list) 0 else edu tag_list[1] if len(tag_list) 1 else tag ,.join(tag_list[2:]) company_name li.css(.company-name::text).get() company_list li.css(.company-tags-box span::text).getall() company_tag ,.join(company_list) href li.css(.job-detail-box a:nth-child(1)::attr(href)).get() print(title, city, money, exp, edu, tag, company_name, company_tag, href)逻辑说明::text取文本::attr(title)取属性值.get()取第一个匹配.getall()取全部匹配返回列表。参数说明tag_list[0]和[1]前加了长度判断这是血泪经验——不是每条岗位都带满标签直接索引会抛IndexError让整个循环崩掉。,.join()把列表拼成逗号分隔字符串方便写进 CSV 单格。提示选择器里的nth-child(1)依赖 DOM 顺序网站改版时最容易失效。如果某天标题全空了优先怀疑这里而不是怀疑请求。4. 落盘与工程化CSV 写入、翻页与异常处理4.1 DictWriter 写 CSV 的正确姿势用csv.DictWriter比手拼字符串靠谱字段顺序由fieldnames决定字典的键必须和它对齐否则会报ValueError。打开文件时newline不能省否则 Windows 下每行之间会多出空行encodingutf-8保证中文不乱码。import csv f open(招聘.csv, modea, encodingutf-8, newline) csv_write csv.DictWriter(f, fieldnames[ 标题, 城市, 薪资, 经验要求, 学历要求, 职位标签, 公司名称, 公司标签, 详情页, ]) # 首次写入时写表头重复运行会重复写建议加判断 csv_write.writeheader() dit { 标题: title, 城市: city, 薪资: money, 经验要求: exp, 学历要求: edu, 职位标签: tag, 公司名称: company_name, 公司标签: company_tag, 详情页: href, } csv_write.writerow(dit) f.close()逻辑说明modea是追加模式多次运行会往同一个文件后面加数据适合边抓边存。参数说明writeheader()只在文件为空时调用一次比较稳妥否则每次运行都会插一行表头。writerow(dit)按fieldnames的顺序把字典值写进去键名对不上会直接报错这其实是好事——能帮你早发现字段拼写问题。4.2 翻页与请求节奏单页数据量有限要抓多页就得改params里的分页参数常见是curPage或page循环递增。但别裸奔式狂请求加个time.sleep控制节奏既降低被封风险也更接近真人行为。import time for page in range(1, 6): data[curPage] str(page) # 分页参数名以实际抓包为准 resp requests.get(urlurl, headersheaders, paramsdata) # ...解析与写入逻辑... time.sleep(2) # 每页间隔 2 秒逻辑说明把分页参数放进循环每轮重新请求。参数说明curPage只是常见命名实际字段名必须看你抓包结果写错会导致每页返回相同内容。time.sleep(2)是经验值太短容易触发限制太长效率低。4.3 异常处理别省网络请求和解析都可能出意外一个try-except能让程序在个别页面失败时继续跑而不是整体崩掉。for li in lis: try: title li.css(.job-title-box div:nth-child(1)::attr(title)).get() # ...其余字段提取... csv_write.writerow(dit) except Exception as e: print(单条解析失败, e) continue逻辑说明把单条记录的提取和写入包在try里出错就打印并跳过不影响后续记录。参数说明捕获Exception是粗粒度做法生产环境可以细化到IndexError、AttributeError但练手阶段先保证不中断更重要。5. 避坑排查那些不跑一次根本不知道的问题5.1 现象返回内容里没有列表项选择器全空原因请求被识别为非浏览器或缺少必要 Cookie/参数。解决补全User-Agent用requests.Session()先访问首页再请求搜索页必要时把抓包看到的Referer也带上。5.2 现象标签字段报 IndexError程序中断原因部分岗位没有经验或学历标签tag_list长度不足 2。解决索引前判断长度用if len(tag_list) 0 else 兜底别假设每条数据都完整。5.3 现象CSV 用 Excel 打开中文乱码原因Excel 默认按本地编码解析不认 UTF-8。解决写入时用encodingutf-8-sig它会带 BOM 头Excel 就能正确识别中文。5.4 现象多次运行后 CSV 里出现多行表头原因writeheader()每次运行都执行。解决先判断文件是否为空或是否已存在只在首次写入时调用。5.5 现象翻页后每页数据一模一样原因分页参数名写错或参数没真正生效。解决回到 Network 面板对比第一页和第二页请求的 Payload 差异找到真正变化的那一项。注意抓取前先看目标站点的 robots.txt控制请求频率只取公开的岗位信息不要碰任何需要登录才能看到的个人数据。6. 进阶技巧把 CSV 变成能直接分析的数据跑通基础版之后真正拉开差距的是数据清洗。原始薪资字段往往是15-25k·13薪这种字符串直接做统计没法用。我一般会写个小函数把它拆成最低、最高两个数值列再算个中位数。import re def parse_salary(text): 把 15-25k·13薪 解析成 (15, 25)解析失败返回 (None, None) if not text: return None, None nums re.findall(r(\d), text) if len(nums) 2: return int(nums[0]), int(nums[1]) return None, None # 用法读回 CSV 后逐行处理 import csv with open(招聘.csv, encodingutf-8-sig) as f: rows list(csv.DictReader(f)) for r in rows: low, high parse_salary(r[薪资]) r[薪资下限] low r[薪资上限] high逻辑说明re.findall抓出字符串里所有数字前两个当作区间上下限。参数说明正则(\d)匹配连续数字遇到面议这类无数字的返回(None, None)后续统计时过滤掉即可。这样处理后用 pandas 一行df.groupby(城市)[薪资上限].mean()就能出城市薪酬对比。另一个实用技巧是去重。同一岗位可能在不同页重复出现用详情页链接做唯一键去重最可靠seen set() unique_rows [] for r in rows: key r[详情页] if key and key not in seen: seen.add(key) unique_rows.append(r) print(f去重前 {len(rows)} 条去重后 {len(unique_rows)} 条)逻辑说明用集合记录已出现的详情页链接重复的跳过。参数说明key为空时也跳过避免把无效记录算进去。从那以后我每次抓完数据都强制先跑一遍去重和薪资解析再谈分析——不然拿着带重复、带字符串薪资的原始表做图结论全是错的。这套 requests parsel csv 的组合不花哨但把请求、解析、落盘、清洗四步走完一份能直接用的岗位数据集就到手了。希望帮到你。本文还有配套的精品资源点击获取