feat(v2.2.1): 修复 Brotli 乱码 + 缓存治理 + 多页聚合 + 研究模式增强

核心修复(v2.2.1):
- 修复 Brotli 乱码 bug: build_browser_headers 智能声明 Accept-Encoding,
  仅在 brotli 可用时才声明 br; fetch.py 双路径 br 解压(requests + stdlib)
  此前 Chrome/Edge UA 抓取 example.com 等返回 br 的站点输出乱码

v2.2.0 新功能:
- main() 拆分为 _handle_verify/_handle_research/_handle_batch/_handle_single
- --cache-max-size MB: 缓存大小上限 + LRU 淘汰(默认 100MB)
- --pages N: 多页聚合 + 跨页去重
- --research 跨角度合并: 新增 merged_results 字段
- --stream / --progress: JSON Lines 流式输出 + request_id 贯穿
- --dry-run / --save-config / --log-format json
- --similarity-dedup / --throttle-* 参数化
- 15-UA 池 + PDF/docx 解析 + error_code 字段

文档与测试:
- SKILL.md: 版本号唯一(元数据),删除版本标记干扰
- README.md: 测试数量 539 -> 544
- 544 passed (新增 5 个 Content-Encoding 解压测试)
This commit is contained in:
2026-08-03 17:14:33 +08:00
parent 0c8fdc1e45
commit 157219d982
10 changed files with 2044 additions and 563 deletions
+60 -15
View File
@@ -69,6 +69,7 @@ AI 调用后,stdout 输出网页正文(text/html/markdown 三种格式),
| `E_EMPTY` | 空结果(exit 2) | 调整查询词、扩大 `--time-range`/`--categories` |
| `E_INPUT` | 输入错误(参数/文件) | 检查语法、标志组合、文件路径 |
| `E_INTERNAL` | 内部错误 | 用 `--verbose` 重跑并报告 |
| `E_UNSUPPORTED_MEDIA` (v2.2.0) | 不支持的二进制媒体类型(PDF/docx/xlsx 解析失败) | 换 URL,或安装 `pdftotext` 用于 PDF 解析 |
### 流式输出与进度事件(AI 高级用法)
@@ -85,15 +86,19 @@ AI 调用后,stdout 输出网页正文(text/html/markdown 三种格式),
**`--progress`**:进度事件流(JSON Lines 到 stderr),AI 可实时跟踪执行:
```
{"event": "start", "query": "...", "instances": 2}
{"event": "instance_try", "url": "https://inst1.example.com", "attempt": 1}
{"event": "instance_ok", "url": "https://inst1.example.com", "latency": 0.342, "results": 10}
{"event": "instance_fail", "url": "https://inst2.example.com", "error": "HTTP 503", "error_code": "E_NETWORK"}
{"event": "cache_hit", "query": "...", "ttl": 30}
{"event": "fetch_ok", "url": "...", "chars": 12345}
{"event": "done", "results": 10, "query": "..."}
{"event": "start", "query": "...", "instances": 2, "request_id": "a1b2c3d4"}
{"event": "instance_try", "url": "https://inst1.example.com", "attempt": 1, "request_id": "a1b2c3d4"}
{"event": "instance_ok", "url": "https://inst1.example.com", "latency": 0.342, "results": 10, "request_id": "a1b2c3d4"}
{"event": "instance_fail", "url": "https://inst2.example.com", "error": "HTTP 503", "error_code": "E_NETWORK", "request_id": "a1b2c3d4"}
{"event": "cache_hit", "query": "...", "ttl": 30, "request_id": "a1b2c3d4"}
{"event": "page_ok", "pageno": 1, "results": 10, "request_id": "a1b2c3d4"}
{"event": "page_fail", "pageno": 2, "error": "HTTP 503", "request_id": "a1b2c3d4"}
{"event": "fetch_ok", "url": "...", "chars": 12345, "request_id": "a1b2c3d4"}
{"event": "done", "results": 10, "query": "...", "request_id": "a1b2c3d4"}
```
v2.2.0:每个事件含 `request_id`8 位 hex,每次运行自动生成);`--pages N` 时新增 `page_ok`/`page_fail` 事件。
**`--dump-schema`**:输出当前版本的 JSON Schema 到 stdout 并退出,AI 可程序化发现字段名与类型,无需解析文档。
```bash
@@ -158,19 +163,23 @@ python scripts/search.py -q "查询词" -i https://your-instance \
[--language zh-CN] \
[--sort-by score|date|engine|none] \
[--no-dedup] \
[--similarity-dedup] [--similarity-threshold 0.85] \
[--max-results 10] \
[--fetch 3] \
[--fetch-report] \
[--pages N] \
[--fetch 3] [--fetch-report [json]] \
[--no-fallback] \
[--referer URL] \
[--request-delay 0.3] \
[--cache-ttl 30] \
[--cache-ttl 30] [--cache-max-size 100] \
[--queries-file queries.txt] \
[--research "研究主题"] \
[--research "研究主题"] [--research-angles "a,b,c"] \
[--include-domain example.com] \
[--exclude-domain spam.com] \
[--proxy http://corp:8080] \
[--auth-bearer-file ~/.token] \
[--throttle-failure-threshold 3] [--throttle-pause-seconds 30] [--throttle-max-delay 10] \
[--log-format text|json] \
[--dry-run] [--save-config FILE] \
[--verify] \
[--stream] \
[--progress] \
@@ -209,6 +218,18 @@ python scripts/search.py -q "查询词" -i https://your-instance \
| `--progress` | 进度事件(JSON Lines 到 stderr | 关闭 |
| `-v / --verbose` | 调试日志 | — |
| `--quiet` | 仅输出警告和错误 | — |
| `--pages N` | v2.2.0 分页聚合:一次获取 N 页并跨页去重合并 | 1 |
| `--similarity-dedup` | v2.2.0 相似度去重(SimHash + Jaccard | 关闭 |
| `--similarity-threshold` | v2.2.0 相似度阈值 | 0.85 |
| `--log-format` | v2.2.0 日志格式:text/json | text |
| `--dry-run` | v2.2.0 预览模式(不发 HTTP 请求) | 关闭 |
| `--throttle-failure-threshold` | v2.2.0 限流失败阈值 | 3 |
| `--throttle-pause-seconds` | v2.2.0 429 全局暂停秒数 | 30 |
| `--throttle-max-delay` | v2.2.0 限流最大延迟秒数 | 10 |
| `--research-angles` | v2.2.0 自定义研究角度(逗号分隔) | 默认 5 角度 |
| `--save-config` | v2.2.0 保存当前参数为 searxng.toml 并退出 | — |
| `--cache-max-size` | v2.2.0 缓存大小上限(MB | 100 |
| `--fetch-report json` | v2.2.0 JSON 格式抓取报告 | text |
### fetch.py — 网页抓取
@@ -242,6 +263,27 @@ python scripts/fetch.py -u https://example.com \
## 能力清单
**v2.2.1 修复**
- Brotli 乱码修复:`build_browser_headers()` 智能声明 `Accept-Encoding`——仅当本机安装了 brotli/brotlicffi 包时才声明 `br`,避免服务器返回 Brotli 压缩字节而 requests 无法自动解压导致全页乱码
- `fetch.py` 双路径 br 解压:requests 路径和 stdlib urllib 路径都添加了 Brotli 手动解压逻辑(作为双保险,应对代理/CDN 强制返回 br 的边缘情况)
- 此前 bug 表现:Chrome/Edge UA 抓取 example.com 等返回 `Content-Encoding: br` 的站点时,输出 302 字符乱码(gzip 二进制被当作文本解码);修复后输出 127 字符正常文本
**v2.2.0 新功能**
- `--pages N` 分页聚合:一次获取 N 页结果并跨页去重合并,每页独立缓存(cache key 含 pageno),进度事件新增 `page_ok`/`page_fail`
- 缓存治理:缓存大小上限 + LRU 淘汰,新增 `--cache-max-size MB`(默认 100MB);`stats()` 新增 `total_bytes`/`max_size_bytes`/`evicted_count`/`utilization_pct` 字段;新增 `evict_expired()` 主动清理方法
- `--similarity-dedup` 相似度去重:基于标题 SimHash + Jaccard 相似度,默认关闭;`--similarity-threshold`(默认 0.85)控制严格程度;O(n²) 复杂度,结果数 > 500 时自动跳过
- PDF/文档解析:fetch 支持解析 PDFpdftotext subprocess)、`.docx`/`.xlsx`stdlib zipfile);不支持的二进制类型返回 `E_UNSUPPORTED_MEDIA`
- `--log-format json` 结构化日志:每行输出 JSON 对象 `{ts, level, logger, msg, request_id}`,便于 AI Agent 程序化解析
- request ID 贯穿:每次运行自动生成 8 位 hex request_id,贯穿所有日志和进度事件
- `--dry-run` 预览模式:不发 HTTP 请求,打印 `{action, url, params, headers_count}` JSON 到 stdout;支持 search/research/batch/verify 四种模式
- AdaptiveThrottle 参数可配置:新增 `--throttle-failure-threshold`(默认 3)、`--throttle-pause-seconds`(默认 30)、`--throttle-max-delay`(默认 10
- `--research-angles` 自定义研究角度:覆盖默认 5 角度,每个角度直接作为查询后缀
- `--save-config FILE`:将当前 CLI 参数保存为 searxng.toml 配置文件并退出
- `--fetch-report json`:输出完整 JSON 报告(含 items 数组 + summary 摘要);原 `--fetch-report`(无参数)保持 text 格式
- UA 池更新到 2026 年版本:Chrome 138-140 / Edge 138 / Firefox 140 / Safari 18,池移至 `_config.py`SSOT
- readability-lite 按语言调整:CJK 内容 100 字符阈值,其他 200 字符阈值
- classify_error 改用异常链(内部改进)、`_domain_ua_cache` 加锁(内部改进)
**搜索**
- 多实例故障转移 + 并行探测
- 指数退避重试(429/5xx/连接错误)
@@ -264,11 +306,12 @@ python scripts/fetch.py -u https://example.com \
- text:提取纯文本
- html:原始 HTML
- markdown:增强 Markdown 转换(GFM 表格、代码块、引用块、嵌套列表、定义列表)
- v2.0.0 readability-lite`<article>`/`<main>` 缺失时,用文本密度算法选最可能正文的 `<div>`
- v2.0.0 readability-lite`<article>`/`<main>` 缺失时,用文本密度算法选最可能正文的 `<div>`v2.2.0CJK 内容 100 字符阈值,其他 200 字符阈值)
- v2.2.0 PDF/文档解析:支持解析 PDFpdftotext subprocess)、`.docx`/`.xlsx`stdlib zipfile);不支持的二进制类型返回 `E_UNSUPPORTED_MEDIA`
**反爬与抓取稳定性(v2.0.0**
- 浏览器指纹头:`build_browser_headers()` 发送完整 Sec-Ch-Ua / Sec-Fetch-* / Accept-Language,不仅靠 User-Agent
- 12 个 UA 池Chrome/Edge/Firefox × Windows/macOS/Linux × 129-131 版本
- 15 个 UA 池v2.2.0 更新):Chrome 138-140 / Edge 138 / Firefox 140 / Safari 18 × Windows/macOS/LinuxUA 池移至 `_config.py`SSOT),common.py 通过导入引用
- 确定性 UA 轮换:`get_ua_for_domain()` 用 SHA-256 为每个域名固定一个 UA(会话内稳定,跨进程可复现)
- requests.Session 复用:连接池 + cookie 持久化 + TLS 会话恢复
- 超时分离:`(connect, read)` 元组,避免大页面下载中途超时浪费已建连接
@@ -281,13 +324,15 @@ python scripts/fetch.py -u https://example.com \
- v2.1.1 精确化:`baidu.com` 从子域匹配改为精确子域列表(www/baike/zhidao/tieba/wenku),`pan.baidu.com`(网盘)/`cloud.baidu.com`(智能云)不再被误伤
- 自适应限流:`AdaptiveThrottle` 状态机,连续 3 次失败自动翻倍延迟 + 减半并发,429 触发全局暂停 30s
- v2.1.1`report_failure` 新增 `error_code` 参数,优先用结构化 `E_RATE_LIMIT` 检测 429(原字符串匹配"429"会漏判"Too Many Requests");`fetch_page` 返回结果新增 `error_code` 字段
- `--fetch-report`:结构化抓取报告到 stderr(每 URL 状态/WAF 类型/兜底方式/字符数 + JSON 摘要
- v2.2.0:原硬编码值现可通过 CLI 配置——`--throttle-failure-threshold`(默认 3)、`--throttle-pause-seconds`(默认 30)、`--throttle-max-delay`(默认 10
- `--fetch-report`:结构化抓取报告到 stderr(每 URL 状态/WAF 类型/兜底方式/字符数 + JSON 摘要);v2.2.0`--fetch-report json` 输出完整 JSON 报告(items 数组 + summary 摘要)
- `--referer` / `--request-delay`:精细控制 Referer 头和请求间隔
- fetch 结果新增字段:`anti_bot_detected`bool)、`waf_type`str|null)、`fallback_used`str|null)、`error_code`str|nullv2.1.1
**缓存**
- SQLite 缓存(`--cache-ttl`),相同查询在 TTL 内跳过网络
- `--clear-cache` / `--cache-stats` 管理缓存
- v2.2.0 缓存治理:`--cache-max-size MB`(默认 100MB)大小上限 + LRU 淘汰;`stats()` 新增 `total_bytes`/`max_size_bytes`/`evicted_count`/`utilization_pct` 字段;新增 `evict_expired()` 主动清理方法
**网络**
- 代理支持(`--proxy`
@@ -306,7 +351,7 @@ python scripts/fetch.py -u https://example.com \
- JSON Lines 流式输出(`--stream`,含 `error` 事件类型)
- 进度事件(`--progress`,含 `instance_try`/`instance_ok`/`instance_fail`JSON Lines 到 stderr
- batch 模式统一 schema`status` 字段区分成功/失败)
- 539 个单元+集成测试
- 544 个单元+集成测试
## 跨 Agent 兼容性