Scarnox · Blog

如何越过验证码抓取搜索结果(Python 指南,2026)

搜索结果页是爬虫最先遇到验证码的地方之一。网站看到同一个地址发出的一连串请求,于是返回一个挑战,在挑战通过之前不给出任何结果。要越过验证码抓取搜索结果,你的脚本需要三个步骤:在解析之前先检测挑战,用一次 API 调用识别它,然后提交 token,让网站返回真正的结果页。

本指南用 requests 和 BeautifulSoup 在 Python 中演示这一流程。它涵盖显示 PopularCaptcha 控件的页面,这是 Scarnox 目前支持的类型(无代理)。请先注册账户,免费试用额度,无需信用卡,并先在你自己的页面上运行这段代码。

验证码是什么样子的

搜索页通常把控件放在一个表单中。控件容器带有 data-sitekey 属性,表单会把数据回传给网站:

<form action="/search/verify" method="post">
  <input type="hidden" name="q" value="web scraping">
  <input type="hidden" name="lang" value="en">
  <div class="h-captcha" data-sitekey="YOUR-SITEKEY-UUID"></div>
  <button type="submit">Continue</button>
</form>

你的代码需要从这段标记中获取三样东西:sitekey、表单的 action URL,以及提交 token 时必须一并回传的隐藏字段。下面的辅助函数会读取这三项,所以无需硬编码它们。

如果你的目标控件是另一种验证码类型,请参阅验证码类型。Cloudflare Turnstile 和 reCAPTCHA 的支持即将推出,因此这里的代码只涵盖 PopularCaptcha。

识别请求

识别请求发送到 Scarnox API,并在 Bearer 请求头中带上你的密钥。调用会阻塞,直到 token 准备好,因此不需要轮询循环:

import time
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

API_KEY = "scarnox_YOURAPIKEY"
SEARCH_URL = "https://search.example.com/search"

session = requests.Session()
session.headers["User-Agent"] = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
)


def solve(sitekey: str, page_url: str) -> str:
    r = requests.post(
        "https://api.scarnox.com/api/tasks/create",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "captcha_type": "PopularCaptchaTaskProxyless",
            "site_key": sitekey,
            "site_url": page_url,
        },
        timeout=185,  # 调用会在 token 准备好后返回
    )
    data = r.json()
    if data.get("errorId") != 0:
        raise RuntimeError(f"{data.get('errorCode')}: {data.get('errorDescription')}")
    return data["solution"]["token"]

识别失败时,errorId 不为 0,而且不会向你收费。重试相关内容见验证码识别错误处理。

获取页面并通过验证码

抓取函数会先检查控件是否存在。如果不存在,就原样返回 HTML。如果存在,就识别挑战,用相同的隐藏字段重建表单,然后提交:

def fetch_results(query: str, page: int = 1) -> str:
    resp = session.get(SEARCH_URL, params={"q": query, "page": page}, timeout=30)
    soup = BeautifulSoup(resp.text, "html.parser")

    widget = soup.select_one("[data-sitekey]")
    if widget is None:
        return resp.text  # 此页面没有验证码

    form = widget.find_parent("form")
    if form is None:
        raise RuntimeError("Captcha widget is not inside a form")

    token = solve(widget["data-sitekey"], resp.url)

    payload = {
        field["name"]: field.get("value", "")
        for field in form.select("input[name]")
    }
    payload["h-captcha-response"] = token
    payload["g-recaptcha-response"] = token

    action = urljoin(resp.url, form.get("action") or resp.url)
    verified = session.post(action, data=payload, timeout=30)

    if "data-sitekey" in verified.text:
        raise RuntimeError("Challenge still shown after submit; token may be rejected")
    return verified.text

这里有两个细节很关键。site_url 是验证码实际提供的 URL(即 resp.url,包含所有重定向),而不是裸的搜索端点。另外,会话会在 GET 和 POST 之间保留 Cookie,许多网站需要这些 Cookie 才会接受 token。

Token 大约两分钟后过期,所以识别应当在读取表单之后立即进行,而不是提前。关于这个时间窗口的更多信息,请参阅验证码 token 过期。

解析结果

当返回真正的结果页之后,解析就只是普通的 HTML 工作了。下面的选择器只是占位符,请在开发者工具中打开页面,并替换为真实的选择器:

def parse_results(html: str) -> list[dict]:
    soup = BeautifulSoup(html, "html.parser")
    rows = []
    for item in soup.select("div.result"):
        link = item.select_one("a[href]")
        heading = item.select_one("h3")
        if link and heading:
            rows.append({
                "title": heading.get_text(strip=True),
                "url": link["href"],
            })
    return rows


if __name__ == "__main__":
    for page in range(1, 4):
        html = fetch_results("web scraping", page)
        for row in parse_results(html):
            print(row)
        time.sleep(5)  # 翻页之间控制请求间隔

请把每一页的输出分开保存。如果某页返回零条结果,先检查是否存在验证码控件,再假定查询确实没有结果。

负责任地抓取

验证码是一个信号,而不是漏洞。在对任何搜索网站运行这段代码之前:

关于法律方面,请参阅网页抓取合法吗?。关于如何从根本上避免被封,请参阅抓取时如何不被封禁。

定价

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

下一步