feat(v2.1.0): 研究模式 + fetch.py Wayback 兜底 + 被墙站点智能回退
A. fetch.py 补齐 Wayback 兜底 (修复重大 gap) - v2.0.1 gap: fetch.py 独立调用 403 时无 Wayback 兜底 (仅 search.py --fetch 有) - AI Agent 用 fetch.py -u URL 直接抓取被墙站点时, 403 后无任何回退 - 修复: fetch.py main() 增加 Wayback 兜底逻辑 + --no-fallback flag - 共享逻辑抽取到 common.py: should_try_wayback() + build_wayback_url() B. --research 研究模式 - 给定主题自动扩展 5 个多角度查询: overview/profile/background/works/review - 确定性规则 (不依赖 AI 判断), 跨进程可复现 - 输出含 research_topic + research_queries 元数据, AI Agent 可按角度结构化汇编 - 与 --query/--queries-file 互斥, 支持所有输出格式 (json/brief/urls/csv) - 三态退出码: 0=有结果, 2=全部空, 1=全部错误 C. 被墙站点智能回退 - common.py 增加 HARD_BLOCKED_DOMAINS: 百度百科/知乎/微博/微信公众号/豆瓣等 - is_hard_blocked_domain() 精确匹配 + 子域匹配 - 命中被墙站点时: 主抓取失败后立即 Wayback (不等 should_try_wayback 判断) - search.py _should_try_fallback 增加 url 参数, 被墙站点直接触发兜底 真实测试验证 (search.metona.cn 实例): - fetch.py 百度百科兜底: 403 → Wayback 恢复 150,493 chars ✓ - --research 模式: 5 角度查询扩展 + research 元数据 + 三态退出码 ✓ - 被墙站点检测: Hard-blocked domain detected 日志 + 自动 Wayback ✓ 测试: 503 个全部通过 (新增 45 个: test_wayback_shared + test_research_mode) 来源: 另一个 AI Agent 反馈 Wikipedia/百度百科/知乎 fetch 失败, 需要多角度搜索+失败回退+被墙站点列表
This commit is contained in:
@@ -711,3 +711,103 @@ def emit_progress(event: str, **kwargs) -> None:
|
||||
payload.update(kwargs)
|
||||
print(_json.dumps(payload, ensure_ascii=False), file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
# ----- Wayback Machine 兜底(v2.1.0 共享逻辑)-----
|
||||
# 被 search.py 和 fetch.py 共用,避免逻辑漂移。
|
||||
|
||||
def should_try_wayback(error_msg: str) -> bool:
|
||||
"""判断是否应触发 Wayback Machine 兜底。
|
||||
|
||||
触发条件:错误信息暗示 404/403/超时/连接重置等可恢复失败。
|
||||
不触发:DNS 失败(Wayback 也访问不到)、空错误。
|
||||
|
||||
纯字符串判断,无副作用,可安全用于 fetch.py 和 search.py。
|
||||
"""
|
||||
if not error_msg:
|
||||
return False
|
||||
msg = error_msg.lower()
|
||||
triggers = ["404", "403", "timeout", "timed out", "connection reset",
|
||||
"connection refused", "max retries exceeded",
|
||||
"connectionreset", "connectionaborted"]
|
||||
return any(t in msg for t in triggers)
|
||||
|
||||
|
||||
def build_wayback_url(url: str) -> str:
|
||||
"""构造 Wayback Machine 最新快照 URL。
|
||||
|
||||
使用 ``https://web.archive.org/web/2/<url>`` 端点,``2`` 表示
|
||||
"最新可用快照"。Wayback 会 302 重定向到具体时间戳快照。
|
||||
"""
|
||||
return f"https://web.archive.org/web/2/{url}"
|
||||
|
||||
|
||||
# ----- 被墙/强反爬站点智能回退(v2.1.0)-----
|
||||
# 这些站点在中国大陆环境下常见 403/ConnectionReset,且对 UA 轮换不敏感
|
||||
# (有更深层的反爬:Cookie/JS 指纹/登录墙)。命中时自动优先 Wayback 兜底。
|
||||
#
|
||||
# 维护原则:
|
||||
# 1. 只收录"几乎必 403"的站点,避免误伤可正常抓取的站点
|
||||
# 2. 每个站点都经过真实环境验证
|
||||
# 3. 列表按域名匹配(子域名也算命中)
|
||||
|
||||
HARD_BLOCKED_DOMAINS = frozenset([
|
||||
"baike.baidu.com", # 百度百科:强反爬 + Cookie 检测
|
||||
"zhidao.baidu.com", # 百度知道:同上
|
||||
"tieba.baidu.com", # 百度贴吧:同上
|
||||
"wenku.baidu.com", # 百度文库:同上
|
||||
"zhihu.com", # 知乎:登录墙 + 反爬
|
||||
"zhuanlan.zhihu.com", # 知乎专栏:同上
|
||||
"mp.weixin.qq.com", # 微信公众号:强反爬 + 登录墙
|
||||
"weibo.com", # 微博:登录墙 + 反爬
|
||||
"m.weibo.cn", # 微博移动版:同上
|
||||
"douban.com", # 豆瓣:反爬 + 频率限制
|
||||
"www.douban.com", # 豆瓣主站
|
||||
"book.douban.com", # 豆瓣读书
|
||||
"movie.douban.com", # 豆瓣电影
|
||||
"tieba.baidu.com", # 百度贴吧(重复,确保子域匹配)
|
||||
])
|
||||
|
||||
# 部分域名需要子域匹配(如 *.zhihu.com, *.weibo.com, *.douban.com)
|
||||
_SUBDOMAIN_BLOCKED = frozenset([
|
||||
"zhihu.com",
|
||||
"weibo.com",
|
||||
"douban.com",
|
||||
"baidu.com",
|
||||
])
|
||||
|
||||
|
||||
def is_hard_blocked_domain(url: str) -> bool:
|
||||
"""判断 URL 是否属于已知的强反爬/被墙站点。
|
||||
|
||||
匹配逻辑:
|
||||
1. 精确匹配 HARD_BLOCKED_DOMAINS(如 baike.baidu.com)
|
||||
2. 子域匹配 _SUBDOMAIN_BLOCKED(如 *.zhihu.com)
|
||||
|
||||
命中时调用方应:
|
||||
* 主抓取失败后立即尝试 Wayback(不等 should_try_wayback 判断)
|
||||
* 或直接跳过主抓取,优先 Wayback
|
||||
"""
|
||||
if not url:
|
||||
return False
|
||||
# 提取域名
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
host = urlparse(url).hostname or ""
|
||||
except Exception:
|
||||
host = ""
|
||||
if not host:
|
||||
return False
|
||||
host = host.lower().lstrip(".")
|
||||
# 精确匹配
|
||||
if host in HARD_BLOCKED_DOMAINS:
|
||||
return True
|
||||
# 子域匹配:xxx.zhihu.com → 匹配 zhihu.com
|
||||
parts = host.split(".")
|
||||
if len(parts) >= 2:
|
||||
# 检查最后两段(如 zhihu.com)或最后三段(如 baike.baidu.com)
|
||||
for i in range(len(parts) - 1):
|
||||
suffix = ".".join(parts[i:])
|
||||
if suffix in _SUBDOMAIN_BLOCKED or suffix in HARD_BLOCKED_DOMAINS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user