This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kb-skills/.planning/phases/08-crawler/08-RESEARCH.md
T

391 lines
15 KiB
Markdown
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.
# Phase 8: 内容爬取实现 - Research
**Researched:** 2026-03-17
**Domain:** Playwright API Interception, Zhihu Crawler, Anti-Scraping
**Confidence:** HIGH
## Summary
本阶段实现知乎内容爬取的核心功能,使用 Playwright 拦截 API 请求的方式获取结构化数据。关键技术点是复用浏览器 Cookie,自动处理 x-zse-96 签名,无需逆向工程。
**Primary recommendation:** 使用 Playwright `page.route()` 拦截知乎 API 请求,直接获取 JSON 响应。热门评论通过 `/api/v4/comment_v5/` 端点获取,代理通过环境变量 `HTTP_PROXY`/`HTTPS_PROXY` 配置并在 `browser.new_context()` 中应用。
<user_constraints>
## User Constraints (from CONTEXT.md)
### Implementation Decisions
#### 爬取方式选择
- **实现方式**: 使用 Playwright 拦截 API 请求
- **原理**: 浏览器访问页面时拦截其发出的 API 请求(如 /api/v4/answers/xxx),直接获取 JSON 响应
- **优势**: 复用浏览器 Cookie,自动处理 x-zse-96 签名,无需逆向
- **支持内容类型**: 问答 (answer)、专栏 (zhuanlan)
- **不支持**: 知乎视频 — 推迟到后续版本
#### 评论爬取范围
- **爬取范围**: 只爬取热门评论
- **嵌套展开**: 不展开回复的回复,只爬第一层评论
- **可选开关**: 评论爬取是可选功能,通过参数控制是否启用
#### 代理配置方式
- **配置方式**: 通过环境变量配置(HTTP_PROXY/HTTPS_PROXY)
- **配置文件**: 添加到 ~/.kb_env,与现有配置方式一致
- **使用策略**: 强制使用代理 — 不配置代理则拒绝爬取,防止 IP 被封
#### 反爬策略
- **请求频率**: 随机延迟,每个请求间隔 1-3 秒
- **重试策略**: 有限重试,失败后重试 3 次,间隔递增
- **错误处理**: 重试全部失败后抛出异常,由上层处理
### Claude's Discretion
- Playwright 拦截的具体实现方式(route/intercept)
- 热门评论的具体数量限制
- 重试间隔的具体递增策略
- 错误提示信息的具体文案
### Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| CONT-01 | 爬取知乎问答 - 问题标题、问题描述、回答内容、元数据 | Playwright route 拦截 `/api/v4/answers/{id}` 获取结构化 JSON |
| CONT-02 | 爬取知乎专栏/文章 - 标题、正文、元数据 | Playwright route 拦截 zhuanlan API 或页面 DOM 解析 |
| CONT-03 | 爬取内容下的评论(可选开关) | `/api/v4/comment_v5/{type}/{id}/comments?order=hot` 热门评论 API |
| CONT-04 | 提取作者信息、发布时间、点赞数等元数据 | API 响应中包含完整元数据字段 |
| NET-01 | 支持代理 IP 配置 | 环境变量 `HTTP_PROXY`/`HTTPS_PROXY` + `browser.new_context(proxy=...)` |
| NET-02 | 请求频率控制,避免触发反爬 | `time.sleep(random.uniform(1, 3))` 随机延迟 |
| NET-03 | 自动获取请求签名(x-zse-96),无需手动逆向 | Playwright 浏览器自动处理签名 |
</phase_requirements>
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| playwright | 1.40+ | 浏览器自动化 | 项目已使用,API 拦截功能完善 |
| pydantic | 2.x | 数据模型验证 | 项目标准,用于响应数据模型 |
| tenacity | 8.x | 重试机制 | 项目已使用,支持可配置超时 |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| httpx | 0.25+ | HTTP 客户端 | 如需直接 API 调用(非拦截模式) |
| python-dotenv | 1.0+ | 环境变量加载 | 代理配置读取 |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Playwright route | requests + 逆向签名 | 需要持续维护签名算法,Playwright 方式更稳定 |
| 直接调用 API | Selenium + DOM 解析 | DOM 结构变化频繁,API 响应更稳定 |
**Installation:**
```bash
# 已安装,无需额外安装
playwright install chromium # 如需更新浏览器
```
## Architecture Patterns
### Recommended Project Structure
```
kb_workflow/
├── crawler/ # 新建爬虫模块
│ ├── __init__.py
│ ├── zhihu_crawler.py # 主爬虫类
│ ├── api_interceptor.py # API 拦截逻辑
│ ├── models.py # 响应数据模型
│ └── config.py # 爬虫配置(代理、延迟等)
├── auth/
│ └── zhihu_login.py # 现有 - 复用 Cookie 管理
└── metadata/
└── zhihu_extractor.py # 现有 - 复用 PlaywrightBrowser
```
### Pattern 1: Playwright Route Interception
**What:** 拦截浏览器发出的 API 请求,直接获取 JSON 响应
**When to use:** 获取结构化数据,避免 DOM 解析
**Example:**
```python
# Source: https://playwright.dev/python/docs/network
from playwright.sync_api import sync_playwright
def intercept_api(route):
"""拦截知乎 API 请求"""
if "/api/v4/" in route.request.url:
# 继续请求以获取响应
response = route.fetch()
json_data = response.json()
# 存储拦截到的数据
route.fulfill(response=response)
else:
route.continue_()
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.route("**/api/v4/**", intercept_api)
page.goto("https://www.zhihu.com/question/123/answer/456")
```
### Pattern 2: 代理配置
**What:** 通过环境变量配置代理,在 Playwright 中应用
**When to use:** 所有爬取请求,防止 IP 被封
**Example:**
```python
# Source: https://playwright.dev/python/docs/network
import os
from playwright.sync_api import sync_playwright
def get_proxy_config():
"""从环境变量获取代理配置"""
proxy_server = os.environ.get("HTTP_PROXY") or os.environ.get("HTTPS_PROXY")
if proxy_server:
# 解析代理 URL
# http://user:pass@host:port 或 http://host:port
return {"server": proxy_server}
return None
# 在 new_context 中应用代理
browser = chromium.launch()
context = browser.new_context(proxy=get_proxy_config())
```
### Pattern 3: 随机延迟与重试
**What:** 请求间随机延迟,失败时有限重试
**When to use:** 所有网络请求
**Example:**
```python
import random
import time
from tenacity import Retrying, stop_after_attempt, wait_exponential
def crawl_with_retry(crawl_func, url):
"""带重试的爬取"""
for attempt in Retrying(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
reraise=True
):
with attempt:
# 随机延迟
time.sleep(random.uniform(1, 3))
return crawl_func(url)
```
### Anti-Patterns to Avoid
- **硬编码延迟时间:** 使用固定延迟容易被检测,应使用随机值
- **忽略代理检查:** 不检查代理配置直接爬取可能导致 IP 被封
- **无限重试:** 重试次数应有限制,避免无限循环
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| 请求重试 | 自定义重试循环 | tenacity.Retrying | 支持指数退避、超时、回调 |
| API 签名 | 逆向 x-zse-96 算法 | Playwright 浏览器自动处理 | 签名算法频繁更新,逆向成本高 |
| Cookie 管理 | 手动解析存储 | 现有 ZhihuLogin + PlaywrightBrowser | 已有成熟实现 |
| 代理解析 | 自定义 URL 解析 | Playwright proxy 参数 | 支持认证、SOCKS5 |
**Key insight:** Playwright 拦截模式的核心优势是复用浏览器的完整上下文(Cookie、签名、指纹),无需维护逆向代码。
## Common Pitfalls
### Pitfall 1: Service Worker 干扰
**What goes wrong:** 使用 `page.route()` 时某些请求未被捕获
**Why it happens:** Service Worker 拦截了网络请求
**How to avoid:** 在 `new_context()` 中设置 `service_workers="block"`
**Warning signs:** API 拦截处理器未被调用
```python
context = browser.new_context(service_workers="block")
```
### Pitfall 2: 代理未生效
**What goes wrong:** 配置了环境变量但请求仍使用直连
**Why it happens:** Playwright 默认不读取环境变量
**How to avoid:** 显式读取环境变量并传入 `new_context(proxy=...)`
**Warning signs:** IP 被封、请求频率限制
### Pitfall 3: 评论 API 分页处理
**What goes wrong:** 只获取了第一页评论
**Why it happens:** 评论 API 使用 cursor 分页
**How to avoid:** 使用 `offset` 参数处理分页,但根据决策只爬热门评论(单页)
**Warning signs:** 评论数量固定为 20 条
### Pitfall 4: Cookie 过期未检测
**What goes wrong:** 爬取时返回登录页面
**Why it happens:** Cookie 已过期但未提前验证
**How to avoid:** 爬取前调用 `/api/v4/me` 验证 Cookie 有效性
**Warning signs:** 响应包含"登录"关键词
## Code Examples
Verified patterns from official sources:
### API 拦截存储响应
```python
# Source: https://playwright.dev/python/docs/network
class ZhihuApiInterceptor:
"""知乎 API 拦截器"""
def __init__(self):
self.captured_data = {}
def handle_route(self, route):
"""处理拦截的请求"""
url = route.request.url
# 拦截答案 API
if "/api/v4/answers/" in url:
response = route.fetch()
data = response.json()
self.captured_data["answer"] = data
route.fulfill(response=response)
return
# 拦截评论 API
if "/comment_v5/" in url:
response = route.fetch()
data = response.json()
self.captured_data["comments"] = data
route.fulfill(response=response)
return
route.continue_()
def setup(self, page):
"""设置路由拦截"""
page.route("**/api/v4/**", self.handle_route)
```
### 代理配置读取
```python
# Source: https://playwright.dev/python/docs/network
import os
from urllib.parse import urlparse
def parse_proxy_env():
"""解析代理环境变量"""
proxy_url = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY")
if not proxy_url:
return None
parsed = urlparse(proxy_url)
config = {"server": f"{parsed.scheme}://{parsed.hostname}:{parsed.port}"}
if parsed.username and parsed.password:
config["username"] = parsed.username
config["password"] = parsed.password
return config
```
### 热门评论 API 调用
```python
# Source: https://docs.tikhub.io/262614090e0
# 知乎评论 V5 API 参数说明
COMMENT_API_TEMPLATE = (
"https://www.zhihu.com/api/v4/comment_v5/"
"{content_type}/{content_id}/comments"
"?order=hot&limit=20"
)
# content_type: "answers" (回答) 或 "articles" (文章)
# order: "hot" (热门) 或 "ts" (最新)
# limit: 每页数量,默认 20
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| requests + 逆向签名 | Playwright 拦截 | 2024+ | 无需维护签名算法 |
| DOM 选择器解析 | API 响应 JSON | 2024+ | 数据结构更稳定 |
| 固定延迟 | 随机延迟 | 最佳实践 | 降低被检测风险 |
**Deprecated/outdated:**
- 逆向 x-zse-96 签名算法: 知乎频繁更新签名,维护成本高
- 基于 DOM 的内容提取: 页面结构变化导致解析失败
## Open Questions
1. **热门评论数量限制**
- What we know: API 默认返回 20 条,支持分页
- What's unclear: 决策说"只爬热门评论",是否限制为 20 条
- Recommendation: 默认 20 条,通过参数可配置
2. **专栏文章的 API 端点**
- What we know: 专栏页面有对应的 API 调用
- What's unclear: 具体的 API 路径格式
- Recommendation: 实现时通过 route 拦截自动发现
3. **重试间隔递增策略**
- What we know: 使用 tenacity 支持指数退避
- What's unclear: 具体的 min/max 值
- Recommendation: min=2s, max=10s, multiplier=1
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | pytest 7.x |
| Config file | pytest.ini |
| Quick run command | `pytest tests/test_crawler/ -x -v` |
| Full suite command | `pytest tests/ -v` |
### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| CONT-01 | 爬取问答内容 | integration | `pytest tests/test_crawler/test_answer_crawler.py -x` | Wave 0 |
| CONT-02 | 爬取专栏文章 | integration | `pytest tests/test_crawler/test_article_crawler.py -x` | Wave 0 |
| CONT-03 | 爬取热门评论 | integration | `pytest tests/test_crawler/test_comment_crawler.py -x` | Wave 0 |
| CONT-04 | 提取元数据 | unit | `pytest tests/test_crawler/test_metadata_extraction.py -x` | Wave 0 |
| NET-01 | 代理配置 | unit | `pytest tests/test_crawler/test_proxy_config.py -x` | Wave 0 |
| NET-02 | 请求频率控制 | unit | `pytest tests/test_crawler/test_rate_limiter.py -x` | Wave 0 |
| NET-03 | 自动签名处理 | integration | `pytest tests/test_crawler/test_api_intercept.py -x` | Wave 0 |
### Sampling Rate
- **Per task commit:** `pytest tests/test_crawler/ -x --tb=short`
- **Per wave merge:** `pytest tests/ -v`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `tests/test_crawler/__init__.py` - 测试模块初始化
- [ ] `tests/test_crawler/conftest.py` - 共享 fixtures (mock browser, mock API responses)
- [ ] `tests/test_crawler/test_answer_crawler.py` - 问答爬取测试
- [ ] `tests/test_crawler/test_article_crawler.py` - 专栏爬取测试
- [ ] `tests/test_crawler/test_comment_crawler.py` - 评论爬取测试
- [ ] `tests/test_crawler/test_metadata_extraction.py` - 元数据提取测试
- [ ] `tests/test_crawler/test_proxy_config.py` - 代理配置测试
- [ ] `tests/test_crawler/test_rate_limiter.py` - 频率控制测试
- [ ] `tests/test_crawler/test_api_intercept.py` - API 拦截测试
## Sources
### Primary (HIGH confidence)
- [Playwright Python Network Docs](https://playwright.dev/python/docs/network) - Route interception, proxy configuration
- [TikHub API Docs - Zhihu Comment V5](https://docs.tikhub.io/262614090e0) - 评论 API 参数说明
- Project existing code: `kb_workflow/auth/zhihu_login.py`, `kb_workflow/metadata/zhihu_extractor.py`
### Secondary (MEDIUM confidence)
- [MediaCrawler GitHub](https://github.com/NanmiCoder/MediaCrawler) - 知乎爬虫实现参考
- [Playwright Proxy Configuration](https://rg9.dev/posts/playwright-http-proxy/) - 多种代理配置方式
### Tertiary (LOW confidence)
- [How to Intercept API Calls in Playwright](https://roundproxies.com/blog/intercept-network-playwright/) - 教程参考
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH - 项目已有 Playwright 和 tenacity 使用经验
- Architecture: HIGH - Playwright 官方文档详细,模式清晰
- Pitfalls: MEDIUM - 部分问题需要实际运行验证
**Research date:** 2026-03-17
**Valid until:** 2026-04-17 (Playwright API 稳定,知乎 API 可能变化)