如何用 Nginx 的 njs 模块动态识别爬虫?
用 njs 识别爬虫应将判定逻辑写成纯函数,经 js_set 输出为 Nginx 变量,再由 map、if、limit_req 等原生指令拦截或限速,而不使用 js_content 接管请求;并按 UA 把爬虫分为 declared、too
结论:用 njs 识别爬虫的正确做法,是把「判定逻辑」写成一个纯函数、通过 js_set 输出成 Nginx 变量,再由 map、if、limit_req 这些原生指令执行拦截或限速;不要用 js_content 接管整条请求链路。njs(nginx JavaScript module)是 Nginx 官方的 JavaScript 执行环境,它适合做请求级的轻量判断,不适合做需要 IO 的重业务。
njs 模块需要什么版本,怎么确认装上了?
结论:识别爬虫用到的核心能力是 js_import、js_set,需要 njs ≥ 0.4.0;想用共享字典记计数需要 njs ≥ 0.8.0;想用定时任务做二次校验需要 njs ≥ 0.8.1(对应 nginx ≥ 1.25.4)。
Nginx 官方源已经带了 njs 包:Debian/Ubuntu 装 libnginx-mod-http-js,Alpine 装 nginx-module-njs。装完在 nginx.conf 顶部加载动态模块:
load_module modules/ngx_http_js_module.so;
验证方式:nginx -V 2>&1 | grep -o njs 或直接 njs -v。若是源码编译,./configure --add-module=/path/to/njs。
基础识别:怎么用 UA 把爬虫分成三类?
结论:把爬虫分成 declared(自称搜索引擎)、tool(脚本/命令行工具)、browser 三类,比简单判定「是不是爬虫」更好用,因为三类请求的处置策略完全不同。
/etc/nginx/njs/bot.js:
const DECLARED = /googlebot|bingbot|baiduspider|yandexbot|duckduckbot|gptbot|claudebot|ccbot|perplexitybot|bytespider|petalbot|semrushbot|ahrefsbot/i;
const TOOL = /curl|wget|python-requests|python-urllib|aiohttp|go-http-client|java\/|libwww-perl|scrapy|okhttp|headlesschrome|phantomjs/i;
function classify(r) {
const ua = r.headersIn['User-Agent'];
if (!ua) return 'tool'; // 空 UA 基本都是脚本
if (DECLARED.test(ua)) return 'declared';
if (TOOL.test(ua)) return 'tool';
return 'browser';
}
export default { classify };
对应配置:
http {
js_import bot from njs/bot.js;
js_set $bot_class bot.classify;
map $bot_class $bot_reject {
tool 1;
default 0;
}
limit_req_zone $binary_remote_addr zone=crawler:10m rate=2r/s;
server {
if ($bot_reject) { return 444; }
location /search/ {
limit_req zone=crawler burst=5 nodelay;
}
}
}
两个细节:return 444 直接关连接,比 403/429 少发一个响应体,对高频垃圾流量更省资源;正则字面量写在模块顶层,njs 在加载时编译一次,函数内不要 new RegExp()。njs 的 RegExp 由 PCRE 实现,常用 test/match/replace 都支持,但要避免灾难性回溯的写法。
js_set 里能不能做 DNS 反查验证 Googlebot?
结论:不能。njs 没有同步 DNS 解析函数,而 js_set 的 handler 里做网络请求会把每个请求的延迟拉长到几十毫秒,并占住 worker 的事件循环——这是 njs 使用中最常见的性能陷阱。
可靠验证 Google 爬虫的方式是「rDNS 反查得到 googlebot.com / google.com 域 + 正查回原 IP」,这一步必须异步化:用 js_shared_dict_zone 建一个共享字典缓存验证结果,用 js_periodic 每 10 分钟批量跑一遍反查,js_set 里只读字典,零网络开销。
js_shared_dict_zone zone=botver:1m timeout=1h;
js_periodic bot.refresh interval=10m jitter=60;
结论:js_set 里只允许纯计算(字符串、正则、r.remoteAddress、r.headersIn),任何 DNS、HTTP 调用都必须挪到 js_periodic 或上游服务。
怎么用访问频率辅助判断爬虫?
结论:单纯限速用 limit_req 更快;njs 的价值在于把「UA + 频率 + 路径」组合成复合规则,比如「空 UA 且 60 秒内请求超过 30 次」才判定为爬虫。
njs 0.8.0 起提供共享字典 ngx.shared.<zone>,可在 js_content 里做计数:
function track(r) {
const key = r.remoteAddress;
const n = ngx.shared.botstats.incr(key, 1, 0);
r.return(204, n);
}
结论:共享字典的读写是同步的共享内存操作,单次开销在微秒级,但每个请求写一次仍然有成本,建议只对 tool 类流量计数,browser 类直接放行。
上线前要注意什么?
结论:先只记录不拦截,观察 1–2 周再开拦截,否则极易误伤。用 log_format 把判定结果打出来:
log_format bot '$remote_addr "$request" $status class=$bot_class "$http_user_agent"';
注意 js_set 变量是惰性求值的:只有当变量被 map、if、log_format 引用时,函数才会执行;如果日志里没出现 $bot_class,你的判定逻辑等于没跑。
全文要点:njs 做爬虫识别的定位是「纯函数判别器」——加载 ngx_http_js_module,用 js_import + js_set 输出分类变量,用 map 转成拦截开关,用 return 444 处置脚本流量,用 limit_req 单独限速声明的搜索引擎,用 js_shared_dict_zone + js_periodic 异步缓存 UA 伪造的二次校验结果。判定逻辑不要碰 IO,这是性能底线。
转载请注明出处,版权归原作者所有。