V2EX 话题爬虫 + AI 分析 — 设计文档

1. 项目概览

用途:抓取 V2EX 论坛指定节点下的帖子及回复,存为本地 JSON 文件,再调用 LLM 提炼创意、痛点、独立开发者机会、趋势洞察。

运行方式:命令行工具,可 Docker 部署。两个核心脚本 + 一个配置文件 + 本地数据目录。

语言选择:不限。推荐 Python(urllib 无依赖),也可用 TypeScript/Go/Rust。Docker 镜像任选。


2. 项目结构

.
├── config.json              # 节点列表 + 爬取参数
├── crawler.py               # 爬虫:抓取帖子 → 本地 JSON
├── analyzer.py              # 分析:读 JSON → 调 LLM → 输出报告
├── data/
│   ├── state.json           # 运行状态(时间戳)
│   ├── analysis.md           # 全量分析报告(analyzer.py 全量输出)
│   ├── analysis_today.md    # 今日增量报告(analyzer.py --today 输出)
│   ├── .cache/              # 分析模块的中间缓存(LLM 返回结果)
│   │   └── topic_lists/    # 爬虫模块的帖子列表日缓存
│   ├── programmer/           # 每个节点一个子目录
│   │   ├── 1229217.json    # 每个帖子一个 JSON 文件
│   │   └── ...
│   └── python/
│       └── ...
└── README.md

关键约定

  • data/ 下按节点名建子目录,每个帖子存一个 {id}.json

  • state.json 记录 last_crawllast_analysis 两个 unix 时间戳

  • 缓存全放在 data/.cache/ 下,按 run_id 区分不同批次


3. 配置文件

3.1 config.json 结构

{
  "nodes": ["programmer", "python", ...],   // 要爬取的节点名列表
  "pages_per_node": 5,                          // 每个节点爬多少页(每页 ~10 条)
  "request_delay": 1.2,                        // API 请求间隔(秒),控制频率
  "max_retries": 3                             // 请求失败重试次数
}

设计要点

  • 节点名对应 V2EX API 中的 node_name 参数

  • 延迟和重试在 crawler.py 中使用,analyzer.py 不直接读

  • 所有硬编码参数都抽象到配置文件

3.2 环境变量(分析模块用)

变量

默认值

说明

OPENAI_API_KEY

无,必传

LLM API Key

OPENAI_BASE_URL

https://api.openai.com/v1

OpenAI 兼容 API 地址

ANALYZE_MODEL

gpt-4o-mini

模型名

设计要点

  • API Key 不写入配置文件,不硬编码,只走环境变量

  • 兼容任何 OpenAI 协议的 API(DeepSeek、OpenRouter 等)


4. 数据结构

4.1 帖子 JSON(爬虫输出/分析输入)

{
  "id": 1229217,                    // 帖子 ID(整数)
  "title": "...",                    // 标题
  "content": "...",                 // 正文(可能为空字符串)
  "member": "用户名",               // 发帖人
  "created": 1784774167,           // 发帖时间(unix timestamp)
  "replies_count": 5,              // 回复数(整数)
  "node": "programmer",            // 所属节点
  "url": "https://www.v2ex.com/t/1229217",  // 帖子链接
  "reply_list": [
    {
      "id": 123456,               // 回复 ID
      "member": "回复人",
      "content": "...",           // 回复内容(可能空)
      "created": 1784775000      // 回复时间
    }
  ]
}

关键约束

  • contentreply.content 都可能是空字符串(V2EX API 的实际行为)

  • reply_list 最多存 10 条回复(分析时只读前 10 条,避免 token 爆炸)

  • replies_count 是原始回复总数,不等于 len(reply_list)

  • created 是 unix timestamp,用于增量过滤和排序

4.2 state.json 结构

{
  "last_crawl": 1785299416,       // 最后一次爬取时间戳
  "last_analysis": 1784793852      // 最后一次分析时间戳
}

字段说明

  • last_crawlcrawler.py --today 用,过滤此时间之后的帖子

  • last_analysisanalyzer.py --today 用,标记增量范围

  • 两者独立,允许只爬不分析或只分析不爬

4.3 缓存结构

爬虫帖子列表缓存data/.cache/topic_lists/{node}_{YYYY-MM-DD}.json

  • fetch_topics 的完整返回(帖子列表数组)

  • 同一天内重复运行直接读缓存,不重复请求 API

  • 校验条件:缓存条数 ≥ pages_per_node * 5(确保覆盖足够)

分析中间缓存data/.cache/{run_id}_{key}.json

  • run_id = md5(帖子ID列表拼接).hexdigest()[:12],确保同一批帖子复用同一缓存

  • key 格式:b{批次号}_{类别名},如 b0_ideasb3_pain

  • {"key": "...", "result": "LLM 原始返回文本"}

  • 分析完成后清除(_clean_cache


5. 爬虫模块 (crawler.py)

5.1 API 层

函数fetch_json(path, params="", retries=MAX_RETRIES)

输入

  • path:API 路径,如 "topics/show.json""replies/show.json""nodes/all.json"

  • params:查询参数字符串,如 "node_name=python&p=1"

输出:解析后的 JSON(list 或 dict),失败返回 []

行为

  1. 拼接 URL:https://www.v2ex.com/api/{path}?{params}

  2. 设置 User-Agent:V2EX-Crawler/1.0(V2EX v1 API 无需认证)

  3. 超时 30 秒

  4. 指数退避重试:delay * 2^attempt,上限 60 秒

  5. HTTP 404/403 直接返回 [],不重试(帖子被删或权限不足)

  6. 其他错误重试到 max_retries

注意:用标准库 urllib.request,不引入第三方依赖(requests 等)。

5.2 帖子列表抓取

函数fetch_topics(node, pages=3)

逻辑

  1. 先检查今日缓存 data/.cache/topic_lists/{node}_{today}.json

    • 存在且条数 ≥ pages * 5:直接返回缓存

  2. 否则逐页请求 topics/show.json?node_name={node}&p={p}

  3. set 去重(API 分页可能返回重复数据)

  4. 每页间 sleep(DELAY)(1.2 秒)

  5. 写入缓存文件

返回:去重后的帖子列表(每帖含 id/title/content/member/created/replies/node/url)

5.3 回复抓取

函数fetch_replies(topic_id)

调用replies/show.json?topic_id={topic_id}

返回:回复列表(每回复含 id/member/content/created)

注意:调用后 sleep(DELAY),控制请求频率

5.4 帖子保存

函数save_topic(out_dir, topic, replies)

输入

  • out_dir:节点子目录,如 data/programmer/

  • topic:帖子元数据(来自 fetch_topics

  • replies:回复列表(来自 fetch_replies

输出{out_dir}/{topic_id}.json,按 4.1 结构序列化

注意reply_list 只存前 10 条回复(控制文件大小和后续分析 token)

5.5 主入口

函数crawl(node, pages=3, output_dir=None, since=None)

参数

  • node:节点名

  • pages:爬取页数

  • output_dir:默认 data/{node}

  • since:unix timestamp,增量模式时过滤此时间之后的帖子

流程

  1. 创建输出目录

  2. 调用 fetch_topics 获取帖子列表

  3. 如果 since 传入,过滤 created >= since 的帖子

  4. 遍历帖子:

    • 本地已有对应 JSON 文件 → 跳过(不重复请求)

    • 否则调用 fetch_repliessave_topic

  5. 打印进度:[i/总数] id - 标题

  6. 返回新增帖子数

5.6 CLI 入口

python3 crawler.py                    # 全量爬取
python3 crawler.py --today            # 增量爬取(只爬上次之后的)
python3 crawler.py list               # 列出所有可用节点
python3 crawler.py list python       # 按关键词过滤节点

--today 模式逻辑

  1. state.json 中的 last_crawl 时间戳

  2. 传入 crawl(..., since=last_crawl)

  3. 爬取完成后更新 state.jsonlast_crawl 为当前时间

list 模式

  • 调用 nodes/all.json 获取所有节点

  • parent_node_name 分组打印

  • 支持关键词过滤(匹配 nametitle


6. 分析模块 (analyzer.py)

6.1 核心分类体系

四个分析维度,每个维度独立分析 + 独立合并:

key

标题

定义

ideas

好的创意/产品点子

帖子中提到或暗示的有价值的想法、工具需求、产品方向

pain

用户痛点

用户反复抱怨、求助、表达不满的问题

indie

个人开发者机会

对独立开发者/小团队友好的方向,侧重低门槛、可快速验证

trend

趋势洞察

社区关注的技术趋势或话题走向

设计要点

  • 四个维度互不干扰,每个类别独立一个 prompt 调用

  • 单次 prompt 只分析一个类别,不混合

  • 每个类别要求「尽可能多地提炼,不要限制条数」

6.2 Prompt 设计

单批次分析 prompt

分析以下 V2EX 帖子,只提炼「{title}」类信息。
​
定义:{desc}
​
要求:
- 只输出这一类,不要输出其他类别
- 不要限制条数,尽可能多地提炼有价值的信息
- 每条附上来源帖子标题
- 用中文回答
​
---(第 {batch_idx+1}/{total_batches} 批)
​
{帖子文本}

帖子文本格式

## {标题}
作者: {用户名} | 回复数: {N}
{正文前 500 字符}
  - {回复人}: {回复内容前 200 字符}
  - ...
---
  • 每个帖子正文截断 500 字符,每帖最多 10 条回复,每条回复截断 200 字符

  • 目的:控制 token,避免超长帖子炸上下文

合并 prompt(全量模式):

以下是多批次的 V2EX 帖子分析结果,请合并去重,输出一份最终报告。
​
要求:
- 去除重复条目,保留最有代表性的描述
- 不要限制条数,尽可能保留所有有价值的信息
- 按价值从高到低排列
- 用中文回答

合并 prompt(增量模式):

同上,但标题注明这是今日新增分析

6.3 批量分析 + 缓存

函数analyze(topics, incremental=False)

输入

  • topics:帖子列表(从 load_topics 读取的完整 JSON 对象)

  • incremental:是否增量模式(影响合并 prompt)

参数

  • BATCH_SIZE = 15:每批 15 个帖子

  • MERGE_SIZE = 3:合并时每 3 个结果合并一次

流程

  1. 计算 run_id

    • run_id = md5(所有帖子 ID 拼接).hexdigest()[:12]

    • 同一批帖子不同次运行复用同一缓存

  2. 分批[topics[i:i+15] for i in range(0, len, 15)]

  3. 每批并发分析 4 个类别

    • 对每个类别 (key, title, desc)

      • 先查缓存 data/.cache/{run_id}_b{batch_idx}_{key}.json

      • 缓存命中 → 直接用

      • 缓存未命中 → 调 LLM → 写缓存

    • 每批返回 [(类别标题, 分析结果), ...] 共 4 项

  4. 按类别独立合并

    • 每个类别自己的 N 个结果,层级合并:

      • N ≤ MERGE_SIZE(3) → 一次合并

      • N > 3 → 每 3 个一组合并,递归直到只剩 1 个

    • 合并时调用 LLM(timeout=300,比分析更长)

    • 增量模式用 MERGE_PROMPT_INCREMENTAL,全量用 MERGE_PROMPT

  5. 清理缓存:分析完成后删除该 run_id 的所有缓存文件

返回{ "好的创意/产品点子": "文本", "用户痛点": "文本", ... } 字典

6.4 API 调用

函数call_api(prompt, timeout=120, retries=3)

实现

  • POST {BASE_URL}/chat/completions

  • Body:{"model": MODEL, "messages": [{"role": "user", "content": prompt}], "temperature": 0.3, "max_tokens": 4096}

  • Header:Authorization: Bearer {API_KEY}Content-Type: application/json

  • 超时 120 秒(单次分析),合并调用传 300 秒

  • 失败重试:指数退避,5 * 2^attempt,上限 60 秒

6.5 帖子加载

函数load_topics(data_dir, since=None)

输入

  • data_dir:节点数据目录,如 data/programmer/

  • since:unix timestamp,只加载此时间之后的帖子

实现

  • 遍历目录下所有 *.json 文件

  • 按文件名排序(确保稳定性)

  • since 传入时过滤 created < since 的帖子

  • 返回完整帖子对象列表

6.6 主入口

函数main(nodes=None, incremental=False)

流程

  1. 自动发现节点:如果 nodes 为 None,扫描 data/ 下所有子目录

  2. 增量模式时读 state.json,取 last_analysislast_crawlsince

  3. 遍历每个节点目录,调用 load_topics 加载帖子

  4. 给每个帖子加 source_node 字段(标记来源节点)

  5. 合并所有节点帖子,调 analyze

  6. 写分析报告到 data/analysis.md(全量)或 data/analysis_today.md(增量)

  7. 更新 state.jsonlast_analysis

  8. 控制台打印每个维度的完整结果

报告格式

# V2EX 多节点分析

节点: programmer, python, ...

基于 1234 个帖子自动生成

## 好的创意/产品点子

{LLM 合并后的文本}

## 用户痛点

{LLM 合并后的文本}

...

6.7 CLI 入口

python3 analyzer.py                                  # 全量分析
python3 analyzer.py --today                          # 增量分析(只分析新帖)

环境变量检查

  • OPENAI_API_KEY 为空时直接 exit(1),提示设置方法

  • 提示中列出三个环境变量和可选值


7. 每日增量工作流

7.1 设计理念

每天上班前跑一次 python3 crawler.py --today && python3 analyzer.py --today

首次运行

  • crawler.py --today:没有 last_crawl 时间戳,since=None,全量爬取

  • analyzer.py --today:没有 last_analysis 时间戳,since=None,全量分析

  • 运行完各自记录时间戳

后续运行

  • 爬虫只请求 created >= last_crawl 的新帖子

  • 分析只加载 created >= last_analysis 的新帖子

  • 本地已存在的 JSON 文件自动跳过

7.2 状态流转

首次运行:
  state.json 不存在 → 全量爬取 → 写 last_crawl
  state.json 不存在 → 全量分析 → 写 last_analysis

后续运行:
  读 last_crawl → 增量爬取(since=last_crawl)→ 更新 last_crawl
  读 last_analysis → 增量分析(since=last_analysis)→ 更新 last_analysis

边界情况

  • 只有 last_crawl 没有 last_analysis:分析时 fallback 到 last_crawl

  • 两个时间戳都没有:全量模式,打印「首次运行」提示

  • 增量模式下没有新帖子:打印「没有新增帖子,无需分析」并退出


8. 关键边界情况 & 健壮性

8.1 API 限流

  • V2EX v1 API 限制约 600 次/小时/IP

  • request_delay 默认 1.2 秒,确保不超限

  • 指数退避重试,404/403 不重试直接跳过

8.2 帖子去重

  • fetch_topics 内部用 set 去重(API 分页可能返回重复)

  • crawl 主循环检查本地文件是否存在,已存在跳过(断点续传)

8.3 大帖子分析

  • 每批 15 个帖子,超过 15 个自动分多批

  • 正文截断 500 字符,回复截断 200 字符,每帖最多 10 条回复

  • 多批次合并使用层级合并,避免单次 prompt 过大

8.4 缓存策略

  • 爬虫缓存:帖子列表按天缓存,同一天不重复请求

  • 分析缓存:按 run_id + key 缓存单个类别的 LLM 结果,重跑不重复调 API

  • 分析完成后清理缓存,避免磁盘堆积

8.5 错误处理

  • 爬虫 API 失败返回 [],不抛异常,不中断流程

  • 分析 API 重试 3 次后仍失败则 raise,终止分析

  • 环境变量缺失时 exit(1),打印配置说明

8.6 目录创建

  • 所有 os.makedirsexist_ok=True

  • 输出目录不存在时自动创建,不要求预先存在


9. Docker 部署

9.1 镜像设计

FROM python:3.12-slim
WORKDIR /app
COPY crawler.py analyzer.py config.json ./
RUN mkdir -p data
ENV OPENAI_API_KEY="" \
    OPENAI_BASE_URL="https://api.openai.com/v1" \
    ANALYZE_MODEL="gpt-4o-mini"

运行

# 爬取
docker run --rm -v $(pwd)/data:/app/data -v $(pwd)/config.json:/app/config.json \
  v2ex-crawler python3 crawler.py --today
​
# 分析
docker run --rm \
  -v $(pwd)/data:/app/data \
  -v $(pwd)/config.json:/app/config.json \
  -e OPENAI_API_KEY=sk-xxx \
  -e OPENAI_BASE_URL=https://api.deepseek.com/v1 \
  -e ANALYZE_MODEL=deepseek-chat \
  v2ex-crawler python3 analyzer.py --today

要点

  • data/ 目录挂载出来,保证数据持久化

  • config.json 单独挂载,方便修改节点列表

  • API Key 通过 -e 传入,不写死

  • 两个命令分开跑,互不依赖

9.2 可选:cron 定时

# 每天 9 点自动跑
0 9 * * * cd /path/to/project && python3 crawler.py --today && python3 analyzer.py --today

Docker 版用宿主 cron 或 docker-compose 的 ofelia 调度。


10. 实现优先级

优先级

模块

理由

P0

crawler.py 基础抓取

核心功能,没有数据什么都没法做

P0

帖子 JSON 数据结构

定义数据格式,两个模块都依赖

P1

analyzer.py 单批次分析

核心功能,能跑通单个类别分析

P1

增量模式(两个模块)

每日工作流,项目主要使用场景

P2

缓存系统(两个模块)

降低 API 调用量,省成本

P2

层级合并

处理大量帖子时的合并策略

P3

Docker 部署

可选项,本地脚本能跑就行

P4

list_nodes / 节点浏览

辅助功能,不常用