进阶与反爬虫对抗

常见的反爬虫机制:

  1. 基于请求头 (Headers) 的反爬:检查 User-Agent, Referer, Cookie 等字段,识别非浏览器行为。
  2. 基于 IP 的反爬:检测单个 IP 在短时间内的访问频率,超过阈值则封禁 IP。
  3. 动态页面加载 (JavaScript渲染):页面内容由 JS 异步生成,直接请求 HTML 无法获取数据。
  4. 验证码 (CAPTCHA):要求用户进行交互式验证,如输入字符、滑动拼图等,以区分人与机器。
  5. JavaScript 加密/混淆:请求参数(如 token, sign)或数据本身由复杂的 JS 代码生成或加密,难以直接模拟。
  6. 字体反爬:使用自定义字体文件,将真实文本映射为乱码或私有编码,使得直接复制或解析 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 核心用法示例

python
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 的一个“超级下载器”来使用。

  1. 安装与配置:
bash
# pip install scrapy-playwright
uv add scrapy-playwright

# 安装浏览器驱动:
uv run playwright install
  1. settings.py 中启用:
python
# 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):浏览器在后台运行,没有可视化的窗口界面,比正常模式运行得更快,资源占用也更少,适用于服务器环境
  1. 在 Spider 中使用 Playwright 启用后,在 Spider 中发起请求时,只需要在 scrapy.Requestmeta 字典中添加 playwright=True 即可
python
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,还能在请求中定义复杂的页面交互动作

示例:模拟滚动和点击

假设一个页面需要先点击“加载更多”按钮,然后再滚动到底部才能显示所有内容:

python
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 的来源

  1. 免费代理:网上有许多免费代理网站。优点:免费。缺点:速度慢、匿名度低、极不稳定,通常只适合学习。
  2. 付费代理:专业的代理服务商。优点:稳定、高速、匿名度高。缺点:收费。对于正式项目,这是唯一可靠的选择

构建一个简单的代理池(原理演示)

python
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) 来实现。

python
# 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 示例 (伪代码)

python
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 加密混淆逆向

这是反爬中最具挑战性的部分。当你发现请求参数中有一个像 signtoken 这样的加密字段时,就需要进行 JS 逆向。

核心流程

  1. 抓包分析:在开发者工具的 Network 面板中,找到包含加密参数的请求。
  2. 定位加密逻辑
    • 全局搜索:在 Sources 面板中,全局搜索加密参数的名称(如 “sign”)。
    • 事件监听断点:在 Elements 面板中,找到触发请求的按钮,为其添加“事件监听断点”(如 click),然后点击按钮,程序会停在相关的 JS 代码处。
    • XHR 断点:在 Sources 面板中,添加 XHR/Fetch 断点,当包含特定 URL 的请求发出时,程序会自动暂停。
  3. 调试与分析:在断点处,通过查看调用栈 (Call Stack)作用域 (Scope) 中的变量,一步步回溯,找到加密函数的源码。
  4. 代码复现:将找到的核心 JS 加密函数复制出来,用 Python 来调用它。
    • 方法一:PyExecJSPyV8:在 Python 环境中执行 JS 代码。适合不依赖浏览器环境(如 window, document)的纯计算型加密。
    • 方法二:RPC 调用:用 Node.js 启动一个简单的 Web 服务,将加密函数封装成一个 API 接口。Python 通过 requests 调用这个接口来获取加密结果。这是更稳定、更推荐的方式。

PyExecJS 示例

python
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 源码只是编码。

破解流程

  1. 找到字体文件:在开发者工具 Network 面板中,筛选 Font,找到 .woff.ttf 文件的 URL 并下载。
  2. 分析映射关系
    • 在 Elements 面板中,你会看到类似 <span class="price">&#xe001;&#xe002;&#xe003;</span> 的结构。
    • 我们需要建立 &#xe001; -> 真实字符 的映射。
  3. 解析字体文件:使用 Python 的 fontTools 库来读取字体文件,获取编码和字形名称(或形状)的对应关系。
  4. 建立映射字典:根据网站的逻辑(有时需要对比多个字体文件或结合 CSS 分析),建立起 编码 -> 真实字符dict
  5. 文本替换:用这个映射字典,将爬取到的乱码文本替换为真实文本。

fontTools 核心用法示例

python
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 = {
    '&#xe001;': '8', # 57345 的十六进制是 0xe001
    '&#xe002;': '1',
}

# 4. 替换文本
raw_text = "&#xe001;&#xe002;"
for code, char in mapping.items():
    raw_text = raw_text.replace(code, char)
print(f"还原后的文本: {raw_text}")