百度统计的搜索词报告突然被大量"威尼斯人""棋牌充值""彩票投注"之类的垃圾词刷屏,这是很多站长在运营过程中都遇到过的糟心事。这些词往往伴随着大量虚假点击,让真实关键词数据被淹没,甚至可能被百度判定为违规优化而影响网站收录。刷词攻击的原理并不复杂:攻击者抓取到你页面中的百度统计代码,提取出统计ID,然后用脚本模拟浏览器请求,在请求参数中携带伪造的搜索词来源,反复提交给百度统计服务器。只要统计ID是公开的,这种伪造请求就能轻松产生海量垃圾数据。

要阻止刷词,必须从请求源头做验证。最直接的方式是检查统计代码加载前,请求是否来自合法的搜索引擎结果页或真实用户浏览器。下面将分三个层次给出具体方案,从最快见效的临时屏蔽,到彻底切断攻击路径的代码级防御,你可以根据自身技术能力选择合适的组合。
1. 用来源域名校验拦截伪造流量
百度统计在记录搜索词时,依赖HTTP请求头中的Referer字段来判断用户是从哪个搜索引擎跳转过来的。真实的百度搜索流量,Referer一定是https://www.baidu.com/s?wd=关键词或https://m.baidu.com/s?word=关键词这样的正规地址。而攻击者用脚本伪造搜索词时,虽然也会构造Referer,但要么缺少必要的查询参数,要么来源域名根本不是百度官方域名。
你可以在统计代码执行前加一段JavaScript校验逻辑,仅当当前页面Referer的域名是baidu.com且包含搜索词参数时才加载统计代码。这样脚本伪造的非真实流量在到达百度统计服务器之前就被拦下。下面的代码示例演示了如何做这层过滤,注意代码必须放在百度统计脚本之前执行。
// 仅当来源是百度搜索时才加载统计代码
(function() {
var refer = document.referrer;
if (!refer) return; // 直接访问不加载,可根据需要调整
var url;
try {
url = new URL(refer);
} catch(e) {
return; // 非法Referer直接丢弃
}
var host = url.hostname;
// 只允许 www.baidu.com 和 m.baidu.com
if (host !== "www.baidu.com" && host !== "m.baidu.com") {
return;
}
// 必须包含搜索词参数
var wd = url.searchParams.get("wd") || url.searchParams.get("word");
if (!wd || wd.trim() === "") {
return;
}
// 通过校验后才加载百度统计
var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?你的统计ID";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
})();
这段代码的原理是:只有Referer来自www.baidu.com或m.baidu.com,且查询参数中确实有wd或word关键词时,才会向页面插入百度统计的脚本。攻击者用脚本伪造请求时,通常直接调用统计接口,不会先正常加载页面再触发JavaScript,因此即使他们携带了假Referer,也过不了页面端的校验逻辑。
需要提醒的是,这种方案会牺牲一部分直接访问流量或非百度搜索流量的统计,如果你需要统计全部来源,可以放宽条件,只过滤掉明显异常的域名,比如含有casino、bet等赌博词汇的Referer。但最稳妥的方式依然是严格的来源白名单,只有当你的网站对全来源统计要求不高时才建议放宽。
2. 在服务端屏蔽非浏览器特征请求
仅仅靠页面JavaScript校验还不够,因为攻击者可以跳过页面直接请求百度统计服务器,也就是所谓的"离线刷词"。要彻底拦截这类请求,需要在服务端对访问百度统计接口的流量做特征过滤。百度统计官方提供的hm.js脚本最终会向hm.baidu.com发送GET请求,请求中包含统计ID和当前页面的URL信息。攻击者只要抓取一次正常请求,就能复制其中的格式批量提交。
一个有效的防守方式是在Nginx等反向代理层配置规则,拒绝明显非法的User-Agent或缺少必要请求头的统计请求。如果你使用Nginx作为入口,可以添加如下配置,过滤掉大部分脚本模拟的流量。
# 放在 server 块内,针对统计接口的请求进行过滤
location = /hm.gif {
# 只允许带有正常浏览器UA的请求
if ($http_user_agent !~* "(Mozilla|Chrome|Safari|Firefox|Edge|Opera)") {
return 444;
}
# 必须带有Referer且域名合法
if ($http_referer !~* "baidu\.com|google\.|bing\.|sogou\.com|360\.cn") {
return 444;
}
# 正常处理
proxy_pass http://hm.baidu.com;
}
上面的配置假设你的Nginx当前有一个/hm.gif路径映射到百度统计的接口,实际情况中百度统计的接收端点路径需要通过抓包确认,不同版本可能不同。核心思路是用Nginx的if指令检查User-Agent是否包含主流浏览器标识,以及Referer是否来自已知搜索引擎。脚本请求往往使用Python的requests库或curl发送,UA是python-requests或空值,会被第一条规则直接拒绝。
如果不想改动Nginx,也可以让网站后端程序在输出页面时生成一个带有签名的一次性统计令牌,统计代码携带该令牌才能被百度统计接收。不过百度统计的接收接口无法自定义参数,所以这套方案需要配合百度统计的异步自定义事件功能,实现起来复杂度较高,更适合有一定开发能力的团队。
3. 切换统计口径与日常清洗策略
即使做了上面两层防御,针对统计ID的刷词攻击仍然可能偶尔漏过,因为攻击者会不断更换UA和Referer模拟得越来越像真实浏览器。此时你需要调整对统计数据的信任程度,把百度统计中的搜索词报告当作参考值而非绝对真实值。建议配合百度搜索资源平台的搜索词数据做交叉验证,后者基于站长工具获取的官方数据,刷词难度远高于公开统计代码。
对于已经产生的垃圾搜索词,百度统计后台提供了"搜索词屏蔽"功能,但它只能在报表中隐藏指定关键词,并不能阻止攻击者继续发送伪造请求。更有效的清洗做法是定期导出数据,用脚本过滤掉包含敏感词库的条目。下面的Python示例展示了如何对导出的CSV文件做批量清洗。
import csv
# 敏感词列表,根据实际攻击词扩展
blacklist = ["赌博", "棋牌", "彩票", "威尼斯人", "色情", "成人", "博彩"]
def is_junk(keyword):
kw = keyword.lower()
for bad in blacklist:
if bad in kw:
return True
return False
def clean_csv(input_file, output_file):
with open(input_file, "r", encoding="utf-8-sig") as fin:
reader = csv.DictReader(fin)
rows = list(reader)
cleaned = [row for row in rows if not is_junk(row.get("关键词", ""))]
with open(output_file, "w", encoding="utf-8-sig", newline="") as fout:
writer = csv.DictWriter(fout, fieldnames=reader.fieldnames)
writer.writeheader()
writer.writerows(cleaned)
print(f"清洗完成,剩余 {len(cleaned)} 条记录")
clean_csv("百度统计导出.csv", "清洗后数据.csv")
这个脚本读取百度统计导出的搜索词数据,逐行判断关键词是否命中黑名单,并输出干净的数据文件。黑名单需要根据你实际遭受的攻击词持续更新,建议先观察一到两周的攻击词特征,提取出现频率最高的前20个词加入列表。要注意的是,该脚本无法找回被垃圾词挤掉的真实数据,只对历史导出数据做清理,不能替代实时防护。
如果刷词情况非常严重,已经影响到网站的正常统计,也可以考虑将统计切换到百度统计的"防刷模式"或改用服务端日志分析。部分国产统计工具提供更严格的请求签名机制,例如友盟、CNZZ的新版实现,在切换前需要评估迁移成本和历史数据保留问题。
4. 长期防御的架构建议
从架构层面看,公开统计ID的刷词问题本质上是统计代码缺少请求签名验证导致的。如果要一劳永逸,建议把统计逻辑从纯前端脚本改为"前端埋点+后端转发"模式。前端只采集行为数据并提交给自己的服务器,由后端加上时间戳和签名后再转发给百度统计接口。这样即使攻击者抓到了统计ID,没有后端签发的合法签名也无法伪造请求。
实现该方案需要自行编写一个简单的统计转发接口。前端通过fetch或XMLHttpRequest把pv数据发给后端,后端验证请求头中的X-Signature与当前时间戳的哈希是否匹配,匹配通过后才调用百度统计的接收地址。攻击者无法知道你的签名算法和密钥,因此无法绕过校验。下面的Node.js代码展示了一个极简的转发接口示例。
const express = require("express");
const crypto = require("crypto");
const app = express();
const SECRET = "你的站点密钥请更换为随机字符串";
// 前端埋点请求转发
app.get("/track", (req, res) => {
const ts = req.query.ts;
const sign = req.query.sign;
if (!ts || !sign) {
return res.status(403).send("forbidden");
}
// 签名 = md5(secret + ts),确保时间戳在5分钟内
const now = Date.now();
const diff = Math.abs(now - parseInt(ts, 10));
if (diff > 5 * 60 * 1000) {
return res.status(403).send("expired");
}
const expect = crypto.createHash("md5")
.update(SECRET + ts)
.digest("hex");
if (sign !== expect) {
return res.status(403).send("invalid sign");
}
// 到这里说明请求合法,再异步转发给百度统计
// 实际可调用百度统计的服务端API,或记录日志后手动上报
res.send("ok");
});
app.listen(3000, () => console.log("track server running"));
这个示例中的签名算法是md5(密钥 + 时间戳),前端在发起埋点请求时用同样的规则生成签名,并附带当前毫秒时间戳。后端校验时间戳在五分钟内且签名匹配,这样就堵住了所有没有密钥的伪造请求。实际生产环境中,密钥需要保存在服务器环境变量中,绝不能写入前端JavaScript,否则攻击者直接下载页面源码就能提取密钥,防护效果归零。
百度统计官方也推出了新版统计代码使用hm.baidu.com/hm.js?xxxx形式,但本质上依然只是加载一个公开脚本,没有做请求签名。指望官方短期解决这个问题不太现实,站点运营者自己加一层签名转发是目前最可靠的长期方案。如果团队资源有限,至少也要做到第一层来源校验加第二层Nginx过滤,先止住最猖獗的刷词潮,再逐步升级到签名方案。