fix(v2.0.1): 修复反爬误判 + Wayback 兜底未触发两个 bug

Bug 1: Wayback 兜底未触发 (search.py fetch_page)

- 根因: Cloudflare JS 质询页常返回 HTTP 200 (非 403), 主抓取 result 非 None

- _should_try_fallback 在 result 非 None 时直接返回 False, 跳过兜底

- 反爬检测在兜底判断之后执行, 错过兜底入口

- 修复: 反爬检测提前到兜底判断之前, anti_bot_detected=True 也触发 Wayback

- Wayback 结果重新做反爬检测 (防御性)

Bug 2: WAF 指纹库误判正常内容 (search.py WAF_FINGERPRINTS)

- 根因: 裸公司名 (cloudflare/akamai) 和宽泛词 (captcha/challenge/dd-) 做全文匹配

- DataCamp 文章引用 cloudflare.com 文档链接 -> 误判为 cloudflare WAF

- 'coding challenges' 正常内容 -> 误判为 generic 反爬

- Wayback 归档正文被误判, 兜底返回的有效内容被丢弃

- 修复: 移除裸公司名和宽泛词, 改用技术标识符 (cf-ray/incap_ses/bm_sz 等)

- 通用文案用完整短语 (please complete the captcha) 替代单词

- 增加 <title> 标签精准检测 (反爬页 title 是特征文案, 误判率极低)

- 新增 Anubis 反爬系统检测 (anubis_challenge/miserere)

真实测试验证 (search.metona.cn 实例):

- v2.0.0: --fetch 3 全部失败 (3 ERR: cloudflare/generic, Wayback 未触发)

- v2.0.1: --fetch 3 全部成功 (3 OK: 38100/93442/1945 chars, UA 轮换绕过 Cloudflare)

测试: 458 个全部通过 (新增 7 个测试覆盖修复行为)
This commit is contained in:
2026-08-01 21:44:58 +08:00
parent 28ff7c0a48
commit d9a08716bc
7 changed files with 215 additions and 66 deletions
+51 -6
View File
@@ -208,22 +208,67 @@ def test_fetch_page_normal_page_no_anti_bot():
def test_fetch_page_anti_bot_triggers_wayback():
"""被反爬拦截后应尝试 Wayback_should_try_fallback 防御性检查)。
"""被反爬拦截后应尝试 Wayback 兜底(v2.0.1 修复)。
当前实现:主抓取成功返回反爬页内容 → result 非 None → 不触发兜底。
此测试记录此行为:反爬页被当作"成功抓取"返回,在 fetch_page 内部检测
v2.0.0 bug:主抓取"成功"返回反爬页(HTTP 200 + Cloudflare 质询)时,
result 非 None 导致 _should_try_fallback 返回 FalseWayback 永不触发
v2.0.1 修复:反爬检测提前到兜底判断之前,反爬阳性也触发 Wayback。
本测试 mock fetch_url 两次调用:
1. 主抓取 → 返回 Cloudflare 质询页
2. Wayback 兜底 → 返回正常归档页
"""
cf_html = "<html><body>Just a moment... cf-ray: 123</body></html>"
wb_html = "<html><body><p>Archived article content.</p></body></html>"
cf_result = _make_result(content=cf_html, final_url="https://example.com")
wb_result = _make_result(content=wb_html,
final_url="https://web.archive.org/web/2024/https://example.com")
with patch("search.fetch_url",
return_value=_make_result(content=cf_html)):
side_effect=[cf_result, wb_result]) as mock_fetch:
result = fetch_page("https://example.com", fallback_enabled=True)
# 反爬页被检测到,标记为 error + anti_bot_detected
# Wayback 兜底成功,反爬标记清除,status=ok
assert result["status"] == "ok"
assert result["anti_bot_detected"] is False
assert result["waf_type"] is None
assert result["fallback_used"] == "wayback"
assert "Archived article content." in result["text"]
# 确认 fetch_url 被调用两次:主抓取 + Wayback
assert mock_fetch.call_count == 2
# 第二次调用应该是 Wayback URL
assert "web.archive.org/web/2/" in mock_fetch.call_args_list[1][0][0]
def test_fetch_page_anti_bot_wayback_also_blocked():
"""主抓取反爬 + Wayback 也反爬/失败 → 最终返回 error。
Wayback 兜底返回 None(失败)时,保留主抓取的反爬检测结果。
"""
cf_html = "<html><body>Just a moment... cf-ray: 123</body></html>"
cf_result = _make_result(content=cf_html, final_url="https://example.com")
with patch("search.fetch_url",
side_effect=[cf_result, RuntimeError("wayback timeout")]):
result = fetch_page("https://example.com", fallback_enabled=True)
# Wayback 失败,保留反爬 error
assert result["status"] == "error"
assert result["anti_bot_detected"] is True
# 但不会触发 Wayback(因为主抓取"成功"了,只是内容是反爬页)
assert result["waf_type"] == "cloudflare"
assert result["fallback_used"] is None
def test_fetch_page_anti_bot_fallback_disabled():
"""--no-fallback 时反爬页直接返回 error,不尝试 Wayback。"""
cf_html = "<html><body>Just a moment... cf-ray: 123</body></html>"
with patch("search.fetch_url",
return_value=_make_result(content=cf_html)) as mock_fetch:
result = fetch_page("https://example.com", fallback_enabled=False)
assert result["status"] == "error"
assert result["anti_bot_detected"] is True
assert result["waf_type"] == "cloudflare"
assert result["fallback_used"] is None
# 只调用一次(主抓取),没有 Wayback
assert mock_fetch.call_count == 1
def test_fetch_page_passes_referer():
"""referer 透传给 fetch_url。"""
with patch("search.fetch_url", return_value=_make_result()) as mock_fetch: