简介:这份资源面向具备Python基础、希望入门网络爬虫的开发者与数据分析初学者,以猎聘网为例,完整演示如何抓取招聘岗位信息并落地为CSV表格。内容围绕Requests发送GET请求、Parsel结合CSS选择器解析页面、请求头伪装、字典组织字段、csv模块追加写入等核心环节展开,并涉及异常处理与robots协议等合规要点,帮助读者打通从请求到结构化存储的完整链路。资源包共1个docx文档,约18KB,以图文笔记形式记录关键代码与解析思路,便于对照练习。目前已有4903人学习下载,适合想快速掌握爬虫取数流程、积累实战案例的读者参考,也可作为数据采集入门练习的模板。
1. 从一份招聘 CSV 说起:这个爬虫项目到底能干什么
前阵子帮朋友整理岗位数据,他手动复制了三天招聘信息,Excel 里贴得乱七八糟,薪资、城市、经验要求全混在一列。我接手后写了个不到 80 行的 Python 脚本,跑一遍就拿到一份带表头的 CSV,字段拆得干干净净。这个项目做的就是这件事:用 requests 发 GET 请求抓招聘列表页,parsel 做 CSS 解析,csv 模块落盘。适合刚学完 Python 基础、想找一个完整爬虫练手的人,也适合做岗位分析、薪资调研的开发者直接改参数复用。它不涉及登录、不碰详情页翻页,核心链路短,但把请求构造、选择器提取、字典写入 CSV 这三个爬虫基本功全串了一遍,拿来当第一个真实项目正合适。
2. 请求构造与页面解析:从 URL 到职位列表的完整链路
2.1 先搞清楚请求怎么发:GET 参数与请求头
打开目标招聘站搜索岗位,右键检查进 Network 面板,筛选 Doc 类型请求,刷新后能看到列表页的请求。重点看两处:Request URL 和 Payload。Payload 里就是 URL 问号后面的查询参数,比如搜索关键词、工作年限、来源标识。请求方式是 GET,代码里也必须用 GET,用 POST 会直接拿不到列表数据。
import requests url = 'https://www.liepin.com/zhaopin/' # 这些参数对应浏览器 Payload 里的键值对 data = { 'inputFrom': 'www_index', 'workYearCode': '0', 'key': 'python', # 想换岗位就改这里 'scene': 'input', 'ckId': 'kfpt1emhvkrshc9o4h2xquxd74pfihmb', } 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(url=url, headers=headers, params=data)params=data会让 requests 自动把字典拼成?inputFrom=...&key=python这种形式,不用自己手拼字符串。headers里最关键的是User-Agent,不带它很多站点会返回空页面或直接拒绝。ckId这类参数是站点生成的会话标识,不同时间访问可能变化,如果请求返回的 HTML 里没有职位节点,优先检查这个值是否过期。
2.2 用 parsel 定位列表容器
拿到response.text后交给 parsel 的 Selector,先选中包裹所有职位的大容器,再在循环里逐个提取字段。这样做的好处是选择器作用域被限制在单个职位卡片内,不会串到别的卡片上。
import parsel selector = parsel.Selector(response.text) # 先定位到每一个职位卡片 lis = selector.css('.left-list-box .job-list-item') print(f'本次共抓到 {len(lis)} 个职位').left-list-box .job-list-item是后代选择器,意思是左侧列表容器下所有职位项。如果打印出来是 0,说明页面结构变了或者请求没拿到真实数据,先别急着改提取逻辑,回头确认请求那一步。
2.3 逐字段提取:标题、城市、薪资、标签
每个职位卡片内部字段位置不同,有的在属性里,有的在文本节点里。标题和详情页链接藏在attr里,城市、薪资、公司名是纯文本,标签是一组文本需要getall()拿列表。
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:]) if len(tag_list) > 2 else '' 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()::attr(title)取元素属性值,::text取文本内容,.get()返回第一个匹配结果,.getall()返回全部匹配的列表。标签列表里前两项固定是经验和学历,剩下的用','.join()拼成字符串,避免 CSV 里出现嵌套列表。这里加了长度判断,防止某个职位标签缺失导致IndexError直接中断整个循环。
2.4 字典组装与 CSV 写入
提取完字段后装进字典,键名就是 CSV 的表头。用csv.DictWriter写入,字段顺序由fieldnames决定,和字典键名一一对应。
import csv f = open('招聘.csv', mode='a', encoding='utf-8', newline='') csv_write = csv.DictWriter(f, fieldnames=[ '标题', '城市', '薪资', '经验要求', '学历要求', '职位标签', '公司名称', '公司标签', '详情页', ]) dit = { '标题': title, '城市': city, '薪资': money, '经验要求': exp, '学历要求': edu, '职位标签': tag, '公司名称': company_name, '公司标签': company_tag, '详情页': href, } csv_write.writerow(dit) print(title, city, money, exp, edu, tag, company_name, company_tag, href)mode='a'是追加模式,多次运行不会覆盖之前的数据。encoding='utf-8'保证中文正常写入,newline=''防止 Windows 下每行多出空行。writerow(dit)按fieldnames顺序把字典值写成一行。如果 CSV 打开后表头重复出现,说明每次运行都写了表头,把writerow换成writeheader()只在文件不存在时调用一次即可。
3. 避坑与排查:请求失败、字段错位、编码乱码怎么查
3.1 返回的 HTML 里没有职位节点
现象:len(lis)打印为 0,但浏览器里明明能看到职位列表。原因通常是请求头不完整或ckId过期,站点返回了验证页或空壳页。解决:先把response.text存成debug.html,用浏览器打开看实际返回内容。如果是验证页,补全Cookie或Referer头;如果只是ckId过期,重新从浏览器 Network 面板复制最新值。
3.2 标签列表长度不足导致索引报错
现象:循环跑到某一条时抛IndexError: list index out of range。原因:部分职位没有经验或学历标签,tag_list长度小于 2。解决:不要直接写tag_list[0],用条件判断兜底,如exp = tag_list[0] if len(tag_list) > 0 else ''。这是最容易被忽略的翻车点,因为前几条数据正常,跑到中间才崩。
3.3 CSV 中文乱码
现象:Excel 打开 CSV 显示乱码,但记事本能正常看。原因:Excel 默认按 GBK 解析,而文件是 UTF-8。解决:写入时用encoding='utf-8-sig',它会自动加 BOM 头,Excel 就能正确识别。如果数据要进数据库或做后续处理,保持utf-8即可,别加 BOM。
3.4 详情页链接是相对路径
现象:CSV 里详情页列只有/job/12345.shtml这种半截地址。原因:页面里href本身就是相对路径。解决:拼接域名前缀,href = 'https://www.liepin.com' + href,或者用urllib.parse.urljoin处理。不拼的话后续想批量请求详情页会直接失败。
3.5 频繁请求被限流
现象:跑几次后请求返回 403 或空数据。原因:同一 IP 短时间请求过多。解决:在循环里加time.sleep(1)到time.sleep(3)的随机延迟,别用固定值。另外把User-Agent换成更常见的浏览器标识,不要用脚本默认的python-requests。
提示:调试阶段先把
response.text落盘,比反复改选择器高效得多。选择器写错和请求没拿到数据,现象都是空列表,但排查方向完全不同。
4. 二次开发:从单页脚本到可复用采集工具
4.1 把硬编码参数抽成配置
原始代码里 URL、关键词、请求头全写在顶层,改一个岗位要翻好几处。我一般会抽一个config.py或直接用命令行参数:
import argparse parser = argparse.ArgumentParser() parser.add_argument('--keyword', default='python', help='搜索岗位关键词') parser.add_argument('--output', default='招聘.csv', help='输出文件名') args = parser.parse_args() data['key'] = args.keyword f = open(args.output, mode='a', encoding='utf-8-sig', newline='')这样跑不同岗位只需python spider.py --keyword java --output java岗位.csv,不用改代码。参数化之后,批量采集多个关键词就是外面再套一层循环的事。
4.2 加异常处理和重试
网络请求天然不稳定,单次失败不该让整个脚本挂掉。给请求加try-except和重试:
import time def fetch(url, headers, params, retry=3): for i in range(retry): try: resp = requests.get(url=url, headers=headers, params=params, timeout=10) if resp.status_code == 200: return resp except requests.RequestException as e: print(f'第 {i+1} 次请求失败: {e}') time.sleep(2) return Nonetimeout=10防止请求卡死,retry=3给三次机会,每次间隔 2 秒。返回None时上层跳过本次采集,继续下一条,保证整体不中断。
4.3 去重与增量采集
追加模式跑多次会产生重复行。简单做法是在写入前用集合记录已见过的标题加公司名组合:
seen = set() key = (title, company_name) if key not in seen: seen.add(key) csv_write.writerow(dit)如果要跨次运行去重,就在启动时先读一遍已有 CSV,把标题和公司名加载进seen集合。数据量不大时这样够用,上万条再考虑上 SQLite。
4.4 字段扩展与数据清洗
薪资字段常带「·13薪」这类后缀,城市可能带「·」分隔的区域名。二次开发时可以在写入前做一轮清洗:
| 原始值 | 清洗后 | 处理方式 |
|---|---|---|
| 15-25k·13薪 | 15-25k | 按·分割取第一段 |
| 北京·朝阳区 | 北京 | 按·分割取第一段 |
| 经验不限 | 不限 | 字符串替换 |
清洗逻辑放在字典组装之前,用split和replace就能覆盖大部分情况。别在提取阶段做,提取阶段保持原样,方便回溯。
4.5 验证改造是否生效
改完代码后别直接跑全量,先用--keyword test跑一次,检查三件事:CSV 表头是否正确、字段有没有错位、中文是否正常。确认无误再换真实关键词。我习惯在脚本里加一行print(f'写入 {len(lis)} 条'),跑完一眼就能看出数量对不对。
从那以后我每次改完爬虫都强制走一遍「小样本验证 → 检查 CSV → 再跑全量」的流程,省得跑了几千条才发现字段错位。希望帮到你。
本文还有配套的精品资源,点击获取