前言
这个标题要求「可直接使用的源码」。先说明为什么本文不会给出那样的代码——这不是技术做不到,而是不该做。
判断能不能抓,第一步是读目标站点的 robots.txt。Boss 直聘的 robots.txt 对User-agent: *给出的规则相当严格,其中包含一条覆盖面极广的Disallow: /*?*——任何带查询参数的 URL 都在禁止之列,而职位搜索列表页恰恰形如.../web/geek/job?query=python&city=...,正好命中;此外/web/geek/*、/web/boss/*、/job_detail/l*.html等路径也被显式禁止,SemrushBot、Jobuispider等爬虫更是被整站拒绝。再加上职位信息需要登录后才有完整内容、站点对自动化访问有明确的技术与条款限制——综合起来,「绕过这些限制去批量抓取职位信息」这件事本身就不合规。
所以本文改讲三件真正有用的事:第一,拿到一个目标时,如何系统性地判断「能不能抓」;第二,要获取职位类数据,正规路径有哪些;第三,同样的 Scrapy 技术,如何在一个允许抓取的靶子上练熟——毕竟你要学的从来不是「怎么绕过某个站」,而是解析、清洗、结构化这套通用能力。文中也不会出现构造特殊请求头、伪造设备指纹、破解验证码之类的内容。
本文的代码以 Python 3 为基准。老教程里常见的 Python 2 写法(print语句、xrange、dict.iteritems())都已失效,Python 2.7 于 2020 年 1 月 1 日停止维护,请一律改用print(...)、range、.items()。
一、合规评估清单
拿到任何抓取目标,先跑一遍这张清单,任何一项不通过,就应该停下来:
| 检查项 | Boss 直聘的情况 | 判定 |
|---|
| robots.txt 是否允许目标路径 | Disallow: /*?*覆盖所有带参 URL | 不允许 |
| 是否需要登录才能看到内容 | 职位详情需登录 | 不允许抓取 |
| 使用条款是否禁止自动化访问 | 通常有相关条款 | 需自行核对 |
| 是否有官方 API / 开放平台 | 需走商务合作 | 优先走此路 |
| 抓取频率是否会打扰对方 | 大规模抓取必然造成压力 | 应避免 |
看清单就知道问题出在前两项,而不是出在「你的 Scrapy 写得不够好」。遇到 robots.txt 明确禁止的路径,正确动作是停止,而不是换 IP、换 UA、上代理池。
二、获取职位数据的正规路径
想做数据分析、职位趋势研究,下面这些路子既合规又往往更省事:
- 官方开放平台 / 商业数据接口。招聘平台大多有面向企业的开放能力,通过商务合作拿到结构化数据,质量远高于自己爬。
- 站点提供的导出功能。部分平台允许用户导出自己的投递记录或收藏,属于用户授权范围内。
- 公开数据集与行业报告。学术与行业机构会发布招聘相关的匿名化数据集,直接拿来研究完全没问题。
- 自建或明确授权的靶站。这也是本文第三节采用的方式:用一个允许抓取的页面练技术。
如果你的目的只是「练习 Scrapy」,那么抓什么站并不重要,重要的是解析、分页、清洗、落地这套链路。选一个允许抓取的靶子,你学到的东西一模一样,还不用担心踩线。
三、用允许抓取的靶子练同一套技术
招聘类页面的解析难点是固定的:卡片列表、分页、薪资区间文本、标签化的城市与经验要求。这些技术在任何结构相似的页面上都能练到。
先看解析这一环。Scrapy 的选择器能力来自parsel这个库,它可以脱离爬虫单独使用,非常适合离线调试解析规则——也就是说,你可以把一段允许使用的 HTML 存下来,反复调规则,完全不产生任何网络请求:
# 适用于 Python 3.8+;parsel 是 Scrapy 的依赖库,可单独导入
from parsel import Selector
html = """
<ul class="job-list">
<li class="job-card">
<h3 class="job-name">Python 后端开发</h3>
<span class="job-area">杭州</span>
<span class="salary">15-25K</span>
<a class="job-link" href="/job/123">查看</a>
</li>
</ul>
"""
sel = Selector(text=html)
for card in sel.css("li.job-card"):
row = {
"name": card.css("h3.job-name::text").get(),
"area": card.css("span.job-area::text").get(),
"salary": card.css("span.salary::text").get(),
"href": card.css("a.job-link::attr(href)").get(),
}
print(row)同样的选择器规则,放进 Spider 就是在真实页面上工作:
# 适用于 Python 3.8+,Scrapy 2.x
# 注意:start_urls 必须指向你被允许抓取的站点或你自建的页面
import scrapy
class JobListSpider(scrapy.Spider):
name = "job_list"
# 换成你拥有、或明确允许你抓取的靶站地址
start_urls = ["https://example.com/jobs/"]
allowed_domains = ["example.com"]
custom_settings = {
"ROBOTSTXT_OBEY": True, # 永远保留这一条
"CONCURRENT_REQUESTS_PER_DOMAIN": 1, # 单域名串行
"DOWNLOAD_DELAY": 1.5, # 请求间隔,宁慢勿快
"USER_AGENT": "job-demo/0.1 (+https://example.com/bot)",
}
def parse(self, response):
for card in response.css("li.job-card"):
yield {
"name": card.css("h3.job-name::text").get(),
"area": card.css("span.job-area::text").get(),
"salary": card.css("span.salary::text").get(),
"detail_url": response.urljoin(card.css("a.job-link::attr(href)").get()),
}
next_page = response.css("a.next::attr(href)").get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)把start_urls换成你自己的博客、公司官网、或者练习站点,这段代码就是完全合规的。技术是中性的,靶子的选择决定了它是否越界。
四、字段清洗:把文本变成可比的数据
招聘数据真正有价值的部分在于清洗。原始页面给你的是「15-25K」「15k-25k·13薪」「面议」这类文本,直接存下来没法做统计。清洗要单独写函数,并且只对能确定格式的值做转换,拿不准的一律保留原文:
# 适用于 Python 3.8+
import re
def parse_salary(text):
"""把薪资文本转成 (最小值, 最大值) 的千元整数;无法解析返回 (None, None)。"""
if not text:
return (None, None)
t = text.strip().lower().replace("k", "000")
nums = re.findall(r"\d+", t)
if not nums:
return (None, None)
if len(nums) == 1:
return (int(nums[0]), int(nums[0]))
return (int(nums[0]), int(nums[1]))
print(parse_salary("15-25K")) # (15000, 25000)
print(parse_salary("面议")) # (None, None)注意最后一行:「面议」这类无法解析的值必须老老实实返回(None, None),而不是硬凑一个 0——0 会被后续统计当成「薪资为零」的样本,把结论带偏。
常见坑点
1. 看到禁止就去研究怎么绕
❌ robots.txt 明确禁止,就改用代理池、伪造 UA、模拟设备指纹。 ✅ 被禁止就是被拒绝;改找官方 API、授权数据源,或换一个允许抓取的靶子。
2. 把「能跑通」当成「可以上线」
❌ 脚本跑通了就长期高频运行。 ✅ 上线前再过一遍合规清单:robots、条款、登录限制、频率影响。
3. 薪资字段直接存字符串
❌ 存下 "15-25K·13薪",后面做统计时无从下手。 ✅ 写成parse_salary这类清洗函数,无法解析的返回(None, None)而非 0。
4. 把不确定的值硬编码成默认值
❌ 解析不到就填 0 或空字符串,污染统计结果。 ✅ 用None表示缺失,让下游显式处理缺失。
5. 忘记urljoin补全相对链接
❌item["detail_url"] = "/job/123",单独请求时解析失败。 ✅ 用response.urljoin(...)或response.follow()。
6. 直接对列表页做详情页跟进,请求量翻十几倍
❌ 每个职位都开详情页,几千次请求压向对方。 ✅ 只取列表页必要字段,或严格限制详情页数量;练手阶段别做详情跟进。
7. 用 Python 2 的老代码
❌ 照抄print "..."、xrange之类的旧写法。 ✅ 改成print(...)、range、.items()。
8. 在parse里做网络请求之外的重活
❌ 在解析回调里同步处理大量数据,拖慢整条流水线。 ✅ 解析只管取字段,清洗与入库交给管道(Item Pipeline)。
总结
| 判断/做法 | 要点 |
|---|
| 先判断能不能抓 | 读 robots.txt、看是否需要登录、核条款 |
| Boss 直聘的结论 | Disallow: /*?*覆盖带参 URL,列表搜索页被禁止 |
| 合规替代 | 官方 API、授权数据、公开数据集、自建靶站 |
| 练技术 | parsel.Selector离线调试 + Scrapy 骨架 |
| 清洗 | 只转确定格式,解析不到返回None |
| 底线的表达 | 「不允许」就是不允许,不是「难度不够」 |
写爬虫的能力,一半在解析与清洗,一半在判断边界。前一半靠练,后一半靠纪律。当目标站明确说「不」的时候,一个成熟工程师的回应是换一条合规的路,而不是去修一条绕过它的路。把这篇文章里的技术练熟、把合规清单养成习惯,你就同时具备了这两半。