需求:评论栏/作品栏要能分清是哪个博主。修好分组字段之后名字仍带星号,因为上游作为 教学版默认对昵称做中间脱敏(首尾各留 1 字,中间星号)。这个脱敏是**有损**的: 「张三」和「张四」都变成「张*」 「小明老师」和「小刚老师」都变成「小***师」 而本仓库的用途是监控一批公开创作者账号,分清谁是谁正是这一层要干的事。所以关掉它。 * config/base_config.py 新增 MASK_NICKNAME = False(和 INJECT_ALL_COOKIES 一样是个 开关,不是删代码 —— 改回 True 就恢复上游行为)。 * tools/user_hash.py 的 mask_nickname 读这个开关,关闭时原样返回。读的是模块属性而 不是导入值,测试才能 monkeypatch。脱敏实现本身一字未动。 * 顺带修一个数据陈旧问题:评论是去重后直接 continue 的,昵称只在首次入库时写一次, 于是开关一改(或评论者改名)老评论永远停在旧值 —— 而重采是唯一能拿到新值的途径。 现在已存在的评论会跟着刷新昵称(作品那边的 creator_name 早就是这么做的)。 * anonymous 的 creator_hash 保持不变:那是分组用的稳定键,不是显示名。 测试: * 三个隐私套件 + weibo 的 autouse fixture 强制把开关打开 —— 它们验的是**脱敏机制 本身**,机制仍然必须正确,所以显式打开来测,而不是让它们随部署配置漂。 * test_mask_and_hash_tools 改成两个方向都覆盖(开着脱敏 / 关着脱敏)。 * test_tieba_extractor.py 里 8 处字面量的脱敏期望值换成真实昵称 —— 提取器现在就是 返回原文的,期望值理应跟着改(这一条是行为变更的直接后果,不是测试放宽)。 * 新增一条:已入库的评论昵称会随重采刷新(且不会因刷新而重复插入)。
199 lines
10 KiB
Markdown
199 lines
10 KiB
Markdown
# 与上游的差异管理
|
||
|
||
本仓库在 [NanmiCoder/MediaCrawler](https://github.com/NanmiCoder/MediaCrawler) 之上加了一层
|
||
监控/鉴权/多平台面板。这份文档记录**改了上游哪些文件、为什么**,以及**上游更新时怎么合并**。
|
||
|
||
---
|
||
|
||
## 一、改动分三类
|
||
|
||
冲突风险从低到高:
|
||
|
||
### 1. 纯新增文件(零冲突)
|
||
|
||
上游怎么改都不会碰到它们:
|
||
|
||
```
|
||
api/auth.py WebUI 登录鉴权
|
||
api/monitor/* 监控层整体(含 platforms.py 能力矩阵)
|
||
api/monitor/db.py MySQL 连接层(可回退 SQLite 供测试用)
|
||
api/monitor/migrate_from_sqlite.py SQLite → MySQL 一次性迁移脚本
|
||
api/monitor/upstream.py 上游更新检查(定时 fetch 上游并比对)
|
||
api/routers/{auth,monitor,settings}.py
|
||
api/schemas/{auth,monitor,settings}.py
|
||
api/services/interpreter.py 解释器探测(uv / .venv / 当前解释器)
|
||
webui/src/components/{monitor,settings,auth}/ 新视图
|
||
webui/src/components/layout/{PlatformSwitcher,UnwiredPlatformNotice}.tsx
|
||
webui/src/{hooks/useMonitor.ts,hooks/usePlatform.ts,store/platformStore.ts,lib/monitorFormat.ts,types/monitor.ts}
|
||
docs/监控功能使用说明.md
|
||
tests/test_{auth,settings,platforms,qrlogin,monitor_*,upstream}.py
|
||
Dockerfile / .dockerignore / docker-compose.yml 服务器部署用
|
||
```
|
||
|
||
> `api/monitor/upstream.py` 要调 `git`,而 `python:3.11-slim` 不带它 —— Dockerfile 里为此
|
||
> **显式装了 git**。改了 Dockerfile 就必须重建镜像(`docker compose build`),`./deploy.sh`
|
||
> 只重建前端,不重建镜像。
|
||
|
||
其中 `api/monitor/qrlogin.py` + `webui/.../QrLoginPanel.tsx` 是**服务器专用**的扫码登录:
|
||
那台机器上 Chrome 跑在 Xvfb 里,`show_qrcode` 调的 PIL `Image.show()` 需要桌面看图程序,
|
||
服务器没有,二维码会无处可去。所以改成用 CDP 把二维码从页面里读出来交给前端 `<img>` 显示。
|
||
|
||
### 2. 加法改动(低冲突)
|
||
|
||
只在既有文件里**新增**内容,不改动原有行:
|
||
|
||
| 文件 | 加了什么 |
|
||
|---|---|
|
||
| `cmd_arg/arg.py` | typer 选项:`--enable_cdp_mode`、`--inject_all_cookies`、`--save_login_state`、`--cookies_file`、`--crawler_max_sleep_sec`,以及对应的 `config.*` 回写 |
|
||
| `api/schemas/crawler.py` | `CrawlerStartRequest` 的若干**可选**字段(默认 `None`,不传则不加对应 CLI 参数) |
|
||
| `config/base_config.py` | `INJECT_ALL_COOKIES = False`;`MASK_NICKNAME = False`(关掉昵称脱敏,见第 3 节) |
|
||
| `api/routers/__init__.py` | 导出新增的 router |
|
||
| `requirements.txt` | 补上 `websockets`(上游 `pyproject.toml` 里有、`requirements.txt` 里漏了) |
|
||
| `tests/conftest.py` | 新增 `_bypass_auth_for_non_auth_suites` fixture |
|
||
|
||
### 3. 接线改动(中冲突,需要人看)
|
||
|
||
| 文件 | 改了什么 | 上游若在此处变动 |
|
||
|---|---|---|
|
||
| `api/main.py` | 注册 4 个 router 并加 `Depends(require_auth)`;`lifespan` 里初始化监控库、启动调度器、跑设置键迁移;`load_dotenv`;CORS 可配;`docs/redoc/openapi` 关闭;监听地址改 env | **最需要人工合并的文件**。留意 router 注册块、lifespan、`__main__` |
|
||
| `api/routers/websocket.py` | 两个 WS 路由加 `dependencies=[Depends(require_ws_auth)]` | 上游若新增 WS 路由,**必须同样加上**,否则那条流是裸奔的 |
|
||
| `api/services/crawler_manager.py` | 解释器探测替换硬编码 `uv run`;`_build_command` 转发新增参数;新增 `is_busy()` / `run_and_wait()` 与完成事件 | 留意 `_build_command` 的参数拼装 |
|
||
| `media_platform/xhs/login.py` | `login_by_cookies` 在 `INJECT_ALL_COOKIES` 打开时注入**全部** cookie(默认关闭,行为不变) | 小改动,好合并 |
|
||
| `tools/user_hash.py` | `mask_nickname` 改为读 `config.MASK_NICKNAME`,本仓库默认**不脱敏**(原样返回)。上游作为教学版默认脱敏,但那是有损的 ——「张三」「张四」都成「张*」,而分清谁是谁正是监控这一层要干的活。脱敏实现本身没删,改回 `True` 即恢复上游行为 | 与 `config/base_config.py` 一起改,两处不同步会不一致 |
|
||
|
||
### 4. 上游 bug 修复(建议回馈上游)
|
||
|
||
| 文件 | 修的问题 |
|
||
|---|---|
|
||
| `media_platform/xhs/core.py` | 见下节 1 |
|
||
| `media_platform/xhs/login.py` | 见下节 2(cookie 加固) |
|
||
| `media_platform/douyin/core.py` | 见下节 3(首页 `goto` 永远超时,采集根本起不来) |
|
||
| `media_platform/douyin/login.py` | 见下节 4(注入 cookie 后页面陈旧,白等十分钟) |
|
||
|
||
---
|
||
|
||
## 二、应该给上游提 PR 的四个修复
|
||
|
||
这四处都是**上游自身的缺陷**,提上去以后就不用自己背着:
|
||
|
||
### 1. 博主主页抓取失败会跳掉整个博主(`xhs/core.py`)
|
||
|
||
`get_creator_info()` 抓主页 HTML 解析 `window.__INITIAL_STATE__`,解析失败抛 `JSONDecodeError`——
|
||
它是 `ValueError` 的子类,被 `except ValueError` 误捕获,日志报成
|
||
"Failed to parse creator URL"(**误导**,URL 根本没解析错),然后 `continue` **跳过整个博主**。
|
||
|
||
而那份资料只喂给 `save_creator()`,它在教学版里是**空函数**。也就是说:
|
||
一个喂给空函数的抓取失败,让真正要抓的作品一条都没抓到,表现为"0 篇作品",
|
||
和"登录失效"长得一模一样。
|
||
|
||
修复:把资料抓取改成**尽力而为**,失败只警告、继续抓作品。
|
||
|
||
### 2. cookie 登录只注入 `web_session`(`xhs/login.py`)
|
||
|
||
`a1` / `webId` 等签名所需 cookie 只能靠持久化 profile 补,冷启动时签名会失败。
|
||
默认行为保持不变,用 `INJECT_ALL_COOKIES` 开关控制。
|
||
|
||
### 3. 抖音首页的 `goto` 永远等不到 `load`(`douyin/core.py:101`)
|
||
|
||
```python
|
||
await self.context_page.goto(self.index_url) # 默认 wait_until="load"
|
||
```
|
||
|
||
抖音首页的 `load` 事件**不会触发**(有长连接/埋点类请求一直挂着)。实测:同一台
|
||
Chrome、同一个地址,`domcontentloaded` 0.7 秒返回,而 `load` 等满 90 秒仍然超时。
|
||
后果是整个采集**一步都没走就崩**,退出码 1 —— 看起来像"抖音不能用"。
|
||
|
||
修复:显式 `wait_until="domcontentloaded"`。上游的贴吧(`tieba/core.py`)和知乎
|
||
(`zhihu/core.py`)本来就是这么写的,抖音这个页面只是恰好属于"永远不 load"的那类。
|
||
|
||
> 这个缺陷在本机可能复现不出来(换个网络/有缓存时 `load` 也许能触发),所以社区里
|
||
> 没人报。它和网络快慢无关:不是"慢",是那个事件根本不会发生。
|
||
|
||
### 4. 注入 cookie 后页面是陈旧的(`douyin/login.py:266`)
|
||
|
||
`login_by_cookies()` 把 cookie 塞进 browser context,但**页面是在这之前加载的** ——
|
||
SPA 只在加载时读一次登录态,`localStorage.HasUserLogin` 于是还停在"未登录",
|
||
紧接着的 `check_login_state()` 会对着这个陈旧的值轮询到超时(600 次 × 1 秒 = 十分钟),
|
||
然后 `sys.exit()`。**下一轮**才正常,因为那时 cookie 已经在 profile 里了。
|
||
|
||
表现是"第一次跑白等十分钟、第二次才行",很容易被当成偶发。
|
||
|
||
修复:注入完 cookie 后 `reload(wait_until="domcontentloaded")`,让站点立刻重新判定会话。
|
||
|
||
> 与第 1 条同源:都是"页面状态是加载那一刻的快照"。本仓库的扫码登录(`api/monitor/qrlogin.py`)
|
||
> 和运营模块也各自踩过这个坑,那里的判据改成了拿 cookie 问后台接口,而不是读页面快照。
|
||
|
||
---
|
||
|
||
## 三、上游更新时怎么操作
|
||
|
||
### 先让机器替你盯着
|
||
|
||
「上游更新检查」(`api/monitor/upstream.py`,开关在 WebUI 的**系统设置 → 上游更新**)会按
|
||
间隔 `git fetch` 上游、算出落后几个提交,有更新就推企业微信。它是这份文档的自动化版:
|
||
没有它,「上游动了」这件事只取决于谁偶尔想起来去 fetch 一次。
|
||
|
||
两个细节决定了它为什么是安全的:它只 fetch 到 `FETCH_HEAD`,**不写工作区、不建 remote、不碰
|
||
`refs/remotes`**,所以和正在跑的采集、和下面的 `git pull` 都不冲突;默认**关闭**,因为要联网,
|
||
且需要镜像里有 git。
|
||
|
||
### 日常流程
|
||
|
||
```bash
|
||
git stash # 或先 commit 到自己的分支(推荐)
|
||
git fetch origin main
|
||
git rebase origin/main # 冲突只会出现在上表第 3、4 类文件里
|
||
./.venv/Scripts/python.exe -m pytest tests/ -q # 502 个测试就是回归网
|
||
```
|
||
|
||
### 直连 GitHub 不通时(本机常见)
|
||
|
||
本机到 `github.com` 时通时不通,**大包传输必断**(`Recv failure: Connection was reset`
|
||
或 `unexpected disconnect while reading sideband packet`),所以 `git clone` / `--unshallow`
|
||
这类一次性拉全量的操作基本必失败。可用的替代源:
|
||
|
||
```bash
|
||
# gitcode 的 GitHub 镜像,国内直连,比 GitHub 本身还新一天以内
|
||
git remote add gitcode https://gitcode.com/gh_mirrors/me/MediaCrawler.git
|
||
git fetch --no-tags --unshallow gitcode # 本仓库当初就是这样补全历史的,约 2 秒
|
||
```
|
||
|
||
注意 `git fetch` 只写 `refs/remotes/*`,**不会动本地 `main`**;
|
||
但拉镜像会把 `upstream/main` 指到镜像的 tip(可能比 GitHub 晚一天),
|
||
等 GitHub 通了再 `git fetch upstream` 正回来即可。
|
||
|
||
### 强烈建议:先把改动提交掉
|
||
|
||
当前状态是**未提交**的(25 个上游文件被改 + 31 个新文件)。在 `main` 分支上裸着工作区,
|
||
一次 `git checkout .` 就全没了,而且没法 rebase。
|
||
|
||
```bash
|
||
git checkout -b local/monitor-panel
|
||
git add -A && git commit -m "监控面板 / 鉴权 / 多平台"
|
||
```
|
||
|
||
### 如果改动持续增长:fork
|
||
|
||
把本仓库 fork 到自己名下,上游设为 remote:
|
||
|
||
```bash
|
||
git remote rename origin upstream
|
||
git remote add origin <你的 fork>
|
||
git push -u origin local/monitor-panel
|
||
```
|
||
|
||
之后同步上游用 `git fetch upstream && git rebase upstream/main`。
|
||
|
||
---
|
||
|
||
## 四、合并时最容易忘的三件事
|
||
|
||
1. **新增的 `/api` 路由必须带鉴权**。跑一下 `tests/test_auth.py`——
|
||
里面有个测试会遍历 `app.routes`,断言除豁免集外每个 `/api` 路由无凭据都返回 401。
|
||
上游新增接口忘了加鉴权,这个测试会直接失败。
|
||
2. **新增的 WebSocket 路由必须加 `require_ws_auth`**。
|
||
`BaseHTTPMiddleware` 对 WS 完全不生效(`scope["type"] != "http"` 直接放行),
|
||
只靠中间件会漏。同样有测试守着。
|
||
3. **上游若改动 `AsyncFileWriter` 的输出路径规则**,`api/monitor/ingest.py::find_run_files`
|
||
会跟着失效——它靠 glob `{out_dir}/{platform}/jsonl/*_contents_*.jsonl` 定位每轮的产物。
|