爬虫的基础积木——每个动作能得到什么效果
前两讲我们解决了"爬虫是什么"和"有哪些采集通道"的问题。
这一讲开始进入实战的第一步:把爬虫拆成一个个最小的"动作积木"。
只要理解每块积木"输入什么、输出什么、解决什么问题",后面再看真实项目里那些上千行的脚本,就会发现——它们不过是这些积木的反复组合和封装而已。
讲按照"发请求 → 解析内容 → 操作浏览器 → 监听接口 → 落盘存储 → 控制节奏 → 处理异常"这个顺序展开,因为这正是数据从网络流到硬盘上的真实路径。
一、最底层的积木:发一个 HTTP 请求
爬虫的本质就是"程序代替人去发请求"。所以第一块积木,就是"发请求"。
1.1 requests.get(url) —— 拿到网页源码或接口数据
import requests
resp = requests.get("https://example.com")
print(resp.status_code) # 200 表示成功
print(resp.text) # 网页 HTML 源码(字符串)
print(resp.json()) # 如果是 API,直接解析成 Python 字典
能得到什么:
- 如果 URL 是网页,
resp.text就是 HTML 字符串,后面交给解析器处理。 - 如果 URL 是 API 接口,
resp.json()直接得到结构化数据(字典/列表),不需要任何解析。
对应到项目里:
通用结构化系统中"API 直连通道"(auto_api/baidu_data_proc_api.py)用的就是这种方式——百度、京东、金蝶等公司的招聘接口直接返回 JSON,根本不需要打开浏览器,效率最高、最稳定。
1.2 给请求加"伪装" —— headers 和 cookies
光会 get 还不够。很多网站一看你是个"裸请求"(没有 User-Agent、没有 Referer),直接拒绝你。
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...",
"Referer": "https://www.liepin.com/",
}
resp = requests.get(url, headers=headers, cookies={"sessionid": "xxx"})
能得到什么: 让服务器以为"这是个正常用户从浏览器点过来的",大幅提高成功率。
这是反爬的第一道门槛,也是项目里所有 HTTP 请求都会带上 headers 的原因。
1.3 加代理 —— 让请求换个"出口 IP"
proxies = {"http": "http://1.2.3.4:8080", "https": "http://1.2.3.4:8080"}
resp = requests.get(url, proxies=proxies)
能得到什么: 同一个 IP 抓太多次会被封,代理能让你"换张脸"继续抓。
对应到项目里: spider.py 基类里的 getProxy() / setProxyRecord() 就是把代理做成了一个"资源池"——不是一次性用,而是"领用 → 上报使用情况 → 触达上限就换",这是工程化爬虫和玩具爬虫最大的区别之一。
二、解析积木:从 HTML / JSON 里把数据"挑"出来
请求拿回来的东西要么是 HTML(一坨标签),要么是 JSON(一坨嵌套字典)。我们要把"职位标题、薪资、公司名"这些字段从里面挑出来。
2.1 BeautifulSoup —— 从 HTML 里提取内容
from bs4 import BeautifulSoup
soup = BeautifulSoup(resp.text, "lxml")
title = soup.select_one("h1.job-title").get_text(strip=True)
links = [a["href"] for a in soup.select("a.job-link")]
能得到什么: 用类似 CSS 选择器的语法,把 HTML 里的标题、链接、文本批量提出来。
适用场景: 静态页面、详情页(项目里猎聘抓职位描述、地址、标签就是用这种方式)。
2.2 json.loads() —— 解析接口返回
import json
data = json.loads(resp.text)
job_list = data["data"]["jobCardList"]
for job in job_list:
print(job["title"], job["salary"])
能得到什么: 接口返回的数据天然就是结构化的,直接按 key 取就行,不用费力解析 HTML。
对应到项目里: 猎聘的 onResponse() 拿到 pc-search-job 接口返回后,直接读 ret['data']['data']['jobCardList']——这就是为什么它"列表页靠接口"比"列表页靠 DOM"稳定得多。
2.3 正则表达式 —— 从乱糟糟的文本里抠特定模式
import re
salary = re.search(r"(\d+)k-(\d+)k", text)
能得到什么: 当数据混在长文本里(比如职位描述里夹着"3-5年经验"),用正则一抓一个准。项目里"届次推断"、"URL 归一化"都用正则。
三、浏览器操作积木:让 Playwright 像人一样动
当网站用 JavaScript 动态渲染、需要登录、需要点击展开时,requests 就不够用了——你必须真的"开一个浏览器"。
3.1 启动浏览器并打开页面
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False) # False 表示能看到浏览器窗口
page = browser.new_page()
page.goto("https://www.liepin.com/zhaopin/")
page.wait_for_load_state("networkidle") # 等所有网络请求结束
html = page.content() # 拿到完整渲染后的 HTML
能得到什么: 拿到的是 JS 执行完毕后的最终 HTML,而不是原始的"骨架"HTML。这是抓动态网站的关键。
3.2 模拟用户操作
page.click("button.search-btn") # 点击按钮
page.fill("input.keyword", "Python 工程师") # 输入文字
page.select_option("select.city", "北京") # 下拉选择
page.mouse.wheel(0, 3000) # 滚动 3000 像素
能得到什么: 触发筛选、翻页、加载更多——就像真人在用网站。
对应到项目里: 猎聘 __selectItem() 通过点击控件选公司性质、规模、经验,鱼泡爬虫还要"模拟滑块拖动"过验证——本质都是这几个 API 的组合。
3.3 反自动化伪装
page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
能得到什么: 隐藏掉"我是个自动化工具"的标志,让网站不容易识破。
3.4 拦截资源,加速 + 省流量
def block_resource(route):
if route.request.resource_type in ("image", "font", "media"):
route.abort()
else:
route.continue_()
context.route("**/*", block_resource)
能得到什么: 不下载图片和字体,页面打开速度翻倍,代理流量也省一大半。猎聘和鱼泡爬虫全都用了这招。
四、监听积木:page.on("response") —— 偷听浏览器和服务器的对话
这是一个非常巧妙的技术,也是前面提到的"第四种采集通道"的核心。
def on_response(response):
if "pc-search-job" in response.url:
data = response.json()
print("拦截到职位列表数据:", data)
page.on("response", on_response)
page.goto("https://www.liepin.com/zhaopin/")
能得到什么: 我们让浏览器正常打开页面、正常和服务器对话,但我们在旁边"偷听",把浏览器收到的接口数据截下来自己用。
为什么这招好用?
- 不用自己研究接口签名(很多接口有复杂的 token、加密参数,自己构造很麻烦)
- 不用解析复杂的 HTML
- 拿到的就是干净的 JSON
对应到项目里: 猎聘所有列表数据都是这么拿的——这就是"页面操作驱动 + 接口监听获取"混合方案的精髓。
五、存储积木:把抓下来的东西存好
工程化爬虫一定不是"抓完直接入库",而是"先落盘,再入库"。
5.1 写 JSON 文件
import json
with open("job_123.json", "w", encoding="utf-8") as f:
json.dump(job_data, f, ensure_ascii=False, indent=2)
能得到什么: 抓到的原始数据先存成文件,任何一步出错都能从文件重跑,不用重新抓。
5.2 文件状态机:.ok / .err / .expired
import os
os.rename("job_123.json", "success/job_123.json.ok") # 入库成功
os.rename("job_123.json", "failed/job_123.json.err") # 入库失败
能得到什么: 一眼看出每个任务的状态,失败的可以批量重试,成功的可以归档。
对应到项目里: 爬虫逻辑分析里反复强调的"先文件、后接口"——com/、job/、success/、failed/、progress/、clean/ 这套目录就是一个手工版的"任务状态机"。
5.3 进度文件 / 状态文件:断点续跑的灵魂
# 每抓完一批就写一次
with open("progress.txt", "w") as f:
f.write(str(last_id))
# 启动时读
last_id = int(open("progress.txt").read()) if os.path.exists("progress.txt") else 0
能得到什么: 程序中断了再启动,从上次的位置继续,不用从头来一遍。
对应到项目里: 猎聘公司版的 spider_status_<ComId>.json 就是这个的豪华版——它不仅记录"抓到哪了",还记录"哪些条件组合已经抓完了",甚至做了临时文件 + 原子重命名 + 备份恢复这种数据库级别的可靠性保证。
六、节奏控制积木:别让网站发现你是机器
6.1 随机休眠
import random, time
time.sleep(random.uniform(1.5, 4.0))
能得到什么: 请求之间有随机间隔,看起来更像人类。猎聘、鱼泡都有 randomSleep()。
6.2 去重判断
if job_id in already_spider_today:
continue # 今天抓过,跳过
能得到什么: 不重复抓,省时间省代理额度。项目里有四层去重:今日是否抓过 → 公司维度是否抓过 → 数据库是否已存在 → 是否在黑名单。
七、异常处理积木:网络是不可靠的
7.1 try / except + 重试
for i in range(3):
try:
resp = requests.get(url, timeout=10)
break
except Exception as e:
print(f"第 {i+1} 次失败: {e}")
time.sleep(2 ** i) # 指数退避:1秒、2秒、4秒
能得到什么: 网络抖动、临时被封、超时——都不会让程序崩掉,而是"等等再来"。
对应到项目里: spider_yupao_clean.py 用的就是"指数退避 + 多选择器兼容"组合,这是工程化爬虫的标配。
7.2 风控识别
if page.title() == "猎聘": # 正常页应该是"职位搜索 - 猎聘"
print("可能被拦截")
change_proxy()
能得到什么: 主动识别"我被发现了",立刻换代理或休眠,避免被永久封禁。
把这些积木拼起来,就是一个最小爬虫
我们可以现在就把上面所有积木串成一个 30 行的小例子,看看一个"麻雀虽小五脏俱全"的爬虫长什么样:
import requests, json, time, random, os
from bs4 import BeautifulSoup
headers = {"User-Agent": "Mozilla/5.0 ..."}
visited = set()
def fetch_list(page_no):
url = f"https://example.com/jobs?page={page_no}"
resp = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, "lxml")
return [(a.get_text(), a["href"]) for a in soup.select("a.job-link")]
def fetch_detail(url):
resp = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, "lxml")
return {
"title": soup.select_one("h1").get_text(strip=True),
"desc": soup.select_one(".job-desc").get_text(strip=True),
}
def save(job_id, data):
os.makedirs("jobs", exist_ok=True)
with open(f"jobs/{job_id}.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
for page_no in range(1, 6):
for title, link in fetch_list(page_no):
job_id = link.split("/")[-1]
if job_id in visited:
continue
try:
detail = fetch_detail(link)
save(job_id, {"title": title, "link": link, **detail})
visited.add(job_id)
except Exception as e:
print(f"出错: {e}")
time.sleep(random.uniform(1, 3))
这就是一个完整的、能跑的、有去重有异常处理的最小爬虫。它包含了:发请求 → 解析 → 翻页 → 详情页 → 落盘 → 去重 → 节奏控制 → 异常处理 这 8 块积木。
真实项目(比如猎聘那 1000 多行的 spider_liepin_company.py)做的事情其实没有本质上的不同,只是把每块积木做强、做大、做工程化了:
- "发请求"被升级成了 Playwright + 代理池 + 反自动化
- "解析"分成了"接口监听拿列表 + DOM 拿详情"
- "去重"变成了四层去重
- "落盘"变成了完整的状态机目录
- "进度记录"变成了带备份恢复的状态文件系统
- "异常处理"变成了风控识别 + 自动换代理 + 跨日恢复
组内导航:⬅️ 四重采集通道 | 🏠 00-爬虫课程六讲 | ➡️ 用积木拼一个最小职位爬虫
💬 评论