Use Cases

药房价格比较的验证码处理

开发跨境药品比价工具,抓取美国药房门户和保险处方目录时,验证码是绕不开的第一道坎:搜索表单和结果页普遍部署 reCAPTCHA v2,部分叠加 Turnstile 或图片验证码。

示例仅覆盖你有权访问的公开定价页面,请遵守当地合规要求。

药房网站的验证码都藏在哪些页面

页面类型 常见验证码 触发场景
药品搜索表单 reCAPTCHA v2 每次搜索请求
价格结果页 Cloudflare Turnstile 疑似自动化访问
药房定位器 reCAPTCHA v2 基于地理位置的查询
优惠券查询 图片验证码 展示折扣码之前
保险处方目录 reCAPTCHA v2 登录或搜索环节

控制请求频率,减少触发验证码

策略 具体做法
错开请求节奏 每次搜索间隔 5–10 秒
定期轮换会话 每 20–30 次查询更换新会话
变化搜索模式 避免连续重复搜索同一种药品
关注出口 IP 质量 数据中心 IP 比普通网络更容易触发验证码

用 CaptchaAI 搭建基础识别流程

基础流程分四步:

  1. 加载搜索页,提取 sitekey
  2. 提交给 in.php
  3. 轮询 res.php 拿到 token
  4. token 随参数一起提交表单
import requests
import time

session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})

def solve_recaptcha(site_key, page_url):
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": "YOUR_API_KEY",
        "method": "userrecaptcha",
        "googlekey": site_key,
        "pageurl": page_url,
        "json": 1
    })
    task_id = resp.json()["request"]

    for _ in range(60):
        time.sleep(3)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": "YOUR_API_KEY",
            "action": "get",
            "id": task_id,
            "json": 1
        })
        data = result.json()
        if data["status"] == 1:
            return data["request"]
    raise TimeoutError("Solve timed out")

def search_drug_prices(drug_name, zipcode):
    search_url = "https://pharmacy.example.com/search"

    # Load the search page to get the site key
    page = session.get(search_url)

    # Extract reCAPTCHA site key
    import re
    match = re.search(r'data-sitekey="([^"]+)"', page.text)
    site_key = match.group(1)

    # Solve the CAPTCHA
    token = solve_recaptcha(site_key, search_url)

    # Submit the search with token
    results = session.post(search_url, data={
        "drug": drug_name,
        "zip": zipcode,
        "g-recaptcha-response": token
    })

    return parse_prices(results.text)

故障排查清单

问题现象 可能原因 处理方式
每次搜索都弹出验证码 会话未保留 cookie 使用 requests.Session()
拿到 token 后价格不出来 缺少 CSRF token 携带隐藏表单字段一起提交
多次搜索后被封 触发限流 请求间加延迟
价格和浏览器里不一样 缺少会话 cookie 或请求头 匹配浏览器请求头

同时比价多个药房来源

把来源封装成配置列表,按验证码类型分派逻辑,按最低价排序:

PHARMACY_SOURCES = [
    {
        "name": "PharmacyA",
        "url": "https://pharmacya.example.com/pricing",
        "captcha_type": "recaptcha_v2"
    },
    {
        "name": "PharmacyB",
        "url": "https://pharmacyb.example.com/drugs",
        "captcha_type": "turnstile"
    },
    {
        "name": "PharmacyC",
        "url": "https://pharmacyc.example.com/search",
        "captcha_type": "image"
    }
]

def compare_drug_price(drug_name, zipcode):
    results = []

    for source in PHARMACY_SOURCES:
        try:
            prices = fetch_prices(
                source["url"],
                source["captcha_type"],
                drug_name,
                zipcode
            )
            results.append({
                "source": source["name"],
                "prices": prices,
                "status": "success"
            })
        except Exception as e:
            results.append({
                "source": source["name"],
                "error": str(e),
                "status": "failed"
            })

    return sorted(results, key=lambda r: r.get("prices", {}).get("lowest", float("inf")))

应对不同厂商的验证码类型

不同门户按类型分支处理,再把对应的 token 字段名传给表单:

def fetch_prices(url, captcha_type, drug_name, zipcode):
    page = session.get(url)

    if captcha_type == "recaptcha_v2":
        import re
        match = re.search(r'data-sitekey="([^"]+)"', page.text)
        token = solve_recaptcha(match.group(1), url)
        field_name = "g-recaptcha-response"

    elif captcha_type == "turnstile":
        import re
        match = re.search(r'data-sitekey="(0x[^"]+)"', page.text)
        token = solve_turnstile(match.group(1), url)
        field_name = "cf-turnstile-response"

    elif captcha_type == "image":
        img_data = extract_captcha_image(page.text)
        token = solve_image_captcha(img_data)
        field_name = "captcha"

    return session.post(url, data={
        "drug": drug_name,
        "zip": zipcode,
        field_name: token
    })
验证码类型 识别方法 token 字段名
reCAPTCHA v2 solve_recaptcha g-recaptcha-response
Turnstile solve_turnstile cf-turnstile-response
图片验证码 solve_image_captcha captcha

用会话管理压低验证码触发率

核心做法:

  1. 复用同一个 session,保留 cookie
  2. searches_since_captcha 计数
  3. 命中验证码页面时重新识别并清零计数
class PharmacySession:
    def __init__(self, base_url):
        self.session = requests.Session()
        self.base_url = base_url
        self.searches_since_captcha = 0

    def search(self, drug_name, zipcode):
        result = self.session.post(f"{self.base_url}/search", data={
            "drug": drug_name,
            "zip": zipcode
        })

        if self.is_captcha_page(result.text):
            token = self.solve_page_captcha(result.text)
            result = self.session.post(f"{self.base_url}/search", data={
                "drug": drug_name,
                "zip": zipcode,
                "g-recaptcha-response": token
            })
            self.searches_since_captcha = 0
        else:
            self.searches_since_captcha += 1

        return result

    def is_captcha_page(self, html):
        return "g-recaptcha" in html or "cf-turnstile" in html

    def solve_page_captcha(self, html):
        import re
        match = re.search(r'data-sitekey="([^"]+)"', html)
        return solve_recaptcha(match.group(1), self.base_url)

Node.js 实现

思路和 Python 版本一致:并发请求各来源,识别拿到 token,再按价格排序:

async function comparePharmacyPrices(drugName, zipCode, sources) {
  const results = await Promise.all(
    sources.map(async (source) => {
      try {
        const price = await fetchDrugPrice(source, drugName, zipCode);
        return { source: source.name, price, status: 'success' };
      } catch (error) {
        return { source: source.name, error: error.message, status: 'failed' };
      }
    })
  );

  return results
    .filter(r => r.status === 'success')
    .sort((a, b) => a.price - b.price);
}

async function fetchDrugPrice(source, drugName, zipCode) {
  // Solve CAPTCHA for this source
  const token = await solveCaptcha(source.siteKey, source.url);

  const response = await fetch(source.url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      drug: drugName,
      zip: zipCode,
      'g-recaptcha-response': token
    })
  });

  return parsePrice(await response.text());
}

常见问题

抓取药房价格数据合规吗?

只采集有权访问的公开定价信息,遵守服务条款及《网络安全法》等当地合规要求。识别验证码不代表可突破使用条款。

国内网络环境下 reCAPTCHA 一直识别失败,是哪里出了问题?

先排除识别以外的原因:reCAPTCHA 依赖 Google 托管脚本,国内网络不稳定时页面可能根本没加载出验证码组件,需先确认脚本能加载。

可以同时处理多个药房网站的验证码吗?

可以,CaptchaAI 支持并发识别,实际并发数取决于账户线程数和各门户的限流策略。

验证码类型换了,代码需要大改吗?

不需要,换识别方法和 token 字段名即可。CaptchaAI 覆盖 reCAPTCHA v2/v3、Turnstile、GeeTest v3 及图片验证码,按类型分支,提交轮询逻辑不变。

相关文章

下一步

开始搭建药房比价工具——获取 CaptchaAI API Key,一次接入覆盖所有来源的验证码识别。

该文章已禁用评论。