很久之前,就经常收到akismet的授权提醒,对应一个错误码10010。
刚开始还以为是多域名访问导致的授权校验出问题了。后来换了n个key,同时添加了插件hook掉所有的垃圾评论检测逻辑,让全部走统一的域名,结果前几天又收到这个提醒了。
插件代码:
<?php
/**
* Plugin Name: Akismet 单一主域名(多域名站点)
* Description: 当站点配置了多个域名时,强制发往 Akismet 的请求只使用一个主域名,避免被计为多站点触发 10010。
* Version: 1.0
* Author: obaby
*
* 使用:在下方设置 AKISMET_CANONICAL_HOME 为主域名(或留空则用 WordPress「设置」里的站点地址)。
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 主域名(规范 URL,不要末尾斜杠)。留空则使用 get_option( 'home' )。
* 例如: https://www.example.com
*/
if ( ! defined( 'AKISMET_CANONICAL_HOME' ) ) {
define( 'AKISMET_CANONICAL_HOME', 'https://zhongxiaojie.cn' );
}
/**
* 获取发往 Akismet 时使用的唯一主域名 URL。
*/
function akismet_single_domain_get_canonical_home() {
$home = AKISMET_CANONICAL_HOME;
if ( $home === '' || $home === null ) {
$home = get_option( 'home' );
}
return untrailingslashit( $home );
}
/**
* 将任意 URL 替换为使用主域名的版本(只改 host,保留 path/query)。
*/
function akismet_single_domain_normalize_url( $url, $canonical_home ) {
if ( empty( $url ) || ! is_string( $url ) ) {
return $url;
}
$parsed = wp_parse_url( $url );
$canon = wp_parse_url( $canonical_home );
if ( empty( $canon['scheme'] ) || empty( $canon['host'] ) ) {
return $url;
}
$scheme = isset( $parsed['scheme'] ) ? $parsed['scheme'] : $canon['scheme'];
$host = $canon['host'];
$path = isset( $parsed['path'] ) ? $parsed['path'] : '/';
$query = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';
$frag = isset( $parsed['fragment'] ) ? '#' . $parsed['fragment'] : '';
return $scheme . '://' . $host . $path . $query . $frag;
}
/**
* 统一 verify-key / get-subscription / get-stats 的 blog 为主域名。
*/
add_filter( 'akismet_request_args', function ( $request_args, $path ) {
$paths = array( 'verify-key', 'get-subscription', 'get-stats' );
if ( ! in_array( $path, $paths, true ) ) {
return $request_args;
}
$canon = akismet_single_domain_get_canonical_home();
if ( ! empty( $request_args['blog'] ) ) {
$request_args['blog'] = $canon;
}
return $request_args;
}, 10, 2 );
/**
* 统一 comment-check(以及 recheck)的 blog、permalink,并把请求里的 HTTP_HOST 等改为主域名。
*/
add_filter( 'akismet_request_args', function ( $request_args, $path ) {
if ( $path !== 'comment-check' ) {
return $request_args;
}
$canon = akismet_single_domain_get_canonical_home();
$parsed = wp_parse_url( $canon );
if ( empty( $parsed['host'] ) ) {
return $request_args;
}
$canon_host = $parsed['host'];
$request_args['blog'] = $canon;
if ( ! empty( $request_args['permalink'] ) ) {
$request_args['permalink'] = akismet_single_domain_normalize_url( $request_args['permalink'], $canon );
}
// 让服务端看到的“当前请求”也统一为主域名,减少被计为多站点
if ( isset( $request_args['HTTP_HOST'] ) ) {
$request_args['HTTP_HOST'] = $canon_host;
}
if ( isset( $request_args['REQUEST_URI'] ) ) {
$uri = $request_args['REQUEST_URI'];
$request_args['REQUEST_URI'] = ( is_string( $uri ) && ( $p = wp_parse_url( $uri, PHP_URL_PATH ) ) !== null ) ? $p : '/';
}
if ( isset( $request_args['DOCUMENT_URI'] ) ) {
$uri = $request_args['DOCUMENT_URI'];
$request_args['DOCUMENT_URI'] = ( is_string( $uri ) && ( $p = wp_parse_url( $uri, PHP_URL_PATH ) ) !== null ) ? $p : '/';
}
return $request_args;
}, 10, 2 );
/**
* 统一 submit-spam / submit-ham 的 blog、permalink。
*/
add_filter( 'akismet_request_args', function ( $request_args, $path ) {
if ( ! in_array( $path, array( 'submit-spam', 'submit-ham' ), true ) ) {
return $request_args;
}
$canon = akismet_single_domain_get_canonical_home();
$request_args['blog'] = $canon;
if ( ! empty( $request_args['permalink'] ) ) {
$request_args['permalink'] = akismet_single_domain_normalize_url( $request_args['permalink'], $canon );
}
return $request_args;
}, 10, 2 );
这次授权的密钥撑得时间稍微长了点,但是最终还是收到了这个提醒,意思是需要订购商业版授权。我这个人站点为了发垃圾评论订购一个商业版授权,确实有些难以接受。
于是,我决定自建反垃圾评论系统,基于scikit-learn实现了现在的这套垃圾评论检测系统,训练数据一部分来源于github的开源数据,另外一个就是我自己博客的评论数据。为了保证样本正例和负例数量差别不至于过大,经过各种方式进行了多轮数据清洗。
如果想要评论识别更加准确,可以提供自己的博客评论数据,如果能提供垃圾评论更好。现在欠缺的主要是垃圾评论数据,正常的评论数据我已经提供几千条数据。
效果测试:
测试地址:https://anti-spam.zhongxiaojie.cn/test/spam
简介:
面向 中英混合 评论的 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 插件
关键配置:
| 变量 | 说明 | |------|------| | `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` |
系统服务启动截图:
wp插件配置:
项目地址:https://anti-spam.zhongxiaojie.cn
代码地址:https://cnb.cool/oba.by/baby-wp-anti-spam
说明:如果自己不想训练数据,下载发布版的spam_pipeline.joblib 放入指定目录下配置服务启动即可,baby-anti-spam.zip 为wp插件。
训练耗时大约11分钟:
博客: obaby 𝐢𝐧⃝ void
地址: https://oba.by/
文章: 《Baby Anti-Spam 自建反垃圾评论系统》










60 comments
😬看不懂看不懂,我只看头图。多发点!!!
嘻嘻。酱紫的吗
还可以再大胆点,清凉些。
我们爱看。 
8行,会被消失的
你们都是高手,我就是个普通用户,啥也不懂🤦♂️
没招,自己折腾,哈哈哈
一点不服,就自己搞插件了啊,强者的世界,不懂
实在是不想再去申请key了
akismet把你当成商业使用了?😂
是的,封了我n个key了
为啥呀,难道是因为那个闺蜜圈?这判断逻辑太迷惑了
很有可能
不过如果是认定广告(我有商业行为),难道还来看网页内容了?
这可太有实力了……
我的小站连垃圾评论都看不上……
这个都不需要实力,wp的垃圾评论插件无差别攻击。哈哈哈
有垃圾评论的说明人家看你那有价值,不像俺这小垃圾站,都某得垃圾评论
这玩意儿都是批量扫描的,和价值无关
我最近在使用 LiMhy,这些个大佬动不动就手搓插件,弄个主题,太厉害了
都是大佬
这个插件连接大模型的吗?
不是 简单机器学习算法
不看内容,我以为你登基了!
别瞎说,
叫女皇
设置个评论验证码不是也能拦住部分吗?
不喜欢验证码,体验太差
这也太强了吧,牛刀杀鸡,用到了机器学习,要另起一个flask服务
php搞算法太麻烦了,直接起个服务最简单
也算是接入ai了
算不上,充其量是个算法,哈哈
哈哈哈哈我现在就是搞注册加验证码,基本上就没垃圾评论了。不过搞个插件也不错
你这又是注册,登录,又是验证码的。太复杂了。
如果不是真爱,没人会费那么大功夫评论。
哈哈哈哈其实也还好,邮箱注册是有点麻烦,不过我最近开发了一个QQ第三方登录插件,注册就方便了很多。
其实一切都是为了让博客干净一些
厉害了,羡慕
没办法,受限制了就不如自己弄一个。
现在写代码不是问题,只要有想法,天天都可以用大模型写脚本和插件,甚至出项目。不过有的时候想法挺好,实现起来总是达不到预想的效果,可能是细节考虑的还是不到位。
好处是,看不顺眼的,可以自己写,坏处是代码不在值钱了。
代码不值钱这个石趋势,必然如此
100多个违禁词、不允许纯数字/字母、不允许网址、必须4个中文以上。差不多了吧?
其实禁止纯英文,网址,就能k掉一大部分了
灵妹妹的露背装第一次见哦
这件秀禾服就是这么设计的。
评论量太多了也不是好事情
越多越好,可以用来训练模型
技术有点难懂,但是背景图很哇塞啊
是叭,我也觉得。
我也用 这个,不过不是自建的,目前正常。
拦截的垃圾评论这么多…..我很少遇到
嗯嗯,所以一直用的这个,不过最近批发给我封序列号,我是真的麻了。
scikit-learn 库我也用过,研究机器学习那时。这篇日志里,主要用他哪一个 api 做的训练和预测呀,线型回归么
return LogisticRegression(
max_iter=max_iter,
class_weight="balanced",
random_state=42,
solver="lbfgs",
C=1.0,
)
mark/你这个专业,我只弄了关键词ip或者cf
关键词的我也弄了个插件,不过我觉得垃圾评论还是得识别一下。
习惯了~~
这可太有实力了……
我的小站连垃圾评论都看不上……
wp众矢之的,tp的貌似少些。
用这个自动识别垃圾,访客体验上来说比搞各种验证码的要好的多 👍
wp的确是众矢之的,谁都能扫扫,谁都想发点垃圾东西
官方这个WP自带的插件一直没有用过,安装完WP第一件事情就是卸载了。妹子这个还能训练啊。
我一直用的官方的,不过现在天天被封key,实在是换不动了。
自己训练个模型,勉强先用着。开源、安全。
来学习了!
自从屏蔽了国外的ip,就没有这个烦恼了。
国内的也发,不但是国外的。嘎嘎
头图好评