Scarnox · Blog

Scrapy 验证码中间件:在爬虫中识别验证码(2026)

Scrapy 是围绕请求和回调构建的,因此验证码并不能很自然地融入它。处理验证码的正确方式是使用下载中间件:它查看每一个响应,发现挑战,获取 token,然后让爬虫带着这个 token 提交表单。本指南为 PopularCaptcha 页面构建这样一个中间件,这是 Scarnox 目前以无代理方式支持的类型。

免费试用额度,无需信用卡,立即注册,并在把代码用于其他任何目标之前,先用一个测试页面试一试。

中间件如何工作

中间件对每个响应都会运行:

  1. 如果响应中没有 data-sitekey,就直接放行该响应。
  2. 如果有,中间件会识别验证码,并返回一份相同请求的副本,其中 token 保存在 meta 中。
  3. Scrapy 会再次抓取该请求。中间件在 meta 中看到 token,就放行该响应。
  4. 爬虫从 meta 中读取 token,并用 FormRequest.from_response 提交表单。

识别调用通过 deferToThread 在线程中运行,因此不会阻塞 Scrapy 的 reactor。在验证码识别的同时,其他请求仍然可以继续推进。

项目设置

把你的密钥放在环境变量中,而不是写进代码:

# settings.py
import os

SCARNOX_API_KEY = os.environ["SCARNOX_API_KEY"]
SCARNOX_TIMEOUT = 185

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.PopularCaptchaMiddleware": 543,
}

ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 3
CONCURRENT_REQUESTS_PER_DOMAIN = 1

下载延迟和每个域名的单一并发是刻意设置的。它们能降低网站的负载,也能减少你遇到的挑战次数。

中间件

# myproject/middlewares.py
import requests
from scrapy import Request
from scrapy.http import Response
from twisted.internet.threads import deferToThread

API_URL = "https://api.scarnox.com/api/tasks/create"


class PopularCaptchaMiddleware:
    def __init__(self, api_key: str, timeout: int):
        self.api_key = api_key
        self.timeout = timeout

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            api_key=crawler.settings.get("SCARNOX_API_KEY"),
            timeout=crawler.settings.getint("SCARNOX_TIMEOUT", 185),
        )

    def _solve(self, sitekey: str, page_url: str) -> str:
        r = requests.post(
            API_URL,
            headers={"Authorization": f"Bearer {self.api_key}"},
            json={
                "captcha_type": "PopularCaptchaTaskProxyless",
                "site_key": sitekey,
                "site_url": page_url,
            },
            timeout=self.timeout,  # 在工作线程中运行,而不是在 reactor 中
        )
        data = r.json()
        if data.get("errorId") != 0:
            raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
        return data["solution"]["token"]

    def process_response(self, request: Request, response: Response, spider):
        # 已经识别过,或者是我们自己构造的表单提交:直接放行。
        if request.meta.get("captcha_token") or request.meta.get("skip_captcha"):
            return response

        sitekey = response.css("[data-sitekey]::attr(data-sitekey)").get()
        if not sitekey:
            return response

        spider.logger.info("PopularCaptcha on %s, solving", response.url)

        def retry_with_token(token: str) -> Request:
            # 返回 Request 会重新调度它。dont_filter 用于绕过去重过滤器。
            return request.replace(
                dont_filter=True,
                meta={**request.meta, "captcha_token": token},
            )

        return deferToThread(self._solve, sitekey, response.url).addCallback(retry_with_token)

Scrapy 支持从 process_response 返回 Deferred。回调把 token 变成一个重试请求,Scrapy 会像调度其他请求一样调度它。

爬虫

# myproject/spiders/search.py
import scrapy
from scrapy.http import FormRequest


class SearchSpider(scrapy.Spider):
    name = "search"
    start_urls = ["https://search.example.com/search?q=web+scraping"]

    def parse(self, response):
        token = response.meta.get("captcha_token")
        if token and response.css("[data-sitekey]"):
            # Scrapy 会填充表单自身的字段;我们再加上 token。
            yield FormRequest.from_response(
                response,
                formdata={
                    "h-captcha-response": token,
                    "g-recaptcha-response": token,
                },
                callback=self.parse_results,
                meta={"skip_captcha": True},
                dont_filter=True,
            )
            return

        yield from self.parse_results(response)

    def parse_results(self, response):
        for item in response.css("div.result"):
            yield {
                "title": item.css("h3::text").get(),
                "url": item.css("a::attr(href)").get(),
            }

选择器只是占位符,请替换为你所抓取网站的真实选择器。

skip_captcha 标志用于阻止中间件再次识别表单提交。没有它,每次提交都会产生一次新的识别费用。

运行

export SCARNOX_API_KEY=scarnox_YOURAPIKEY
scrapy crawl search -O results.json

观察日志。出现类似 PopularCaptcha on ..., solving 的行,表示发现了挑战并请求了 token。如果每个页面都出现这一行,请放慢速度。频繁出现挑战,说明网站已经注意到了你的抓取。

Token 的时机

PopularCaptcha 的 token 大约两分钟后过期。中间件在 token 到达后立即请求重试,爬虫则在同一串回调中提交。请保持这条链路简短,不要在 token 与表单提交之间加入缓慢的下载或冗长的处理。关于这一时间窗口的更多信息,请参阅验证码 token 过期。

错误

识别失败会在线程中抛出 RuntimeError。Scrapy 会把它记录为一次失败的请求。识别失败不收费,因此重试成本很低。如果需要带上限的重试策略,请使用 Scrapy 内置的 RetryMiddleware,并在一个简短的循环中包装 _solve。我们关于验证码识别错误处理的指南列出了在生产环境中经受住考验的模式。

负责任地抓取

法律背景请参阅网页抓取合法吗?。

定价

Scarnox 按成功的识别收费,识别失败不收费。费率请查看当前定价。免费试用额度,无需信用卡,立即注册。

下一步