Files
searxng-use-cli/tests/test_anti_bot.py
T
thzxx d9a08716bc 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 个测试覆盖修复行为)
2026-08-01 21:44:58 +08:00

238 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for v2.0.0 anti-bot / WAF detection enhancements.
Covers: _detect_anti_bot (WAF fingerprint library: Cloudflare/Imperva/
PerimeterX/DataDome/Akamai/generic), full-document scanning (not just
first 2000 chars), _is_blocked_page backward compatibility.
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
from search import _detect_anti_bot, _is_blocked_page, WAF_FINGERPRINTS
# ----- Cloudflare detection -----
def test_detect_cloudflare_cf_ray():
html = "<html><head><title>Just a moment...</title></head>" \
"<body>cf-ray: 8abc123</body></html>"
assert _detect_anti_bot(html) == "cloudflare"
def test_detect_cloudflare_just_a_moment():
html = "<html><body>Just a moment...</body></html>"
assert _detect_anti_bot(html) == "cloudflare"
def test_detect_cloudflare_checking_browser():
html = "<html><body>Checking your browser before accessing</body></html>"
assert _detect_anti_bot(html) == "cloudflare"
def test_detect_cloudflare_attention_required():
html = "<html><body>Attention Required! | Cloudflare</body></html>"
assert _detect_anti_bot(html) == "cloudflare"
# ----- Imperva detection -----
def test_detect_imperva_incap_ses():
html = "<html><body>incap_ses_123_cookie</body></html>"
assert _detect_anti_bot(html) == "imperva"
def test_detect_imperva_incapsula():
html = "<html><body>Request unsuccessful. Incapsula incident ID: 123</body></html>"
assert _detect_anti_bot(html) == "imperva"
# ----- PerimeterX detection -----
def test_detect_perimeterx_px_captcha():
html = "<html><body>px-captcha challenge</body></html>"
assert _detect_anti_bot(html) == "perimeterx"
def test_detect_perimeterx_press_hold():
html = "<html><body>Press & hold to confirm you are a human</body></html>"
assert _detect_anti_bot(html) == "perimeterx"
# ----- DataDome detection -----
def test_detect_datadome_protected():
html = "<html><body>Protected by DataDome</body></html>"
assert _detect_anti_bot(html) == "datadome"
def test_detect_datadome_cookie():
html = "<html><body>datadome cookie set</body></html>"
assert _detect_anti_bot(html) == "datadome"
# ----- Akamai detection -----
def test_detect_akamai_bm_sz():
html = "<html><body>bm_sz cookie</body></html>"
assert _detect_anti_bot(html) == "akamai"
def test_detect_akamai_reference():
html = "<html><body>Reference #123.akamaighost</body></html>"
assert _detect_anti_bot(html) == "akamai"
# ----- Generic detection -----
def test_detect_generic_captcha():
"""v2.0.1:收窄为完整短语 'please complete the captcha',避免正文误判。"""
html = "<html><body>Please complete the CAPTCHA</body></html>"
assert _detect_anti_bot(html) == "generic"
def test_detect_generic_verify_human():
html = "<html><body>Verify you are human</body></html>"
assert _detect_anti_bot(html) == "generic"
def test_detect_generic_access_denied():
"""v2.0.1:裸 'access denied' 从全文档扫描移除(避免权限文章误判),
改由 <title> 标签检测覆盖。反爬页 title 常为 'Access Denied'。"""
html = "<html><head><title>Access Denied</title></head><body>Access Denied</body></html>"
assert _detect_anti_bot(html) == "generic"
def test_detect_generic_access_denied_sucuri():
"""v2.0.1Sucuri WAF 的完整文案在全文档扫描中保留。"""
html = "<html><body>Access Denied - Sucuri Website Firewall</body></html>"
assert _detect_anti_bot(html) == "generic"
def test_detect_generic_blocked():
html = "<html><body>You have been blocked</body></html>"
assert _detect_anti_bot(html) == "generic"
def test_detect_generic_unusual_traffic():
html = "<html><body>unusual traffic from your computer</body></html>"
assert _detect_anti_bot(html) == "generic"
def test_detect_generic_robot():
html = "<html><body>Are you a robot?</body></html>"
assert _detect_anti_bot(html) == "generic"
# ----- Full-document scanning (v2.0.0 key improvement) -----
def test_detect_anti_bot_beyond_2000_chars():
"""反爬指示词在前 2000 字符之外也能检测到。
v2.0.0 核心改进:旧版只扫前 2000 字符,大页面反爬页可能漏检。
"""
# 构造 3000 字符的无意义填充 + 反爬关键词
padding = "x" * 2500
html = f"<html><body>{padding}<div>Just a moment...</div></body></html>"
assert _detect_anti_bot(html) == "cloudflare"
def test_detect_anti_bot_large_page_end():
"""反爬关键词在文档末尾也能检测到。"""
padding = "y" * 5000
# v2.0.1:用完整短语而非裸 captcha,避免误判
html = f"<html><body>{padding}please complete the captcha</body></html>"
assert _detect_anti_bot(html) == "generic"
# ----- Negative cases -----
def test_detect_anti_bot_normal_page():
"""正常页面不触发检测。"""
html = "<html><body><h1>Welcome</h1><p>This is a normal article about Python programming.</p></body></html>"
assert _detect_anti_bot(html) is None
def test_detect_anti_bot_empty_content():
"""空内容返回 None。"""
assert _detect_anti_bot("") is None
assert _detect_anti_bot(None) is None
def test_detect_anti_bot_article_mentions_captcha_in_context():
"""v2.0.1 修复:文章讨论 captcha 但不是反爬页,不再误判。
v2.0.0 用裸 "captcha" 做全文匹配,导致"讨论 captcha 工作原理的文章"
被误判为反爬页。v2.0.1 收窄为 "please complete the captcha" 等完整短语,
正常讨论 captcha 的文章不再触发。
"""
html = "<html><body><p>This article explains how CAPTCHA works.</p></body></html>"
# v2.0.1:收窄后不再误判
assert _detect_anti_bot(html) is None
def test_detect_anti_bot_article_mentions_cloudflare_in_context():
"""v2.0.1 修复:文章引用 Cloudflare 文档链接,不再误判为 cloudflare WAF。
v2.0.0 用裸 "cloudflare" 做全文匹配,Wayback 归档的 DataCamp 文章
正文里有 cloudflare.com 链接,被误判为反爬页导致兜底失败。
"""
html = ("<html><body><article>"
"<p>Learn more at <a href='https://www.cloudflare.com/learn/'>Cloudflare</a></p>"
"<p>Join our daily coding challenges!</p>"
"</article></body></html>")
assert _detect_anti_bot(html) is None
def test_detect_anti_bot_article_mentions_challenge_in_context():
"""v2.0.1 修复:文章含 'coding challenge' 等正常内容,不再误判。"""
html = "<html><body><p>Daily 5-minute coding challenges.</p></body></html>"
assert _detect_anti_bot(html) is None
# ----- Priority: specialized WAF before generic -----
def test_detect_priority_cloudflare_over_generic():
"""同时匹配 cloudflare 和 generic 时,返回 cloudflare(优先级)。"""
# "just a moment" 是 cloudflare 专用,"captcha" 是 generic
# cloudflare 在 WAF_FINGERPRINTS 中排在 generic 之前
html = "<html><body>Just a moment... captcha</body></html>"
assert _detect_anti_bot(html) == "cloudflare"
# ----- WAF_FINGERPRINTS structure -----
def test_waf_fingerprints_has_six_types():
"""指纹库覆盖 6 种 WAF 类型。"""
types = [waf_type for waf_type, _ in WAF_FINGERPRINTS]
assert "cloudflare" in types
assert "imperva" in types
assert "perimeterx" in types
assert "datadome" in types
assert "akamai" in types
assert "generic" in types
def test_waf_fingerprints_generic_is_last():
"""generic 排在最后(优先级最低)。"""
assert WAF_FINGERPRINTS[-1][0] == "generic"
def test_waf_fingerprints_all_have_indicators():
"""每个 WAF 类型都有至少 2 个指示词。"""
for waf_type, indicators in WAF_FINGERPRINTS:
assert len(indicators) >= 2, f"{waf_type} has too few indicators"
# ----- _is_blocked_page backward compat -----
def test_is_blocked_page_delegates_to_detect():
"""_is_blocked_page 应委托给 _detect_anti_bot。"""
# v2.0.1:用完整反爬短语而非裸 captcha
assert _is_blocked_page("<html><body>please complete the captcha</body></html>") is True
assert _is_blocked_page("<html>normal content</html>") is False
def test_is_blocked_page_cloudflare():
"""Cloudflare 页面被检测为 blocked。"""
assert _is_blocked_page("<html>cf-ray: 123</html>") is True