Files
MediaCrawler/UPSTREAM.md
T
butubb cd85587f00
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat(privacy): 关掉昵称脱敏 —— 脱敏有损,撞名就分不出博主
需求:评论栏/作品栏要能分清是哪个博主。修好分组字段之后名字仍带星号,因为上游作为
教学版默认对昵称做中间脱敏(首尾各留 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 处字面量的脱敏期望值换成真实昵称 —— 提取器现在就是
  返回原文的,期望值理应跟着改(这一条是行为变更的直接后果,不是测试放宽)。
* 新增一条:已入库的评论昵称会随重采刷新(且不会因刷新而重复插入)。
2026-10-10 15:48:15 +08:00

10 KiB
Raw Blame History

与上游的差异管理

本仓库在 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 篇作品", 和"登录失效"长得一模一样。

修复:把资料抓取改成尽力而为,失败只警告、继续抓作品。

a1 / webId 等签名所需 cookie 只能靠持久化 profile 补,冷启动时签名会失败。 默认行为保持不变,用 INJECT_ALL_COOKIES 开关控制。

3. 抖音首页的 goto 永远等不到 load(douyin/core.py:101)

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 也许能触发),所以社区里 没人报。它和网络快慢无关:不是"慢",是那个事件根本不会发生。

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。

日常流程

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 这类一次性拉全量的操作基本必失败。可用的替代源:

# 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。

git checkout -b local/monitor-panel
git add -A && git commit -m "监控面板 / 鉴权 / 多平台"

如果改动持续增长:fork

把本仓库 fork 到自己名下,上游设为 remote:

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 定位每轮的产物。