🌐 在线介绍页:https://whochatting.pages.dev/
本地部署的舆情分析工具链:多平台采集 → 本地清洗去重 → 情感/主题分析 → 看板 + 企微预警。 数据不出内网,零依赖即可跑通全链路,LLM 可选接入。
介绍链接:https://whochatting.pages.dev/
Python 3.10+ · 7 个平台 · 6 层链路 · 324 个测试 · 词典法情感 90.9%
# 0. 依赖(conda base 已含 torch/transformers/wordcloud/pandas/jieba,只需补这几个)
pip install sqlalchemy streamlit datasketch json-repair
pip install -e .
# 1. 环境自检 —— 一次把"什么能跑、什么没装"说清楚
python -m Whochat.cli init
# 2. 零依赖跑通全链路(不需要爬虫、不需要模型)
python -m Whochat.cli demo
# 3. 可视化(双入口:看板端 / 操作端)
python -m Whochat.cli dashboard # → http://localhost:8501
# / 看板端 —— 只读:趋势/情感/主题/词云/传播/预警记录(默认页)
# /console 操作端 —— 可写:L1~L5 触发与微调(采集/分析/规则/推送)
# 4. 回归测试(324 个用例,约 50 秒)
python -m pytest -qdemo 能跑通,说明整条业务链路是好的。之后逐步换成真实数据源。
demo 是幂等的:造数据用固定种子,连跑多次落库量不变、告警数不变 (第二次起
分析 0 条)。所以demo也可以当回归基线用 —— 数字变了就说明业务逻辑被改动了。
⚠️ 端口别选浏览器禁用端口(6665~6669等原 IRC 端口段在内)。 浏览器会在建连前直接拒绝,报"无法访问此页面";而curl/requests不检查这份清单,服务端一切正常 —— 于是 HTTP 200 会给人"页面没问题"的错觉。 默认端口是 8501,改端口用--port或WHOCHAT_DASHBOARD_PORT。
⚠️ 看板端在根路径/,不是/dashboard。Streamlit 规定默认页的url_path恒为空字符串,给它传url_path会被静默忽略(不报错)。 因此/dashboard是一条不存在的路由:访问它会先报 "Page not found", 再回退到默认页(恰好就是看板端),表现为"报错一下又刷新出来"。 注意curl /dashboard仍返回 200 —— SPA 任何路径都发同一个外壳, HTTP 200 证明不了路由存在。
# 采集(首次需扫码登录,浏览器会弹出)
python -m Whochat.cli crawl --platform xhs --keyword "你的品牌"
python -m Whochat.cli crawl --platform douyin --keyword "你的品牌"
# 或者导入已有的 JSONL/JSON/CSV
python -m Whochat.cli import path/to/comments.jsonl --platform xhs
# 分析 → 主题 → 词云
python -m Whochat.cli analyze
python -m Whochat.cli topics # 需要 bertopic
python -m Whochat.cli wordcloud
# 预警
python -m Whochat.cli alert --seed-rules
python -m Whochat.cli alert --since-hours 24 # 回放模式:补数/复盘用
# 看看数据
python -m Whochat.cli status支持平台:douyin xhs kuaishou bilibili weibo tieba zhihu
调度器不是"一次性采集":每个关键词是一条周期任务,跑完自动按
next_run_at 排下一次,失败按指数退避重试。这才是"长期监测 100 个关键词"
能成立的原因 —— 早期实现把任务置成 done 就再也不过问,每个词只会被采集一次。
# 一行一个关键词,支持 # 注释;可同时挂多个平台(每个平台各一套任务)
python -m Whochat.cli keywords add --platform xhs --platform douyin --file keywords.txt
python -m Whochat.cli keywords add --platform weibo --keyword "品牌A" --keyword "品牌B"
# 规模与健康度(任务数 / 启用 / 待跑 / 连续失败)
python -m Whochat.cli keywords status
python -m Whochat.cli keywords list --platform xhs
# 停用/启用/删除(停用不丢历史与归因)
python -m Whochat.cli keywords disable --task-id <id>
python -m Whochat.cli keywords enable --platform xhs
python -m Whochat.cli keywords rm --platform xhs --all --yes操作端 /console → L1 采集 → 关键词监控 有同样的可视化入口
(多行文本框 + 平台多选 + 间隔 + 启停/删除)。
python -m Whochat.scheduler.jobs # 常驻:快通道 1min / 采集+分析 30min / 主题 3:00 / 日报 9:00
python -m Whochat.scheduler.jobs --once crawl # 手动跑一轮(也可放进系统计划任务)每轮只处理到期的任务;同一平台串行、跨平台并行。
单个采集任务会拉起一次浏览器,耗时取决于 max_items。粗估:
一轮耗时 ≈ (词数 / 并发) × 单任务耗时 (同平台仍需串行)
默认 WHOCHAT_CRAWL_CONCURRENCY=1(最稳)。100 个词、单任务约 2 分钟时,
串行一轮约 3.3 小时;若把间隔设成 30 分钟就永远追不上,"待跑"会持续累积
(keywords status 会显式告警)。两条路:把间隔调到与吞吐匹配(如 612 小时),
或把并发提到 34(每个任务一个浏览器,注意内存与风控)。
| 项 | 说明 |
|---|---|
| 进程守护 | scheduler.jobs 是单进程,崩了不会自愈。用 Windows 任务计划程序 / NSSM / systemd / Docker restart 拉起它 |
| 文件日志 | 启动自动写 data/logs/Whochat.log,10MB × 10 份轮转(此前只打 stdout,进程一关就没) |
| 超时保护 | 单个采集子进程默认 30 分钟硬超时(WHOCHAT_CRAWL_TIMEOUT),卡死会被终止并读取已产出的部分数据 |
| 崩溃恢复 | 进程中断残留的 running 任务会自动回收重排,不会静默停采 |
| SQLite | 已开 WAL + busy_timeout,采集写与看板读不互相阻塞 |
| 数据保留 | python -m Whochat.cli maintenance --prune-snapshots-days 90 --prune-raw-days 180(默认试运行,--yes 才真删) |
⚠️ 采集后端由调度器自行注册。早前job_crawl直接调resolve_source, 而注册表在无人值守进程里是空的 —— 每个任务都报"未注册的采集后端", 长期采集其实从未真正跑起来。现在统一走crawler.source_for(platform)。
任何 OpenAI 兼容接口都能用。在 .env 里填这两行就生效,不需要额外装包
(直接走 HTTP 调,requests 本来就是核心依赖):
WHOCHAT_LLM_BASE_URL=https://api.deepseek.com/v1
WHOCHAT_LLM_API_KEY=sk-xxxxxxxxxxxxxxxx模型名可以留空 —— 会自动调 GET /models 挑一个对话模型(会跳过 embedding /
rerank / whisper 这类非对话模型),挑不出来会明确报错让你填 WHOCHAT_LLM_MODEL。
base_url 只写到域名也行(https://api.deepseek.com),会在
{base}/chat/completions 与 {base}/v1/chat/completions 之间自动探测一次并缓存。
配置完用 python -m Whochat.cli init 自检,应显示:
LLM 分析 : 就绪 · 模型 deepseek-chat(自动选择(候选 42 个))
| 做 | 不做 |
|---|---|
| 广告识别 —— 判意图而不是关键词("商家刷单太明显了"是在批评,不是广告;纯正则会误杀) | 情感判定 —— ADR#5。永远走词典法,实测比模型更准(90.9% vs 60.6%) |
主体识别 —— 这条在骂哪个产品/型号(subject 字段,规则完全做不到) |
声量统计 —— 必须是精确计数 |
| 关键词抽取 —— "这条在说什么",比 TF-IDF 高频词有用 | 快通道预警 —— 那条链路的设计前提就是"无模型、秒级" |
主题命名 —— 把 发热 / 续航 / 掉电 归纳成「屏幕发热与续航」 |
|
字段映射(import --llm-map)—— 字段名陌生的数据集,学一次就能导入 |
- 版本隔离:开启 LLM 后,分析结果写入
lexicon-v1-llm,与原有的lexicon-v1并存,不覆盖。事后能对比"用没用模型"的差异。 - demo 的幂等性只对未开启 LLM 时成立 —— 模型是概率性的。跑回归基线时 请关掉 LLM。
- 漏项不丢数据:模型少返回几条时,那几条会继续被正常分析,只是不带 LLM 标签。丢数据比少打一个标严重得多(有测试钉死)。
批量请求(默认一次 10 条)。1 万条评论约 1000 次请求。可用
WHOCHAT_LLM_BATCH 调批大小、WHOCHAT_LLM_RPM 限速、WHOCHAT_LLM_TIMEOUT 调超时。
跑完会打印 token 用量。
⚠️ 访问境外 API(OpenAI 等)需要代理:设HTTPS_PROXY=http://127.0.0.1:7897。 采集流量与它是分开的,不会互相影响(见 §2.3 的代理分工)。
不想改文件的话,打开操作端 http://127.0.0.1:8501/console → L2 清洗 →
「LLM 分析」,有三个输入框(地址 / 密钥 / 模型名),填完点「保存并测试连接」即可。
配置会写进 .env(已在 .gitignore 里),并立即生效,不用重启看板。
密钥框是 password 类型且永不回显,已保存时显示成
sk-a***wxyz的掩码。
⚠️ 本机想跑本地模型的话:RTX 3060 Laptop 只有 6GB 显存, Qwen2.5-14B q4 约 9GB 装不下。要用 7B q4(~4.7GB,勉强)或 3B/4B。 本地 Ollama 也走同一套:WHOCHAT_LLM_BASE_URL=http://127.0.0.1:11434/v1, key 随便填(它不校验)。
L1 采集 MediaCrawler(Playwright) · MockSource · ManualImport
↓ CrawlerSource 协议 ← 唯一的解耦点
L2 清洗 规则清洗 → 精确去重 → MinHash/SimHash 近重复 → LLM 打标(可选)
↓
┌──────┴──────┐
↓ 快通道 ↓ 慢通道
L3 分析 规则引擎 情感分析 · BERTopic · 词云 · 时序
(秒级,无模型) (分钟~小时)
└──────┬──────┘
↓
L4 存储 Repository 统一读写接口 ← 事实上的"中台"边界
↓
┌──────┴──────┐
L5 预警 聚合·分级·冷却 L6 看板 FastAPI/Streamlit :8501
→ 企微机器人
| 模块 | 位置 | 说明 |
|---|---|---|
| 采集协议 | crawler/base.py |
CrawlerSource 协议 + 时间/ID 归一化工具 |
| MediaCrawler 适配 | crawler/mediacrawler_source.py |
子进程调用,读 JSONL 归一化 |
| 字段归一化 | crawler/normalize.py |
别名表 + 多候选回退,含 "1.2万" → 12000 解析 |
| 规则清洗 | pipeline/rules.py |
清洗、广告识别、分词、关键词抽取 |
| 去重 | pipeline/dedup.py |
精确(SHA256) + 近重复(MinHash/SimHash 分桶) |
| LLM 客户端 | pipeline/llm_client.py |
任意 OpenAI 兼容接口(只填 base_url + api_key);端点/模型自动探测、重试退避、JSON Mode 降级、用量统计 |
| LLM 打标 | pipeline/llm_clean.py |
广告识别(看意图不只看关键词)/ 主体识别 / 关键词抽取;批量 + 序号对齐,漏项不丢数据 |
| LLM 字段映射 | crawler/llm_map.py |
字段名陌生的平台:学一次「原始字段 → schema」并落盘缓存,泛化别名表覆盖不到的情况 |
| 情感分析 | analysis/sentiment.py |
词典法(子串扫描+否定+程度+转折) / Transformer 双后端 |
| 主题建模 | analysis/topics.py |
BERTopic(中文嵌入模型降级链)+ 零下载离线方案兜底 |
| 时序 | analysis/timeseries.py |
爆发检测、阶段划分、情感漂移、KOL |
| 传播分析 | analysis/propagation.py |
传播曲线(metric_snapshots → 起爆点/峰值/增速拐点)+ 传播路径(parent_content_id → 转发链 DOT 图) |
| 存储 | store/repository.py |
统一读写接口,上层不碰 SQL |
| 预警规则 | alert/rules_engine.py |
快通道,支持 since/until 回放;风险词与情绪词双线 |
| 企微推送 | alert/notifier.py |
窗口聚合 + 分级路由 + 冷却 + 限流 + dry-run;失败保持 pending 自动重试(不丢警) |
自建标注集(tests/annotated_sample.json,33 条):
python -m Whochat.cli evaluate tests/annotated_sample.json词典后端准确率 90.9%(对比:子串扫描改造前是 69.7%)。
剩余错误集中在两类:
- 反讽("这手机真好用,用了三天就送修了")
- 词典语境("问题当天就解决了" — "问题"是中性语境)
另外两条已知短板已写成 xfail 用例(pytest -q 会显示为 2 xfailed),
修好后会自动变成 XPASS 提醒:
"这也叫好用?垃圾死了"→ 判 neutral(负面词被正面词抵消后落在中性带内)"质量真稳定,稳定地坏"→ 判 neutral(程度副词"真"把"稳定"放大后压过"坏")
实测(同一 33 条标注集)把 WHOCHAT_SENTIMENT_BACKEND 切成 transformer:
| 后端 | 准确率 | 说明 |
|---|---|---|
lexicon |
90.9% | 默认。零依赖、毫秒级、可复现 |
transformer |
60.6% | IDEA-CCNL/Erlangshen-Roberta-110M-Sentiment |
原因是模型选错了,不是代码问题:Erlangshen 这款是二分类(正/负)模型, 没有"中性"这一档,而本链路(schema / 看板 / 预警)是完整三分类。 实测它的分数分布:
negative [-0.998, +0.603]
neutral [-0.824, +0.997] ← 铺满整个区间
positive [+0.928, +0.999]
中性样本(+0.97 ~ +1.0)和正面样本(+0.93 ~ +1.0)完全重叠 ——
不存在任何一个能把它们分开的中性带,所以调 neutral_band 是徒劳的
(拉宽会把正确的正面一起吞掉)。
代码现在会在加载二分类模型时打印警告。结论:在拿到三分类中文情感模型、
或用业务数据微调之前,保持 lexicon。 这也正是方案文档 §4.1 Phase 3
把"微调情感模型"列为长期任务的原因。
⚠️ 33 条样本量很小,这个数字证明的是链路可用,不是准确率有 90.9%。 请务必用自己业务的数据建 300~500 条标注集重测 —— 没有基线, 后面所有的情感统计、预警阈值、主题分析都建立在流沙上。
这些是踩过坑之后定下来的,改之前建议先看 方案文档 §13。
| # | 决策 | 为什么 |
|---|---|---|
| 1 | 采集用 MediaCrawler,不自己写 | 抖音 a_bogus 是 JSVMP 混淆+环境检测+数月一升级;浏览器自动化零逆向且不随加密更新失效 |
| 2 | 国内平台采集不走代理 | 代理 IP 特征反而触发风控;出口地域跳变与登录态冲突。7897 只用于下载 |
| 3 | 只取评论区文本 | 评论区是情绪最集中处,信噪比高于视频正文 |
| 4 | 采集层抓全字段 | 采集不可逆,分析可重跑。parent_content_id/follower_count 事后补不回来(内容已删) |
| 5 | LLM 只做打标,不判情感 | 情感是封闭分类任务,词典法更快更准更可复现。用 LLM 逐条判是拿大炮打蚊子(实测:transformer 60.6% vs 词典 90.9%) |
| 6 | 预警走快慢双通道 | 等模型清洗完再告警,时效性已丧失(行业标准 30 秒~分钟级) |
| 7 | 不建真"数据中台" | 单人本地工具,过度工程化是最大死因。但要保留存储/展示边界 |
| 8 | 编排用 APScheduler,不用 Airflow | Windows 本机开发,Airflow 需 WSL/Docker 且过重 |
| 9 | MVP 用 Streamlit,二期换 FastAPI | 抢时间验证,再演进 |
| 10 | 看板措辞限定"讨论区" | 评论区样本天然偏向极端情绪,不可外推为"公众意见" |
| 限制 | 说明 |
|---|---|
| MediaCrawler 是 NON-COMMERCIAL 许可 | 明确禁止商业用途。要商业交付必须实现 OfficialAPISource 替换 mediacrawler_source.py —— 这正是协议存在的意义 |
| 采集成功率 70~90% | 403/滑块/登录态失效是常态。这是健康水平,不是 bug |
| 反讽识别 | 规则法基本无解,需模型或上下文 |
| 评论区样本偏斜 | 只代表"愿意评论的人",天然偏极端情绪 |
| 主题建模需 ≥100 文档 | 小样本 BERTopic 会退化成一堆碎片主题。文档数 <20 时直接拒绝并说明原因 |
| HuggingFace 可达性不稳定 | 嵌入模型下载会失败。此时自动降级到 TF-IDF+SVD+HDBSCAN 离线方案(零下载),效果弱于 BERTopic 但功能不报废。联网后重跑即可提升。现状:text2vec-base-chinese 已缓存到本地,在线 BERTopic 路径已验证可用(断网时仍自动回退离线方案) |
| 传播分析依赖采集字段 | 没抓 parent_content_id 就做不了,且事后补不回来 |
| LLM 输出是概率性的 | 同一条文本两次调用可能给不同标签。所以它只做打标(可容忍抖动),不参与情感判定与声量统计 |
| LLM 会自动挑模型 | 挑的是"符合偏好表的第一个对话模型",不代表最适合你。不确定就显式填 WHOCHAT_LLM_MODEL |
| LLM 打标结果未在真实数据上评估过 | 现有测试用的是本地假服务,验证的是契约(不漏项、不错位、不丢数据),不是准确率。广告识别比正则好在哪、差在哪,需要你自己的标注集来测 |
| 字段映射可能学错 | 已有防护(只认 schema 内字段、别名表优先、一个目标只认一次),但学错仍会往库里写错数据。缓存文件在 data/llm_field_map.json,可直接查看/删除重学 |
| 免费代理池不好用 | 可用率低、生命周期短。真要规模化得买国内住宅代理 |
src/Whochat/
├── config.py 配置(代理/采集/模型/情感/预警/存储)
├── cli.py 命令行入口
├── console.py 控制台编码兼容(GBK 下 emoji 降级不崩)
├── logging_setup.py 长期运行日志落盘 + 轮转(stdout Tee 到文件)
├── crawler/ L1 采集
├── pipeline/ L2 清洗
├── analysis/ L3 分析
├── store/ L4 存储
├── alert/ L5 预警
├── web/ L6 可视化(双入口)
│ ├── app.py 入口/导航:看板端 + 操作端
│ ├── dashboard.py 看板端(只读)
│ └── console.py 操作端(L1~L5 控制与微调)
└── scheduler/ L0 编排(APScheduler)
dicts/ 停用词 / 敏感词 / 情感词 / jieba 自定义词典
data/ SQLite、原始 JSONL、导出物、LLM 字段映射缓存
vendor/MediaCrawler/ 采集基座(git clone,未修改)
tests/ pytest 用例(324 个)+ 情感标注集
词典分工(dicts/):
| 文件 | 作用 | 改它会怎样 |
|---|---|---|
user_dict.txt |
jieba 分词自定义词典 | 影响词云/主题的词切分,不影响情感 |
stopwords.txt |
分词停用词(追加到内置表) | 影响词云/主题,不影响情感 |
positive_words.txt |
正面情感词(追加到内置表) | 直接改情感判定,改完务必重跑 evaluate |
negative_words.txt |
负面情绪词 | 影响情感判定 + 预警"情绪激增" |
sensitive_words.txt |
风险词(监管/法律/安全/隐私) | 影响预警"敏感词命中"规则 |
情绪词(失望/垃圾)衡量"骂得多凶";风险词(起诉/12315/召回)衡量"事情闹多大"。 一条冷静的"已向12315投诉并准备起诉"情感分很低,但风险等级最高 —— 两者不能混。
- Python 3.10+(实测 3.13)
- 系统代理
7897(Clash 混合端口)仅用于下载:GitHub / HuggingFace / pip / Ollama - 采集直连,不走代理
cp .env.example .env # 按需修改它不在本仓库里,两个原因:
- 它是第三方项目且是 NON-COMMERCIAL 许可,不适合随本仓库一起分发;
- 它自己就是个完整项目,体积不小。
所以 pip install -e . 之后,demo / import / 分析 / 看板 / 预警都能正常跑
(它们不依赖采集器)。只有真实采集需要先补上它:
git clone https://github.com/NanmiCoder/MediaCrawler.git vendor/MediaCrawler
# 再按它的 README 装依赖(Playwright + 浏览器),必要时在 .env 里指定解释器:
# WHOCHAT_MC_PYTHON=C:\path\to\mediacrawler\venv\Scripts\python.exe本仓库的代码对 MediaCrawler 只做子进程调用 + 读它产出的 JSONL, 没有修改它的源码。想验证整条链路而不装爬虫,直接用
python -m Whochat.cli demo。
仓库、Python 包、命令、环境变量前缀统一是 Whochat(此前内部包名误拼成
wochat,少了一个 h,已全量更正):
python -m Whochat.cli demo改名影响三处:包目录
src/Whochat/、命令python -m Whochat.cli ...、 环境变量前缀WHOCHAT_*(旧的.env需同步改名)。本地默认库文件也从data/db/wochat.db变为data/db/Whochat.db。