Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
973d1df
docs: 小红书评论采集与定时管道设计文档
DNNinfo Jun 5, 2026
72855c0
docs: XHS Insight Pipeline 实现计划
DNNinfo Jun 5, 2026
b606d72
feat(insight): 包骨架、配置、insight_runs schema 与依赖声明
DNNinfo Jun 5, 2026
19dd791
feat(insight): InsightDB 运行记录与上游表计数
DNNinfo Jun 5, 2026
f52473c
feat(insight): build_crawl_args 把 job 翻译为爬虫参数
DNNinfo Jun 5, 2026
31b1b97
feat(insight): run_crawl 子进程封装(超时/退出码/stderr)
DNNinfo Jun 5, 2026
28c5097
feat(insight): crawl_entry 子进程入口(env 覆盖 max_notes 后委托上游)
DNNinfo Jun 5, 2026
c5e527a
feat(insight): orchestrator.run_job 编排一次完整采集周期
DNNinfo Jun 5, 2026
20d30c5
feat(insight): APScheduler 守护进程,按 cron 触发各 job
DNNinfo Jun 5, 2026
1ab8429
feat(insight): cli 入口 crawl-once / run-daemon / status
DNNinfo Jun 5, 2026
737cb6b
docs(insight): 使用说明与上游同步指南
DNNinfo Jun 5, 2026
1d79150
chore(scripts): add upstream sync & local commit helpers
DNNinfo Jun 6, 2026
e2a75be
chore(scripts): use ASCII-only strings to fix UTF-8 parse error
DNNinfo Jun 6, 2026
b447323
fix(insight): subprocess lifecycle hardening + cdp connect-existing fix
DNNinfo Jun 6, 2026
b3a4c4d
docs(viewer): 小红书数据查看器设计文档 (Streamlit)
DNNinfo Jun 6, 2026
cc5eef7
docs(viewer): spec self-review fixes (mockup cols, field map, run cmd)
DNNinfo Jun 6, 2026
5da2af2
docs(viewer): 实现计划 (7 个 task,TDD 驱动)
DNNinfo Jun 6, 2026
1caaf55
docs(plan): self-review fixes (Streamlit API, import order)
DNNinfo Jun 6, 2026
e56ff19
feat(viewer): 包骨架与 data.py stub(含烟雾测试)
DNNinfo Jun 6, 2026
0c6c4fb
feat(viewer): format_ts 时间戳格式化(含 None/0 兜底)
DNNinfo Jun 6, 2026
dda087d
test(viewer): use dynamic timestamp to avoid TZ coupling
DNNinfo Jun 6, 2026
51694f3
feat(viewer): load_notes 按发布时间倒序读取 xhs_note
DNNinfo Jun 6, 2026
7811332
feat(viewer): load_comments 按 note_id 过滤 + create_time 升序
DNNinfo Jun 6, 2026
81f95a4
feat(viewer): app.py Streamlit 单页 UI(笔记列表+详情+评论)
DNNinfo Jun 6, 2026
cad5fd7
fix(viewer): 安全 int 解析 + 刷新清旧选择 + 清理无用 import
DNNinfo Jun 6, 2026
7b773e1
docs(viewer): README 启动与使用说明
DNNinfo Jun 6, 2026
4aaf546
fix(viewer): format_ts 兼容毫秒级时间戳(DB 实际存的是 ms)
DNNinfo Jun 6, 2026
9d8689e
fix(viewer): 启动命令补 PYTHONPATH,新增 view_data.ps1 启动脚本
DNNinfo Jun 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,159 changes: 1,159 additions & 0 deletions docs/superpowers/plans/2026-06-05-xhs-insight-pipeline.md

Large diffs are not rendered by default.

722 changes: 722 additions & 0 deletions docs/superpowers/plans/2026-06-06-xhs-data-viewer.md

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions docs/superpowers/specs/2026-06-05-xhs-insight-pipeline-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# 小红书评论采集与定时管道(XHS Insight Pipeline)设计文档

- 日期:2026-06-05
- 状态:已确认设计,待编写实现计划
- 作者:Jake (chuangjieren@gmail.com)

## 1. 背景与目标

基于 MediaCrawler(本仓库为 `DNNinfo/MediaCrawler` fork)做二次开发,实现:

1. **定时**爬取小红书(XHS)的笔记与评论原始数据。
2. 把原始数据落入 **SQLite**,供后续分析使用。
3. 保持本项目代码与上游 MediaCrawler **可持续同步更新**,同时**不影响**自己开发的代码。

> 本期范围**仅包含**「定时爬取 + 原始数据入库 + 运行日志」。**不包含 LLM / 文本分析**。文本分析(计划使用本地 Ollama + Qwen)作为后续迭代,在不改动本期代码的前提下扩展。

## 2. 核心隔离策略(最重要的约束)

- 自研代码全部放入**单一新增顶层包 `insight/`**。
- **不修改** MediaCrawler 既有任何文件(`main.py`、`media_platform/`、`store/`、`database/`、`config/` 等保持上游原样)。
- `insight/` 通过两种方式与上游交互,均为「只读 / 旁路」:
- **调用**:以**子进程**方式运行 `uv run main.py …`。
- **读取**:读取爬虫写入的 SQLite 表(`xhs_note` / `xhs_note_comment`)。
- Git 层面:新增 `upstream` 远程,定期 `fetch` + `merge`/`rebase`。由于自研代码都是 `insight/` 下的**新增文件**,几乎不会与上游产生冲突。

### Git 同步工作流

```bash
# 一次性
git remote add upstream https://github.com/NanmiCoder/MediaCrawler.git

# 定期同步
git fetch upstream
git merge upstream/main # 或 git rebase upstream/main
```

- 不在上游文件中写任何自研配置;所有自研配置在 `insight/config.py` 自行声明。
- `insight/` 产生的数据/缓存通过 `.gitignore` 忽略(若需改 `.gitignore`,仅追加自研条目,尽量减少与上游接触面)。
- `insight/README.md` 记录上述同步步骤备忘。

## 3. 目录结构

```
MediaCrawler/ # 上游,零改动
├─ main.py, media_platform/, store/, database/, config/ …
└─ insight/ # ← 自研全部代码
├─ __init__.py
├─ config.py # db 路径、job 定义、爬虫参数
├─ cli.py # 入口:crawl-once / run-daemon / status
├─ runner.py # 子进程封装 uv run main.py …
├─ db.py # 写/读 insight_runs;按需读 xhs_* 校验数据
├─ schema.sql # insight_runs 表定义
├─ scheduler/
│ ├─ __init__.py
│ └─ daemon.py # APScheduler,每日触发
└─ README.md # 上游同步步骤 + 使用说明

tests/
└─ insight/ # 自研测试,独立目录
```

## 4. 数据流(一次调度周期)

1. **APScheduler**(`insight/scheduler/daemon.py`)到点触发某个 job。
2. **runner.py** 启动子进程:`uv run main.py --platform xhs --type search|detail|creator …`,并强制 `SAVE_DATA_OPTION=sqlite`(通过命令行/环境变量传入,不改上游配置文件)。
3. 爬虫将原始数据写入上游既有表 `xhs_note` / `xhs_note_comment`。
4. **db.py** 在 `insight_runs` 记录本次运行:job 名、开始/结束时间、子进程退出码、爬取条数、状态/错误信息。
5. 周期结束。原始数据留存于 SQLite,供后续分析迭代使用。

> 设计上爬取与记录是可独立运行的步骤:`insight crawl-once <job>` 可手动跑单次;`insight run-daemon` 启动常驻调度;`insight status` 查看最近运行记录。

## 5. 数据模型

### 5.1 上游既有表(只读,不改)

- `xhs_note`:`note_id`、`title`、`desc`、`source_keyword`、`time`、`liked_count`、`comment_count`、`collected_count` 等。
- `xhs_note_comment`:`comment_id`、`note_id`、`content`、`create_time`、`like_count`、`parent_comment_id`、`sub_comment_count`、`ip_location`、`nickname`、`user_id` 等。

### 5.2 自研表(`insight/schema.sql`,本期唯一新增表)

**`insight_runs`** — 每次调度周期一行:

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | INTEGER PK | 自增主键 |
| `job_name` | TEXT | 对应 `config.JOBS[].name` |
| `crawler_type` | TEXT | search / detail / creator |
| `started_ts` | INTEGER | 开始时间戳 |
| `finished_ts` | INTEGER | 结束时间戳(可空,运行中为空) |
| `exit_code` | INTEGER | 子进程退出码(可空) |
| `notes_crawled` | INTEGER | 本次新增/涉及笔记数(尽力统计) |
| `comments_crawled` | INTEGER | 本次新增/涉及评论数(尽力统计) |
| `status` | TEXT | running / success / error / timeout |
| `error_msg` | TEXT | 失败信息(可空) |

> 表名以 `insight_` 前缀与上游表隔离。后续分析迭代时新增的表同样使用该前缀。

## 6. Job 配置(`insight/config.py` 示例形态)

```python
# SQLite 路径(与爬虫共用同一文件)
DB_PATH = "./data/mediacrawler.db"

# 子进程超时(秒)
SUBPROCESS_TIMEOUT = 1800

# 调度任务定义
JOBS = [
{"name": "kw_daily", "type": "search", "keywords": "编程副业,编程兼职", "hour": 2, "max_notes": 20},
{"name": "watch_notes", "type": "detail", "note_ids": ["xxx", "yyy"], "hour": 3},
{"name": "creator_daily", "type": "creator", "creator_ids": ["zzz"], "hour": 4},
]
```

- 每个 job 映射为一次 `main.py` 调用;`type`→`--type`,其余字段→对应命令行参数/环境变量。
- 默认每日触发(`hour` 指定时刻)。

## 7. 运行环境与前置条件

- **登录态**:XHS 需扫码登录。调度运行在本机已登录的 Chrome(CDP 模式,`ENABLE_CDP_MODE=True`)。登录失效时子进程失败并记入 `insight_runs`,daemon 不崩溃。
- **常驻进程**:APScheduler 为进程内守护(B 方案)。本机重启后需**手动重启** daemon。
- **依赖**:APScheduler 作为自研依赖引入(评估加入 `pyproject.toml` / 单独 requirements,尽量不污染上游依赖声明,具体在实现计划中确定)。

## 8. 错误处理

- **子进程**:检查退出码 + 超时(`SUBPROCESS_TIMEOUT`)。失败 → `insight_runs` 记 `error`/`timeout`,不影响其他 job。
- **登录失效**:子进程失败被捕获并记录,等待下次调度或人工介入。
- **幂等**:重复运行安全——上游 SQLite 存储按主键/唯一键去重。
- **错过触发**:APScheduler 使用 `misfire_grace_time` 容忍短暂错过。

## 9. 测试策略

- `runner`:mock subprocess,验证命令拼装、超时与退出码处理。
- `db`:临时 SQLite,验证 `insight_runs` 写入/查询。
- `scheduler`:可控触发,验证 job → runner → 日志 串联,以及单 job 失败不影响其他 job。
- 测试位于 `tests/insight/`,与上游测试隔离。

## 10. 后续迭代(本期不实现,仅预留)

- 在 `insight/analysis/` 增加本地 Ollama(Qwen)分析模块:逐评论结构化打标 + 逐笔记聚合简报。
- 新增分析结果表(`insight_*` 前缀),按 `comment_id` 左连接做增量分析。
- 这些扩展均为新增文件,不改动本期代码,符合隔离策略。

## 11. 未决/待实现计划阶段确认的细节

- APScheduler 依赖的具体引入方式(pyproject vs 独立 requirements)。
- `notes_crawled` / `comments_crawled` 的统计方式(运行前后对表计数差值)。
- `SAVE_DATA_OPTION=sqlite` 的传参方式(命令行参数 vs 环境变量 vs 临时配置覆盖)——需在实现时确认上游 `main.py` 支持的覆盖手段,仍以「不改上游」为前提。
186 changes: 186 additions & 0 deletions docs/superpowers/specs/2026-06-06-xhs-data-viewer-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# 小红书数据查看器(XHS Data Viewer)设计文档

- 日期:2026-06-06
- 状态:已确认设计,待编写实现计划
- 作者:Jake (chuangjieren@gmail.com)
- 关联:本文是 [2026-06-05-xhs-insight-pipeline-design.md](./2026-06-05-xhs-insight-pipeline-design.md) 的下游配套(数据查看器)

## 1. 背景与目标

上一期「XHS Insight Pipeline」已经能定时把小红书笔记与评论爬到 `database/sqlite_tables.db`。当前数据规模:44 条 `xhs_note`、435 条 `xhs_note_comment`、10 条 `insight_runs`。

本期目标:**提供一个可重复启动的本地小工具,让人能快速查看这些已爬到的数据**。

- **使用方式**:每次爬完一批数据后,执行一个命令 → 浏览器里看到笔记列表 + 点进去看评论。
- **范围**:基础查看即可。**不**做搜索、筛选、导出、采集运行面板等。
- **平台约束**:Windows;实现要简单。

## 2. 核心隔离策略

延续上期「不修改任何上游文件」的原则:

- 自研代码全部放入**新增子包 `insight/viewer/`**。
- **不修改** MediaCrawler 既有任何文件,不修改 `insight/` 中既有的 `cli.py` / `runner.py` / `orchestrator.py` / `db.py` / `config.py` / `crawl_entry.py` 等。
- 只读 `database/sqlite_tables.db`,**不写**任何表(`insight_runs` 也只读)。
- 与 `insight/` 同级(`insight/viewer/`),未来可单独升级/替换。

## 3. 方案选型

候选三选一:

| 方案 | 代码量 | 表格展示 | 依赖体积 | 结论 |
|---|---|---|---|---|
| **A. Streamlit** | ~80 行 Python,0 行 HTML | 极好 | 中(~150MB) | **采用** |
| B. Flask + HTML | 1 个 .py + 1 个 .html | 一般 | 小 | 否 |
| C. Gradio | ~100 行 | 弱 | 中 | 否 |

理由:

- 「基础查看 + Windows + 简单实现」三个约束下,Streamlit 匹配度最高。
- 不写一行 HTML/CSS/JS,符合「简单实现」。
- `st.dataframe` 自带列排序、`st.session_state` 选行状态、`st.cache_data` 缓存都内置。
- 依赖通过 `uv run --with streamlit` 临时拉取,**不写进** `pyproject.toml` / `requirements.txt`,避免污染上游依赖。

## 4. 文件结构

```
insight/viewer/
├── __init__.py # 空,仅作包
├── app.py # Streamlit 入口(页面布局、session_state)
├── data.py # 纯函数:load_notes() / load_comments(note_id) / format_ts(ts)
└── README.md # 启动说明
```

职责切分:

- `data.py`:纯数据层,**不导入** Streamlit,方便单测与未来换 UI。
- `app.py`:仅做页面布局和交互,调 `data.py` 取数据。
- 复用约定:SQLite 路径 = `database/sqlite_tables.db`(与上游 / `insight/db.py` 一致),**不读 `.env`**。

## 5. 界面与交互

单页面三块布局(自上而下):

```
┌─────────────────────────────────────────────────────────┐
│ 小红书数据查看 共 44 条笔记 / 435 条评论 [刷新] │ ← 顶部
├─────────────────────────────────────────────────────────┤
│ 笔记列表(按发布时间 time 倒序) │
│ ┌─────┬──────────────┬──────┬─────────┬────────┬──────┐│
│ │ # │ 标题 │ 点赞 │ 评论数 │ 关键词 │ 时间 ││
│ ├─────┼──────────────┼──────┼─────────┼────────┼──────┤│
│ │ 1 │ 春日穿搭... │ 1.2k │ 38 │ 穿搭 │ 03-15││
│ │ 2 │ 护肤心得... │ 856 │ 12 │ 护肤 │ 03-14││
│ │ ... │ │ │ │ │ ││
│ └─────┴──────────────┴──────┴─────────┴────────┴──────┘│
├─────────────────────────────────────────────────────────┤
│ ▾ 笔记详情(选中后展开) │
│ 作者:xxx | 发布时间:2024-03-15 | 关键词:穿搭 │
│ 正文:今天分享... │
│ ───────────────────────────────── │
│ 评论(38 条) │
│ ┌────┬──────────┬───────────────────────────────┐ │
│ │ 点赞│ 用户 │ 内容 │ │
│ └────┴──────────┴───────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```

交互细节:

- 笔记列表用 `st.dataframe`,自带列排序。
- 「查看」用按钮 → 写入 `st.session_state["selected_note_id"]`,详情区根据它重新查询。
- 顶部 `[刷新]` 按钮 → `st.cache_data.clear()` 后重读。
- 时间戳统一用 `format_ts` 转 `YYYY-MM-DD HH:MM`。
- 大文本字段(正文/评论)用 `st.markdown` 容器展示,长文本自然换行(数据规模小,**不做折叠/分页**)。
- 缓存:`@st.cache_data(ttl=60)`,60 秒自动重读;不点刷新也最多延迟 1 分钟。
- 数据规模小(44 + 435),**不做评论分页**。

## 6. 字段映射

UI 展示字段与数据库字段对应:

| UI 列 | 数据源 |
|---|---|
| 标题 | `xhs_note.title` |
| 点赞 | `xhs_note.liked_count` |
| 评论数 | `xhs_note.comment_count` |
| 发布时间 | `xhs_note.time` → `format_ts` |
| 关键词 | `xhs_note.source_keyword` |
| 笔记作者 | `xhs_note.nickname` |
| 正文 | `xhs_note.desc` |
| 评论用户 | `xhs_note_comment.nickname` |
| 评论内容 | `xhs_note_comment.content` |
| 评论点赞 | `xhs_note_comment.like_count` |
| 评论时间 | `xhs_note_comment.create_time` → `format_ts` |

## 7. 错误处理

所有错误在 UI 层用 `st.error` 友好展示,不抛 traceback:

| 场景 | 行为 |
|---|---|
| SQLite 文件不存在 | `未找到数据库 database/sqlite_tables.db,请先运行爬虫` |
| `xhs_note` 表不存在 | `数据库缺少表 xhs_note,请先执行 uv run python main.py --init_db sqlite` |
| 笔记表为空 | 表格区显示 `暂无数据` |
| 选中笔记无评论 | 评论区显示 `该笔记暂无评论` |
| 数据库被爬取进程持锁 | `try/except sqlite3.OperationalError` → `数据库正忙,请稍后重试` |
| 单条记录字段为 None | UI 显示 `—`,不崩 |

## 8. 数据流

```
app.py
└─ st.cache_data(ttl=60)
└─ data.load_notes() → list[dict] (SELECT * FROM xhs_note ORDER BY time DESC)
└─ data.load_comments(id) → list[dict] (SELECT … FROM xhs_note_comment WHERE note_id = ?)
└─ data.format_ts(int|None) → str (时间戳→可读字符串)
```

设计决策:

- **不**复用 `database/db_session.py`:那是异步 SQLAlchemy,为爬取设计;本工具只读、低频、同步访问,独立 `sqlite3.connect` 更轻、更稳、更易测。
- 所有 SQL 集中在 `data.py`,`app.py` 不出现 SQL 字符串。
- 工具对数据库**只读不写**。

## 9. 测试

`tests/insight/test_viewer.py`,**仅测 `data.py` 纯函数**,不启 Streamlit:

- `test_load_notes_returns_list`:临时 SQLite 灌 3 条假数据 → 验证返回条数与字段
- `test_load_comments_filters_by_note_id`:灌 2 笔记 + 3 评论 → 验证过滤正确
- `test_load_comments_empty_when_no_match`:不存在的 `note_id` → 返回 `[]`
- `test_format_ts_handles_none_and_zero`:None / 0 / 正常时间戳都正常处理
- `test_missing_table_raises_operational_error`:删表后调函数 → 验证抛 `sqlite3.OperationalError`(让 UI 捕获后展示)

`app.py` 不写 Streamlit 单测(成本/收益不划算),通过 `uv run … insight.viewer.app` 手动烟雾测试一次。

## 10. 启动方式

`insight/viewer/app.py` 是普通的 Streamlit 脚本(模块级调用 `st.*`),从项目根目录执行:

```bash
uv run --with streamlit streamlit run insight/viewer/app.py
# → 自动打开 http://localhost:8501
```

补充:

- 依赖通过 `--with streamlit` 临时拉取,**不修改** `pyproject.toml` / `requirements.txt`。
- `insight/viewer/README.md` 写一份简要说明(含首次跑、刷新操作、停止 Ctrl+C)。

## 11. YAGNI 清单(本期明确不做)

- 全局搜索框(笔记标题 + 正文 + 评论)
- 按关键词/作者下拉筛选
- 数据导出按钮(CSV / Excel / JSON)
- `insight_runs` 采集运行记录面板
- 评论分页
- 多人协作 / 用户登录
- 在查看器中编辑 / 删除数据

## 12. 与上期设计的关系

- 上期 `insight/` 提供「爬数据」能力,本期 `insight/viewer/` 提供「看数据」能力。
- 两者**互不依赖**:viewer 不 import `insight.cli` / `insight.runner` / `insight.orchestrator` / `insight.config`。
- 共用同一个 SQLite 文件与同一个表结构,**这是唯一的耦合点**。
- 上期 Git 同步策略(`upstream` remote + `merge`/`rebase`)继续适用。
57 changes: 57 additions & 0 deletions insight/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# insight — 小红书评论定时采集(MediaCrawler 二次开发)

本包是对 MediaCrawler 的二次开发,**不修改任何上游文件**,全部代码在 `insight/` 内。
本期范围:定时爬取 + 原始数据入库(复用上游 SQLite)+ 运行日志。**不含文本分析**(后续迭代)。

## 一次性准备

```bash
# 1. 初始化上游 SQLite 表结构(创建 xhs_note / xhs_note_comment 等)
uv run python main.py --init_db sqlite

# 2. 确保已登录小红书(CDP 模式,复用本机 Chrome 登录态)
# 参见项目根 README 的 Chrome 远程调试配置
```

## 配置

编辑 `insight/config.py` 的 `JOBS` 列表(job 类型 search/detail/creator、关键词/笔记ID/创作者ID、触发时刻 hour/minute、max_notes 等)。

## 使用

```bash
# 立即跑一次某个 job(不依赖 apscheduler)
uv run python -m insight.cli crawl-once kw_daily

# 查看最近运行记录(不依赖 apscheduler)
uv run python -m insight.cli status --limit 20

# 启动定时守护进程(前台运行,Ctrl+C 退出;需 apscheduler)
uv run --with "apscheduler>=3.10,<4" python -m insight.cli run-daemon
```

> 守护进程为前台常驻进程;本机重启后需手动重新启动。

## 运行测试

```bash
uv run --with "apscheduler>=3.10,<4" pytest tests/insight -v
```

## 与上游 MediaCrawler 同步更新

```bash
# 一次性:添加上游远程
git remote add upstream https://github.com/NanmiCoder/MediaCrawler.git

# 定期同步
git fetch upstream
git merge upstream/main # 或 git rebase upstream/main
```

因为本包全部是 `insight/` 下的新增文件、未改动任何上游文件,合并几乎不会冲突。
唯一与上游耦合的假设:
- 上游 CLI 参数(`--platform/--type/--keywords/--specified_id/--creator_id/--save_data_option` 等)保持不变;
- SQLite 路径仍为 `database/sqlite_tables.db`(见 `config/db_config.py`);
- `main.main` / `main.async_cleanup` / `tools.app_runner.run` 接口保持不变。
同步后若上述任一处变化,只需相应调整 `insight/runner.py`、`insight/config.py`、`insight/crawl_entry.py`。
2 changes: 2 additions & 0 deletions insight/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
"""自研二次开发包:小红书评论定时采集与运行记录。不修改 MediaCrawler 上游文件。"""
Loading