开发跨境药品比价工具,抓取美国药房门户和保险处方目录时,验证码是绕不开的第一道坎:搜索表单和结果页普遍部署 reCAPTCHA v2,部分叠加 Turnstile 或图片验证码。
示例仅覆盖你有权访问的公开定价页面,请遵守当地合规要求。
药房网站的验证码都藏在哪些页面
| 页面类型 | 常见验证码 | 触发场景 |
|---|---|---|
| 药品搜索表单 | reCAPTCHA v2 | 每次搜索请求 |
| 价格结果页 | Cloudflare Turnstile | 疑似自动化访问 |
| 药房定位器 | reCAPTCHA v2 | 基于地理位置的查询 |
| 优惠券查询 | 图片验证码 | 展示折扣码之前 |
| 保险处方目录 | reCAPTCHA v2 | 登录或搜索环节 |
控制请求频率,减少触发验证码
| 策略 | 具体做法 |
|---|---|
| 错开请求节奏 | 每次搜索间隔 5–10 秒 |
| 定期轮换会话 | 每 20–30 次查询更换新会话 |
| 变化搜索模式 | 避免连续重复搜索同一种药品 |
| 关注出口 IP 质量 | 数据中心 IP 比普通网络更容易触发验证码 |
用 CaptchaAI 搭建基础识别流程
基础流程分四步:
- 加载搜索页,提取 sitekey
- 提交给
in.php - 轮询
res.php拿到 token - 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 |
用会话管理压低验证码触发率
核心做法:
- 复用同一个
session,保留 cookie - 用
searches_since_captcha计数 - 命中验证码页面时重新识别并清零计数
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 及图片验证码,按类型分支,提交轮询逻辑不变。
相关文章
- 如何通过 API 处理 reCAPTCHA v2 回调
- reCAPTCHA v2 与 Turnstile 同站点处理方案
- reCAPTCHA v2 回调机制解析
下一步
开始搭建药房比价工具——获取 CaptchaAI API Key,一次接入覆盖所有来源的验证码识别。