常见的反爬虫机制:
- 基于请求头 (Headers) 的反爬:检查
User-Agent
,Referer
,Cookie
等字段,识别非浏览器行为。 - 基于 IP 的反爬:检测单个 IP 在短时间内的访问频率,超过阈值则封禁 IP。
- 动态页面加载 (JavaScript渲染):页面内容由 JS 异步生成,直接请求 HTML 无法获取数据。
- 验证码 (CAPTCHA):要求用户进行交互式验证,如输入字符、滑动拼图等,以区分人与机器。
- JavaScript 加密/混淆:请求参数(如
token
,sign
)或数据本身由复杂的 JS 代码生成或加密,难以直接模拟。 - 字体反爬:使用自定义字体文件,将真实文本映射为乱码或私有编码,使得直接复制或解析 HTML 得到的内容无效。
浏览器自动化爬取
当数据是由 JS 动态加载,且找不到其后端 API 接口时,最后的“杀手锏”就是模拟真实用户操作浏览器
核心思想:启动一个真实的浏览器(或无头浏览器),执行 JS 代码渲染出完整页面后,再从中提取数据。
Playwright简介
工具选择:Selenium
vs. Playwright
特性 | Selenium |
Playwright (更推荐) |
---|---|---|
定位 | Web 自动化测试的元老 | 微软出品,专为现代 Web 应用打造 |
架构 | WebDriver 中间层,通信有额外开销 | 通过 WebSocket 直接通信,速度更快 |
API | 同步阻塞 | 原生支持异步 (async/await ) |
功能 | 功能全面 | 自动等待、网络拦截、设备模拟等强大功能 |
安装 | pip install selenium |
pip install playwright & playwright install |
Playwright 核心用法示例
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
# 启动 Chromium 浏览器 (headless=True 表示无头模式)
browser = await p.chromium.launch(headless=False)
page = await browser.new_page()
# 1. 访问页面
await page.goto("http://quotes.toscrape.com/js/")
# 2. 等待特定元素出现,确保 JS 已加载完成
await page.wait_for_selector("div.quote")
# 3. 执行操作:滚动到底部以加载更多内容
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await page.wait_for_timeout(1000) # 等待1秒让内容加载
# 4. 获取页面源码
html = await page.content()
# 现在你可以用 BeautifulSoup 或 lxml 解析这个完整的 html 了
print("页面标题:", await page.title())
# 5. 关闭浏览器
await browser.close()
if __name__ == "__main__":
asyncio.run(main())
适用场景:
- 页面由复杂的前端框架(如 React, Vue)生成。
- 无法找到数据接口,或接口加密极其复杂。
- 需要模拟登录、点击、滚动等复杂用户交互。
scrapy-playwright
scrapy-playwright:唯一作用就是将 playwright 的功能接入到 Scrapy 的生态中
使用 Scrapy 时,可以将 Playwright 作为 Scrapy 的一个“超级下载器”来使用。
- 安装与配置:
# pip install scrapy-playwright
uv add scrapy-playwright
# 安装浏览器驱动:
uv run playwright install
- 在
settings.py
中启用:
# settings.py
# 启用 scrapy-playwright 的下载处理器
DOWNLOAD_HANDLERS = {
"http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
"https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}
# 启用异步支持 (Twisted 的 asyncio 反应器)
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
# (可选) 配置 Playwright 的行为
# PLAYWRIGHT_LAUNCH_OPTIONS = {
# "headless": True, # 默认为 True,即无头模式
# "timeout": 20 * 1000, # 20 秒超时
# }
- 正常模式 (Headed Mode):当你运行自动化脚本时,一个看得见的、带有图形用户界面 (GUI) 的浏览器窗口会弹出来。你可以实时看到脚本正在执行的每一步操作,比如打开网页、点击按钮、输入文字等。这对于调试脚本非常有帮助。
- 无头模式 (Headless Mode):浏览器在后台运行,没有可视化的窗口界面,比正常模式运行得更快,资源占用也更少,适用于服务器环境
- 在 Spider 中使用 Playwright
启用后,在 Spider 中发起请求时,只需要在
scrapy.Request
的meta
字典中添加playwright=True
即可
import scrapy
class JsQuotesSpider(scrapy.Spider):
name = 'js_quotes'
def start_requests(self):
url = "http://quotes.toscrape.com/js/"
# 在 meta 中添加 'playwright': True 来触发 Playwright
yield scrapy.Request(url, meta={"playwright": True})
def parse(self, response):
# 此时的 response.body 是 Playwright 渲染后的完整 HTML
# 我们可以像往常一样使用 CSS 或 XPath 选择器
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
}
Playwright页面交互
scrapy-playwright 不仅仅是渲染页面,通过在 meta
中传递 playwright_page_methods
,还能在请求中定义复杂的页面交互动作
示例:模拟滚动和点击
假设一个页面需要先点击“加载更多”按钮,然后再滚动到底部才能显示所有内容:
import scrapy
from scrapy_playwright.page import PageMethod
class InteractiveSpider(scrapy.Spider):
name = 'interactive'
def start_requests(self):
url = "https://example.com/infinite_scroll"
yield scrapy.Request(
url,
meta={
"playwright": True,
"playwright_page_methods": [
# 1. 等待“加载更多”按钮出现
PageMethod("wait_for_selector", "button#load-more"),
# 2. 点击按钮
PageMethod("click", "button#load-more"),
# 3. 等待网络空闲,确保新内容已加载
PageMethod("wait_for_load_state", "networkidle"),
# 4. 滚动到底部
PageMethod("evaluate", "window.scrollTo(0, document.body.scrollHeight)"),
# 5. 等待 2 秒
PageMethod("wait_for_timeout", 2000),
],
}
)
def parse(self, response):
# response.body 现在包含了所有交互后加载的内容
for item in response.css('div.item'):
yield {'title': item.css('h2::text').get()}
IP 代理池 (对抗 IP 封禁)
当网站通过限制单个 IP 的访问频率来反爬时,我们需要使用代理 IP 来伪装我们的真实来源。
核心思想:维护一个包含大量可用代理 IP 的池子。每次请求时,随机从中抽取一个 IP 来发送请求,并在请求失败时自动更换。
代理 IP 的来源:
- 免费代理:网上有许多免费代理网站。优点:免费。缺点:速度慢、匿名度低、极不稳定,通常只适合学习。
- 付费代理:专业的代理服务商。优点:稳定、高速、匿名度高。缺点:收费。对于正式项目,这是唯一可靠的选择。
构建一个简单的代理池(原理演示)
import requests
def get_proxies():
# 这是一个示例,实际中需要从代理服务商API或你自己的代理池获取
return [
"http://user:pass@host1:port",
"http://user:pass@host2:port",
]
def check_proxy(proxy):
"""检查代理是否可用"""
try:
# 使用 httpbin.org 测试代理
response = requests.get("https://httpbin.org/ip",
proxies={"http": proxy, "https": proxy},
timeout=5)
if response.status_code == 200:
print(f"代理 {proxy} 可用, IP: {response.json()['origin']}")
return True
except requests.RequestException:
print(f"代理 {proxy} 不可用")
return False
# 简单的代理池逻辑
available_proxies = [p for p in get_proxies() if check_proxy(p)]
print(f"可用的代理IP: {available_proxies}")
在 Scrapy 中使用代理: 通常通过编写一个下载中间件 (Downloader Middleware) 来实现。
# middlewares.py
import random
class ProxyMiddleware:
def __init__(self, proxies):
self.proxies = proxies
@classmethod
def from_crawler(cls, crawler):
# 从 settings 中读取代理列表
return cls(proxies=crawler.settings.getlist('PROXIES'))
def process_request(self, request, spider):
# 为每个请求随机设置一个代理
proxy = random.choice(self.proxies)
request.meta['proxy'] = proxy
spider.log(f"使用代理: {proxy}")
# settings.py
# 启用中间件
# DOWNLOADER_MIDDLEWARES = {
# 'my_project.middlewares.ProxyMiddleware': 543,
# }
# PROXIES = ['http://proxy1:port', 'http://proxy2:port']
验证码 (CAPTCHA) 的识别
验证码是区分人机最直接的方式。完全自动化的破解非常困难,但有几种常见的应对策略。
策略一:手动介入
- 当爬虫遇到验证码时,暂停程序,将验证码图片下载到本地,并在命令行中等待用户输入。适用于一次性或小规模任务。
策略二:Cookie 复用
- 先用自己的浏览器手动登录,通过验证码,然后将登录后的 Cookies 复制到爬虫的请求头中。爬虫携带有效的 Cookies 进行访问,从而绕过验证码。
策略三:第三方打码平台
- 这是最常用的自动化方案。将验证码图片发送给专业的打码平台(如
dama2.com
,2Captcha
),平台由人工或 AI 进行识别,并将结果返回给你。
使用打码平台 API 示例 (伪代码)
import requests
def solve_captcha(image_url):
api_key = "YOUR_API_KEY"
platform_api_url = "http://api.captcha-solver.com/solve"
# 1. 下载验证码图片
image_data = requests.get(image_url).content
# 2. 发送给打码平台
response = requests.post(platform_api_url,
files={'image': image_data},
data={'key': api_key})
# 3. 获取识别结果
captcha_text = response.json().get('text')
return captcha_text
# captcha_result = solve_captcha("http://example.com/captcha.jpg")
# 然后用 captcha_result 去提交表单
JavaScript 加密混淆逆向
这是反爬中最具挑战性的部分。当你发现请求参数中有一个像 sign
或 token
这样的加密字段时,就需要进行 JS 逆向。
核心流程:
- 抓包分析:在开发者工具的 Network 面板中,找到包含加密参数的请求。
- 定位加密逻辑:
- 全局搜索:在 Sources 面板中,全局搜索加密参数的名称(如 “sign”)。
- 事件监听断点:在 Elements 面板中,找到触发请求的按钮,为其添加“事件监听断点”(如
click
),然后点击按钮,程序会停在相关的 JS 代码处。 - XHR 断点:在 Sources 面板中,添加 XHR/Fetch 断点,当包含特定 URL 的请求发出时,程序会自动暂停。
- 调试与分析:在断点处,通过查看调用栈 (Call Stack) 和作用域 (Scope) 中的变量,一步步回溯,找到加密函数的源码。
- 代码复现:将找到的核心 JS 加密函数复制出来,用 Python 来调用它。
- 方法一:
PyExecJS
或PyV8
:在 Python 环境中执行 JS 代码。适合不依赖浏览器环境(如window
,document
)的纯计算型加密。 - 方法二:RPC 调用:用 Node.js 启动一个简单的 Web 服务,将加密函数封装成一个 API 接口。Python 通过
requests
调用这个接口来获取加密结果。这是更稳定、更推荐的方式。
- 方法一:
PyExecJS
示例
import execjs
# 假设我们找到了核心的加密函数
js_code = """
function getSign(data) {
// 这是一个简化的示例,实际代码可能很复杂
return data + "_signed";
}
"""
# 编译 JS 代码
ctx = execjs.compile(js_code)
# 调用 JS 函数
data_to_sign = "my-request-data"
sign = ctx.call('getSign', data_to_sign)
print(f"生成的 sign: {sign}")
字体反爬的破解
当页面上的数字或文字显示正常,但复制出来却是乱码时,很可能遇到了字体反爬。
核心原理:网站定义了一个自定义字体 (.woff
或 .ttf
文件)。在这个字体文件中,字符的编码(如 
)与真实字符(如 8
)的对应关系是自定义的。浏览器会自动渲染出正确的字符,但爬虫直接获取的 HTML 源码只是编码。
破解流程:
- 找到字体文件:在开发者工具 Network 面板中,筛选
Font
,找到.woff
或.ttf
文件的 URL 并下载。 - 分析映射关系:
- 在 Elements 面板中,你会看到类似
<span class="price"></span>
的结构。 - 我们需要建立

->真实字符
的映射。
- 在 Elements 面板中,你会看到类似
- 解析字体文件:使用 Python 的
fontTools
库来读取字体文件,获取编码和字形名称(或形状)的对应关系。 - 建立映射字典:根据网站的逻辑(有时需要对比多个字体文件或结合 CSS 分析),建立起
编码 -> 真实字符
的dict
。 - 文本替换:用这个映射字典,将爬取到的乱码文本替换为真实文本。
fontTools
核心用法示例
from fontTools.ttLib import TTFont
# 1. 加载字体文件
font = TTFont('my_font.woff')
# 2. 获取编码到字形名称的映射 ( cmap )
# 这是一个示例,具体逻辑可能更复杂
cmap = font.getBestCmap()
print(cmap) # {57345: 'uniE001', 57346: 'uniE002', ...} (编码 -> 字形名)
# 3. 假设我们通过分析知道 'uniE001' 对应 '8','uniE002' 对应 '1'
# 建立最终的映射字典
mapping = {
'': '8', # 57345 的十六进制是 0xe001
'': '1',
}
# 4. 替换文本
raw_text = ""
for code, char in mapping.items():
raw_text = raw_text.replace(code, char)
print(f"还原后的文本: {raw_text}")