项目官网: https://anti-spam.zhongxiaojie.cn
作者: obaby,博客署名 baby 𝐢𝐧⃝ void
面向 中英混合 评论的 WordPress 垃圾识别方案:PHP 插件在评论入库前调用 本机 Python 服务,由小型多语种向量模型 + 分类器(或演示用规则)给出垃圾概率。
适合评论量不大、单机部署(例如 4 核 / 8GB RAM 的 Ubuntu),服务与 WordPress 同机时使用 127.0.0.1 即可。
baby_anti_spam/ ├── README.md ├── screenshots/ # 文档:服务启动与 curl 自测示意 │ ├── service.png │ └── test.png ├── service/ # Python FastAPI 侧车服务 │ ├── .env.example │ ├── requirements.txt │ ├── requirements-ml.txt │ ├── run.py │ ├── app/ │ │ └── stats_backends/ # 统计存储:sqlite / mysql │ └── scripts/ │ ├── init_stats_mysql.sql │ └── init_stats_mysql.py │ ├── train_sklearn.py │ ├── download_embedding_model.py │ └── download_embedding_model.sh └── wordpress/baby-anti-spam/ └── baby-anti-spam.php # WordPress 插件
service/requirements.txt,多语种向量见 service/requirements-ml.txt(含 PyTorch 等,单独安装可规避部分 pip 解析 bug)*.joblib 与 scikit-learn 版本需一致:训练机与服务机的 sklearn 应同一大版本(本仓库约束 >=1.8)。混用 1.7 与 1.8 易出现反序列化警告或推理报错(例如 LogisticRegression 缺少 multi_class);请在服务器上 pip install -U "scikit-learn>=1.8" 与训练环境对齐,或改用与线上一致的 sklearn 重新训练后再部署。务必先升级 pip,再继续(可减少 AssertionError: assert len(weights) == expected_node_count 一类错误):
cd service
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install -U "pip>=24.2" setuptools wheel
pip install -r requirements.txt
pip install -r requirements-ml.txt # 若仅开演示规则、不用 .joblib 模型,可跳过本行
cp .env.example .env
# 编辑 .env:至少配置 SPAM_API_SECRET,或使用 SPAM_API_KEYS / SPAM_API_KEYS_FILE(见下文)
python run.py
默认监听:http://127.0.0.1:8765(由 SPAM_HOST / SPAM_PORT 决定,run.py 可用 --port 覆盖端口)。
run.py 本机 / 对外开放(命令行覆盖 SPAM_HOST):
| 参数 | 说明 |
|---|---|
| (默认) | 使用 .env 中的 SPAM_HOST,未设置时为 127.0.0.1 |
--local | 强制只监听 127.0.0.1 |
--remote | 监听 0.0.0.0(供局域网 / 公网访问,务必配合防火墙与强密钥) |
--port N | 覆盖端口(否则用 SPAM_PORT 或默认 8765) |
启动时会在终端打印作者与博客信息、监听地址、模型与统计库等摘要(便于确认当前配置)。
若出现 get_topological_weights / assert len(weights) == expected_node_count(常见于 未先升级 pip 就装 requirements-ml.txt):
python -m pip install -U "pip>=24.2" setuptools wheelpip install -r requirements-ml.txtrequirements.txt,再 requirements-ml.txt,不要合并成一条长命令。PIP_USE_DEPRECATED=legacy-resolver pip install -r requirements-ml.txt$env:PIP_USE_DEPRECATED = "legacy-resolver" pip install -r requirements-ml.txt
pip install torch --index-url https://download.pytorch.org/whl/cpupip install -r requirements-ml.txt| 变量 | 说明 |
|---|---|
SPAM_HOST | 监听地址,同机建议 127.0.0.1 |
SPAM_PORT | 端口,默认 8765 |
SPAM_API_SECRET | 单密钥模式(兼容旧版):未配置 SPAM_API_KEYS 且未配置 SPAM_API_KEYS_FILE 时,仅此密钥有效,等价于 name=default、不限流(max_rpm=0)。与 WP 插件里填写的密钥一致 |
SPAM_API_KEYS | 多密钥:JSON 数组。每项为 name(唯一,用于统计与限流分组)、key 或 secret(与请求头一致)、max_rpm 或 rpm(每分钟最大请求数,0 表示不限制)。与 SPAM_API_KEYS_FILE 合并时:先读文件条目,再追加本变量 |
SPAM_API_KEYS_FILE | 可选,指向 JSON 文件,根节点为与上表相同结构的数组。文件必须存在,否则进程启动失败 |
SPAM_MODEL_PATH | 训练得到的 *.joblib 路径;留空则取决于 SPAM_FALLBACK_RULES |
SPAM_FALLBACK_RULES | 无模型文件时是否启用内置极简规则(演示用);生产训练后应设为 false 并配置 SPAM_MODEL_PATH |
SPAM_LABEL_THRESHOLD | 可选,默认 0.8。spam_score ≥ 此值时 JSON 中 label 为 spam,否则为 normal |
SPAM_DFA_ENABLED | 默认 true。为 true 时使用 dfa-python-filter/keywords 做敏感词检测;命中则直接 spam_score=1、detail=dfa_sensitive(早于 sklearn) |
SPAM_DFA_KEYWORDS_PATH | 可选,自定义敏感词文件路径;留空则用 service/dfa-python-filter/keywords |
SPAM_NON_CHINESE_FLOOR_ENABLED | 默认 true。为 true 时若合并后的 author/email/url/text 中无任何 CJK 表意字符(主要针对中文训练语料),则将 spam_score 至少抬到 SPAM_NON_CHINESE_SPAM_FLOOR |
SPAM_NON_CHINESE_SPAM_FLOOR | 默认 0.9。与上项配合,在「无中文」评论上与 sklearn / 规则分取 max |
SPAM_STATS_ENABLED | 默认 true。为 true 时记录每次成功返回的 /v1/classify 请求与响应(失败 / 401 不落库),并允许 /v1/mark-spam 写入 spam_marks 表 |
SPAM_STATS_BACKEND | sqlite(默认)或 mysql。选 mysql 时需安装 pymysql(已在 requirements.txt)并配置下方 MySQL 变量 |
SPAM_STATS_DB_PATH | 仅 sqlite:数据库文件路径;留空则为 service/data/stats.sqlite(已加入 .gitignore) |
SPAM_STATS_MYSQL_HOST / SPAM_STATS_MYSQL_PORT | 仅 mysql:默认 127.0.0.1 / 3306 |
SPAM_STATS_MYSQL_USER / SPAM_STATS_MYSQL_PASSWORD | 仅 mysql:连接账号(user 必填) |
SPAM_STATS_MYSQL_DATABASE | 仅 mysql:库名(必填),默认示例 baby_spam_stats |
SPAM_STATS_MYSQL_CHARSET | 仅 mysql:默认 utf8mb4 |
将统计后端从默认 SQLite 迁到 MySQL 8+ 或 MariaDB 10.5+ 时,按下面顺序配置即可(脚本与表结构与运行时一致,见 service/scripts/init_stats_mysql.sql、service/scripts/init_stats_mysql.py)。
在 service 目录准备 .env
在已有 SPAM_API_SECRET(或 SPAM_API_KEYS)基础上增加例如:
SPAM_STATS_ENABLED=true SPAM_STATS_BACKEND=mysql SPAM_STATS_MYSQL_HOST=127.0.0.1 SPAM_STATS_MYSQL_PORT=3306 SPAM_STATS_MYSQL_USER=baby_spam SPAM_STATS_MYSQL_PASSWORD=请改为强密码 SPAM_STATS_MYSQL_DATABASE=baby_spam_stats SPAM_STATS_MYSQL_CHARSET=utf8mb4
SPAM_STATS_MYSQL_USER 与 SPAM_STATS_MYSQL_DATABASE 在使用 MySQL 后端时必填;库名可与示例不同,但须与后续建库一致。
安装客户端库
requirements.txt 已包含 pymysql,正常执行 pip install -r requirements.txt 即可。
创建数据库与表(任选其一;建议生产由 DBA 或脚本预置)
Python 初始化脚本(推荐):在已激活 venv、service 为当前目录、且上一步 MySQL 变量已写入 .env 后执行:
cd service
python scripts/init_stats_mysql.py
该账号需能首次执行 CREATE DATABASE IF NOT EXISTS(或你事先建好库则仅需对该库有建表权限);脚本会创建库(若不存在)并调用与应用相同的 CREATE TABLE / 索引逻辑。不要求事先把 SPAM_STATS_BACKEND 设为 mysql,脚本只读取 SPAM_STATS_MYSQL_*。
手工执行 SQL:用高权限账号执行 init_stats_mysql.sql(与 service/scripts/init_stats_mysql.py 建库建表语义一致),例如当前目录为仓库根时:
mysql -h 127.0.0.1 -P 3306 -u root -p < service/scripts/init_stats_mysql.sql
若已在 service 目录下,则改为 < scripts/init_stats_mysql.sql。
脚本内含有注释掉的「创建专用用户 + GRANT」示例,可按环境取消注释并替换密码后再执行,再让应用使用受限账号连接。
仅依赖服务启动:须先人工创建好目标库(应用连接时使用 database=…,不会在运行时 CREATE DATABASE)。对该库授予 CREATE TABLE、CREATE INDEX、ALTER(用于旧表补列)等权限后,设置 SPAM_STATS_BACKEND=mysql 并启动服务, lifespan 中的 init_stats_db() 会创建 classify_calls、spam_marks 及索引。
启动并自检
运行 python run.py,终端不应出现统计库初始化失败日志;可用带 X-Baby-Anti-Spam-Secret 的 GET /v1/stats/calls 验证,或在库中查询上述两张表是否有新写入。
多密钥示例(单行 JSON,注意引号转义):
SPAM_API_KEYS=[{"name":"blog_a","key":"第一个长随机串","max_rpm":120},{"name":"blog_b","secret":"第二个密钥","rpm":30}]
WordPress 只需填写当前站点使用的那一个 key/secret,与服务端列表中任一项匹配即可。
限流:对每个 name 独立计数,60 秒滑动窗口;超限返回 429,detail 为 rate_limit_exceeded。/v1/stats/calls、/v1/classify、/v1/mark-spam 共用同一组密钥,且都会计入该密钥的 max_rpm。
/health — 健康检查,无需认证/ — 粉色风格首页:系统简介、随机背景图(zhongxiaojie.cn/baby_images/)、链向测试页与 /docs/test/spam — 浏览器内粉色测试页(无需单独密钥即可打开页面;检测时请在页内填写与后端一致的 API 密钥)。同源访问可避免 CORS,例如 http://127.0.0.1:8765/test/spam/v1/stats/calls — 调用统计(需密钥)。返回全库汇总次数、是否启用统计,以及按密钥 name 分组的已落库次数(by_key)。仅统计已成功写入统计库 classify_calls 的分类请求/v1/mark-spam — 人工标记评论(需密钥)。text 必填;label:spam(标记为垃圾,默认)或 normal(标记为不是垃圾)。其余字段与分类对齐;可选 source、note。写入表 spam_marks 的 marked_label 列,并记录 api_key_name。SPAM_STATS_ENABLED=false 时返回 503 stats_disabled/v1/classify — 分类,需在请求头携带 X-Baby-Anti-Spam-Secret: <与任一已配置密钥相同>请求体(JSON):
{
"text": "评论正文",
"author": "昵称",
"email": "邮箱",
"url": "网址"
}
响应示例:
{
"spam_score": 0.12,
"label": "normal",
"detail": "sklearn"
}
label 为二分类:normal 或 spam(由 SPAM_LABEL_THRESHOLD 与 spam_score 决定)。WordPress 插件可用 spam_score + 阈值,也可按 label 分项阈值 再判垃圾 / 待审 / 放行。
统计接口响应示例(GET /v1/stats/calls):
{
"call_count": 42,
"stats_enabled": true,
"by_key": { "default": 30, "blog_a": 12 }
}
在已配置 SPAM_MODEL_PATH 时,返回的 spam_score 为 sklearn 概率与内置规则分的较大值(常见英文 SEO、click here+链接等会抬高分数),detail 可能为 sklearn+rules(...);纯模型主导时仍为 sklearn。
敏感词检测(dfa_sensitive)在匹配前会 去掉 http(s)://… 与 www.… 整段 URL,避免长链接里的 Base64/参数偶然拼出词表短串(例如 wyd)误命中。
若启用 SPAM_NON_CHINESE_FLOOR_ENABLED,且整条评论(含昵称、邮箱、网址、正文)里检测不到 CJK 表意字符,最终分数会与 SPAM_NON_CHINESE_SPAM_FLOOR(默认 0.9)取较大值,detail 中会出现 no_cjk_floor。
将下面地址、端口与密钥换成你 .env 里的 SPAM_HOST / SPAM_PORT / 当前配置的 SPAM_API_SECRET 或 SPAM_API_KEYS 中任一的 key。
健康检查(无需密钥):
curl -sS "http://127.0.0.1:8765/health"
正常中文评论(期望 spam_score 偏低):
curl -sS -X POST "http://127.0.0.1:8765/v1/classify" \
-H "Content-Type: application/json" \
-H "X-Baby-Anti-Spam-Secret: change-me-long-random" \
-d '{"text":"写得很清楚,感谢分享!","author":"读者","email":"","url":""}'
中英混合、含链接或推广话术(演示规则或模型下通常分数更高):
curl -sS -X POST "http://127.0.0.1:8765/v1/classify" \
-H "Content-Type: application/json" \
-H "X-Baby-Anti-Spam-Secret: change-me-long-random" \
-d '{"text":"SEO optimization click here https://example.com/junk","author":"Agent","email":"spammer@example.com","url":"https://example.com"}'
错误密钥(响应头里应为 401 Unauthorized):
curl -sS -i -X POST "http://127.0.0.1:8765/v1/classify" \
-H "Content-Type: application/json" \
-H "X-Baby-Anti-Spam-Secret: wrong-secret" \
-d '{"text":"test","author":"","email":"","url":""}'
调用次数(需与上面相同的有效密钥头):
curl -sS "http://127.0.0.1:8765/v1/stats/calls" \
-H "X-Baby-Anti-Spam-Secret: change-me-long-random"
标记垃圾 / 标记正常(写入 spam_marks,marked_label 与 label 一致,api_key_name 为当前密钥的 name):
# 标记为垃圾(label 可省略,默认 spam)
curl -sS -X POST "http://127.0.0.1:8765/v1/mark-spam" \
-H "Content-Type: application/json" \
-H "X-Baby-Anti-Spam-Secret: change-me-long-random" \
-d '{"text":"明显广告","author":"spammer","source":"manual","note":"后台标记"}'
# 标记为不是垃圾
curl -sS -X POST "http://127.0.0.1:8765/v1/mark-spam" \
-H "Content-Type: application/json" \
-H "X-Baby-Anti-Spam-Secret: change-me-long-random" \
-d '{"text":"认真讨论","label":"normal","source":"manual","note":"误杀纠正"}'
服务已启动(本机 python run.py / Uvicorn 监听示例):

curl 自测分类接口(/v1/classify 请求与 JSON 响应示例):

本项目里实际上是两件东西,不要混在一起:
句向量模型(Hugging Face 上的 sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2)
官方模型页(境外,若超时可用下方镜像链接):
https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
国内常用镜像(同一条 repo_id,仅域名不同,便于浏览说明页):
https://hf-mirror.com/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
拉到指定目录的脚本(便于离线拷贝;仅需 pip install huggingface_hub,不必先有 PyTorch):
cd service
pip install huggingface_hub
# 默认走 huggingface.co;若超时请加 --mirror 或先 export HF_ENDPOINT=https://hf-mirror.com
python scripts/download_embedding_model.py --mirror --out-dir models/paraphrase-multilingual-MiniLM-L12-v2
# 或: bash scripts/download_embedding_model.sh models/paraphrase-multilingual-MiniLM-L12-v2
huggingface.co 拉取仍失败时,可在一台能访问外网的机器上下载整个 models/paraphrase-multilingual-MiniLM-L12-v2 目录,再打包拷贝到服务器(repo_id 不变,仍是同一模型)。
训练时改为指向该目录(同一 repo 内相对路径即可):
python scripts/train_sklearn.py --data /path/to/comments.csv --out models/spam_pipeline.joblib \ --model-name models/paraphrase-multilingual-MiniLM-L12-v2
默认行为(不写脚本): 在已安装 sentence-transformers 的前提下,下面任一操作会自动从 Hub 下载到缓存(~/.cache/huggingface/,约几百 MB):
scripts/train_sklearn.py 训练;或SPAM_MODEL_PATH 指向训练好的 *.joblib,第一次对评论做推理、管线里要加载 SentenceTransformer 时。只想预下载向量模型到默认缓存(可选): 在 service 的 venv 里执行:
Linux / macOS(bash):
export HF_ENDPOINT=https://hf-mirror.com # 可选,再执行下行
python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')"
Windows PowerShell(当前会话生效):
$env:HF_ENDPOINT = "https://hf-mirror.com" # 可选 python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')"
Windows CMD:
set HF_ENDPOINT=https://hf-mirror.com python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')"
缓存默认在 ~/.cache/huggingface/(也可用环境变量 HF_HOME 指定目录;无网机器可把缓存目录整体拷过去)。
huggingface_hub / sentence-transformers 走镜像下载时,一般设置:export HF_ENDPOINT=https://hf-mirror.com(与 hf-mirror 文档一致)。Windows 用 PowerShell 的 $env:HF_ENDPOINT = "https://hf-mirror.com",或 CMD 的 set HF_ENDPOINT=https://hf-mirror.com。其它第三方镜像若有提供等价 Hub API 地址,可用下载脚本的 --endpoint URL。
「是不是垃圾」的分类头(spam / normal)
spam_pipeline.joblib(train_sklearn.py + 你的 CSV)。本仓库不提供一份「官方预训练 joblib」供下载。spam、sms spam 等),但它们多半是 英文、任务定义(标签含义)和 推理框架 与当前服务不一致;不能把某个 .bin / safetensors 直接填进 SPAM_MODEL_PATH——当前服务只认 joblib.dump 的 sklearn 流水线格式。joblib;或自行 fork 后在服务里增加对 Transformers pipeline("text-classification") 的调用路径,再指定 Hub 上的模型 ID(需自行评估语种、误杀率和许可协议)。只用内置演示规则时(detail 为 fallback_rules)不会去下载上述向量模型。
准备 CSV,两列:text、label,其中 label 为 normal 或 spam(旧数据里 ham 在训练脚本中会映射为 normal):
text,label 这是一条正常评论,normal 点击领取红包...,spam
方式 A(推荐):本站已通过评论 + fixed 仅垃圾
sources/private/ 下 所有 *.csv(WordPress 导出格式:comment_approved=1 且 comment_type=comment)都会并入;例如 mars_comments.csv、zhong.wp_comments.csv。正文会做简单去 HTML。该目录已在 .gitignore 中忽略,请自行放入导出文件。sources/fixed/data/spam.txt(一行一条)。fixed 下的 normal 数据不参与训练。 若语料里游戏公会 / COC「几本」类内容过多,可在 service 下执行 python scripts/filter_spam_remove_game_lines.py(会先备份 spam.txt.bak 再剔除匹配行;--dry-run 仅统计)。python scripts/filter_spam_obvious_only.py(备份 spam.txt.bak_obvious;--dry-run 仅统计)。cd service
source .venv/bin/activate
python scripts/sources_to_csv.py \
--spam-file sources/fixed/data/spam.txt \
--private-dir sources/private \
--out data/comments.csv
还可追加单个导出文件:--normal-wp-export path/to/extra.csv(可多次写)。
方式 B:两个纯文本整文件(一行一条;会用到 fixed 的 normal 时选此项):
python scripts/sources_to_csv.py \ --normal-file sources/fixed/data/normal.txt \ --spam-file sources/fixed/data/spam.txt \ --out data/comments.csv
方式 C:目录 sources/normal/*.txt 与 sources/spam/*.txt:
python scripts/sources_to_csv.py --out data/comments.csv
--sources /path/to/root 指定含 normal/、spam/ 的根路径。--dedupe。--dedupe 或裁切子集。Windows PowerShell(方式 A):
python scripts/sources_to_csv.py ` --spam-file sources/fixed/data/spam.txt ` --private-dir sources/private ` --out data/comments.csv
训练(默认会从 Hub 拉取 paraphrase-multilingual-MiniLM-L12-v2,约几百 MB;若已下载或拷贝到本地目录,请加 --model-name 指向该目录):
cd service
source .venv/bin/activate
python scripts/train_sklearn.py --data data/comments.csv --out models/spam_pipeline.joblib
# 已本地化向量模型时,例如:
# python scripts/train_sklearn.py --data data/comments.csv --out models/spam_pipeline.joblib \
# --model-name models/paraphrase-multilingual-MiniLM-L12-v2
python scripts\train_sklearn.py --data data\comments_blog_fixed_spam.csv --out models\spam_obaby_pipeline.joblib
Windows(--data 换成你的 CSV 绝对路径或 .\data\comments.csv):
cd service .\.venv\Scripts\Activate.ps1 python scripts/train_sklearn.py --data .\data\comments.csv --out models\spam_pipeline.joblib # 若需本地向量模型: 再加 --model-name models\paraphrase-multilingual-MiniLM-L12-v2
在 .env 中设置:
SPAM_MODEL_PATH=/绝对路径/models/spam_pipeline.joblib SPAM_FALLBACK_RULES=false
重启 Python 服务后生效。
若加载模型时出现 InconsistentVersionWarning( pickle 时 sklearn 比运行环境新)或推理时报 LogisticRegression/multi_class 相关错误:在服务的 venv 内执行 pip install -U "scikit-learn>=1.8"(与 requirements.txt 一致),重启服务;不要在低版本 sklearn 上加载高版本导出的 joblib。
wordpress/baby-anti-spam 复制到 wp-content/plugins/baby-anti-spam/,或打包为 zip 在后台上传安装。http://127.0.0.1:8765SPAM_API_SECRET(单密钥模式)或 SPAM_API_KEYS 中该站点对应条目的 key 一致spam_score,或按 API 的 normal / spam 分项阈值再结合分数normal / spam 两档分别设置)插件使用过滤器 pre_comment_approved:spam_score 达垃圾线则 spam,达待审线则 hold,否则保持原状态。
127.0.0.1);若使用 run.py --remote 或 SPAM_HOST=0.0.0.0,应对公网暴露有明确需求并配合防火墙、反代与强密钥。SPAM_API_KEYS 分密钥并设置 max_rpm,降低单 key 泄露后的滥用面。classify_calls)含评论正文等数据:SQLite 注意文件权限;MySQL 注意账号最小权限、网络隔离与备份留存策略。joblib。插件头注明 GPLv2 or later(与 WordPress 生态常见约定一致);若你单独发布服务代码,可自行选择与服务侧一致的许可证。