feat(v2.5.0): Sec-Ch-Ua 头修复 + 结构化提取 + token 预算 + 正文去重 + 连接复用
正确性修复: - 修复 Sec-Ch-Ua 构造 bug: 原实现产出 ""Not_A Brand";v="99"" 双重引号 畸形头(Chrome/Edge 两路径), 严格校验的 WAF 会忽略; 改为品牌数组拼接 - search 403 快速失败: 实例级 403 不再退避重试(~10.5s 空等), 立即 failover - AdaptiveThrottle: --throttle-failure-threshold 0 现为真正禁用语义 - number_of_results 缺失时用 len(results) 兜底(JSON/HTML 路径契约对齐) - 版本对齐: pyproject.toml 与 _config.py 同步 2.5.0 AI 代理体验: - fetch.py --extract json: 结构化骨架(title/meta/headings/links/images) - fetch.py --max-chars N: 提取后语义级截断(区别于 --max-size 字节截断) - search --fetch-total-chars N: --fetch 全局字符预算, 耗尽后 status=skipped - --dedup-fetched-content: 抓取正文 SimHash 去重, status=duplicate - --progress 新增 angle_start/ok/fail + fetch_skip/fetch_duplicate 事件 - fetch.py 补齐 --log-format json + --dump-schema - CSV 媒体列自适应(images/videos 类别自动追加媒体字段列) - --dry-run 批量模式打印实际查询列表 - search 连接复用: requests 可用时走模块级 Session(连接池) 工程治理: - 新增 scripts/release_check.py 发布一致性检查(版本/错误码表漂移) - 新增 tests/test_v250_features.py 46+4 个回归测试(全量 622 通过) - tests/conftest.py: autouse fixture 强制 stdlib 路径(本机有 requests 时 既有 urllib mock 测试不失效)
This commit is contained in:
+38
-5
@@ -329,14 +329,25 @@ def build_browser_headers(user_agent: str, referer: str = None,
|
||||
|
||||
# Sec-Ch-Ua 系列仅 Chrome/Edge 发送,Firefox 不发
|
||||
if not is_firefox:
|
||||
# 从 UA 提取主版本号,构造 Sec-Ch-Ua
|
||||
# 从 UA 提取主版本号,构造 Sec-Ch-Ua。
|
||||
# v2.5.0 修复:原实现把已含引号的 not_a_brand 再包进 f-string 引号,
|
||||
# 生成 ""Not_A Brand";v="99"" 的畸形头——Chrome/Edge 两条路径都中招,
|
||||
# 严格校验 Sec-CH-UA 的 WAF(Cloudflare 等会校验与 UA 一致性)会直接
|
||||
# 忽略或判定不一致。现改为品牌数组统一拼接,产出合法格式:
|
||||
# Chrome: "Not_A Brand";v="99", "Chromium";v="138", "Google Chrome";v="138"
|
||||
# Edge: 上述 + , "Microsoft Edge";v="138"
|
||||
m = re.search(r"Chrome/(\d+)", user_agent)
|
||||
ver = m.group(1) if m else "131"
|
||||
not_a_brand = '"Not_A Brand";v="99"' if ver != "99" else '"Not/A)Brand";v="99"'
|
||||
headers["Sec-Ch-Ua"] = f'"{not_a_brand}", "Chromium";v="{ver}", "Google Chrome";v="{ver}"'
|
||||
not_a_brand = "Not_A Brand" if ver != "99" else "Not/A)Brand"
|
||||
brands = [
|
||||
f'"{not_a_brand}";v="99"',
|
||||
f'"Chromium";v="{ver}"',
|
||||
f'"Google Chrome";v="{ver}"',
|
||||
]
|
||||
if is_edge:
|
||||
# Edge 的品牌标识
|
||||
headers["Sec-Ch-Ua"] = headers["Sec-Ch-Ua"].rstrip('"') + f'", "Microsoft Edge";v="{ver}"'
|
||||
# Edge 额外声明 Microsoft Edge 品牌
|
||||
brands.append(f'"Microsoft Edge";v="{ver}"')
|
||||
headers["Sec-Ch-Ua"] = ", ".join(brands)
|
||||
headers["Sec-Ch-Ua-Mobile"] = '"?1"' if "Mobile" in user_agent else '"?0"'
|
||||
# 平台标识
|
||||
if "Windows" in user_agent:
|
||||
@@ -1054,3 +1065,25 @@ def is_similar(result_a: dict, result_b: dict, threshold: float = 0.85) -> bool:
|
||||
max_distance = max(0, min(3, int(round((1.0 - threshold) / 0.05))))
|
||||
return _hamming_distance(hash_a, hash_b) <= max_distance
|
||||
|
||||
|
||||
def texts_are_similar(text_a: str, text_b: str, threshold: float = 0.85) -> bool:
|
||||
"""判断两段正文是否近似重复(v2.5.0,供抓取正文去重用)。
|
||||
|
||||
镜像/转载站点的正文与原文高度相似但 URL 不同——URL 级去重无法合并,
|
||||
需要按正文指纹判定。复用与 :func:`is_similar` 相同的
|
||||
SimHash + 汉明距离逻辑,但针对**正文**而非标题:
|
||||
|
||||
* 归一化(小写/去标点/CJK 单字分词)后取前 1000 字符做指纹窗口——
|
||||
镜像页前 1000 字符通常一致,长文全量计算只会稀释指纹且增加开销
|
||||
* 空文本(len<窗口)返回 False,避免短错误文本误判为重复
|
||||
* threshold → 汉明距离阈值映射与 is_similar 一致(0.85→3 ... 1.0→0)
|
||||
"""
|
||||
norm_a = _normalize_title(text_a)[:1000]
|
||||
norm_b = _normalize_title(text_b)[:1000]
|
||||
if len(norm_a) < 5 or len(norm_b) < 5:
|
||||
return False
|
||||
hash_a = _simhash(norm_a)
|
||||
hash_b = _simhash(norm_b)
|
||||
max_distance = max(0, min(3, int(round((1.0 - threshold) / 0.05))))
|
||||
return _hamming_distance(hash_a, hash_b) <= max_distance
|
||||
|
||||
|
||||
Reference in New Issue
Block a user