15 KiB
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:
# 已安装,无需额外安装
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:
# 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:
# 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:
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 拦截处理器未被调用
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 拦截存储响应
# 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)
代理配置读取
# 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 调用
# 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
-
热门评论数量限制
- What we know: API 默认返回 20 条,支持分页
- What's unclear: 决策说"只爬热门评论",是否限制为 20 条
- Recommendation: 默认 20 条,通过参数可配置
-
专栏文章的 API 端点
- What we know: 专栏页面有对应的 API 调用
- What's unclear: 具体的 API 路径格式
- Recommendation: 实现时通过 route 拦截自动发现
-
重试间隔递增策略
- 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 - Route interception, proxy configuration
- TikHub API Docs - Zhihu Comment V5 - 评论 API 参数说明
- Project existing code:
kb_workflow/auth/zhihu_login.py,kb_workflow/metadata/zhihu_extractor.py
Secondary (MEDIUM confidence)
- MediaCrawler GitHub - 知乎爬虫实现参考
- Playwright Proxy Configuration - 多种代理配置方式
Tertiary (LOW confidence)
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 可能变化)