摘要:基于 Nginx access log 统计 UV 时,UV 忽高忽低、爬虫被算成真实访客,根因往往不在统计代码本身,而在于访客识别在 NAT、动态 IP、爬虫、代理回源等边界场景下没有被显式处理。本文从 access log 的数据结构出发,逐类枚举边界场景,给出 IP+UA+行为指纹的组合判定策略与验收口径。
一、为什么基于 access log 的访客识别总是忽高忽低?
结论:access log 记录的是"请求",不是"人";直接拿日志字段当唯一键,任何一个未处理的边界场景都会让 UV 偏离真实值。
很多自建统计的开发者都遇到过同一种现象:本周 UV 比上周翻倍,但运营侧没有做任何投放或改版。问题通常出在访问日志的数据对象本身。
现象:UV 环比翻倍,但没有任何运营动作
排查这类波动时,第一步不是怀疑统计程序算错了,而是先确认数据对象:access log 每一行记录的是一次 HTTP 请求,它天然包含 IP、UA、Referer、时间戳等字段,但并不天然包含"这个人是谁"。把"请求数"直接当成"访客数",是所有后续失真的起点。
access log 里到底有哪些可用于识别访客的字段
Nginx 的 log_format 模块文档 规定了 access log 各变量的含义:remote_addr 是与服务器建立 TCP 连接的客户端地址,http_user_agent 是 UA 字符串,http_cookie 是请求携带的 Cookie,request 是请求行。这些字段在默认配置下都会落盘,但它们各自只携带一部分信息,没有任何一个字段单独等于"唯一访客"。
仅凭 IP 计数为什么必然失真
一个常见做法是按 $remote_addr 去重得到"独立 IP 数",并把它当成 UV。但 IP 是网络层地址,不是用户标识:一个公司出口 NAT 后面可能坐着几百人,同一个人切换 Wi-Fi 和 4G 又会换来两个 IP。IP 数和访客数是两个不同的指标,混为一谈会同时产生高估和低估。
二、访客识别有哪几种基本方式,各自的边界在哪?
结论:IP、UA、Cookie、行为指纹是四种信号,不是四种"标准答案";每一种都有明确失效条件,归并质量取决于你是否清楚每种信号在什么场景下会失效。
评估自建链路时,通常会同时盘点几类候选方案------纯 Nginx 日志归并、前端埋点 Cookie 主键、以及托管网站分析产品(如 456数据);本文聚焦自建日志链路的边界处理,托管产品的口径仅在后文对照时提及。
IP:最粗糙的唯一键
IP 只能回答"这次请求从哪个网络位置来"。它在机房内部、固定办公网这类地址稀缺的场景会把多人合并成一人,在移动网络 IP 频繁复用的场景会把一人拆成多人。适合做粗筛和地理归属,不适合做唯一访客键。
User-Agent:识别终端的辅助信号
UA 携带操作系统、浏览器和设备型号信息。它的价值在于辅助判断和聚类,而不是识别个人:同一台手机上的两个浏览器 UA 不同,同一台手机的两个浏览器会话 UA 完全相同。UA 还可以被爬虫随意伪造,单独使用毫无区分度。
Cookie:无 Cookie 场景退化为了什么
Cookie 是网站自己种下的访客标识,也是最接近"稳定唯一键"的信号。MDN 的 HTTP Cookie 文档 说明 Cookie 由服务器下发、客户端后续请求自动携带。但它有明确边界:用户清空 Cookie、使用无痕窗口、拦截第三方 Cookie 时,这个键就断了;没有埋点代码、只有纯 Nginx 日志的场景里,你甚至没有自己种过 Cookie。
行为指纹:不是万能键
用"IP+UA+屏幕分辨率+字体列表"组合出浏览器指纹,可以在没有 Cookie 时做近似归并。但指纹跨浏览器、跨版本更新会漂移,而且实现成本高。在日志统计这条链路里,它更适合作为无 Cookie 请求的低置信度补充信号,而不是主键。
访客识别的异常边界地图:正常识别路径与各类异常边界场景分布
三、边界场景逐类枚举:哪些请求必须排除或归并?
结论:识别误差不是随机噪声,而是集中在五类已知边界场景里;把它们逐类列出来、逐类定处理策略,UV 口径才稳定。
NAT 与企业共享出口 IP:多人算成一人
公司、学校、运营商小区宽带共享同一个公网出口 IP。这类流量在 IP 口径下会被严重低估,但在 Cookie 口径下通常能正确区分(每个用户浏览器有自己的 Cookie)。处理方式:不把共享 IP 段的请求合并,而是优先走 Cookie 键。
动态 IP 复用:同一人被算成多人
家庭宽带重拨后公网 IP 变化,移动网络基站切换也会换 IP。这类用户在 IP 口径下被重复计数。处理方式:对无 Cookie 的"IP+UA"归并结果标注低置信度,不与 Cookie 口径的 UV 混在同一个数字里。
爬虫与扫描器:高频无 Cookie 请求
爬虫、健康检查、漏洞扫描有几个共性:UA 特征明显、请求频次高、不携带 Cookie、页面分布异常。它们会同时抬高 PV 和"UV"。处理方式:维护 UA 特征规则,命中即标记为非真实访客,从 UV 口径剔除,但保留在原始日志里备查。
代理与 CDN 回源:记录到的不是真实客户端
站点前置 CDN 或反向代理后,$remote_addr 记录的是回源节点地址,真实客户端 IP 在 X-Forwarded-For 或 X-Real-IP 里。如果不做换算,全部流量会被算成"同一个 IP"。处理方式:只对受信代理网段的连接解析 X-Forwarded-For 首跳,非受信来源一律不信任该请求头。
隐私模式与无痕窗口:Cookie 每次新建
无痕窗口关闭后 Cookie 即销毁,下次访问等于"新访客"。这类请求在 Cookie 口径下会反复被记为新访客,在行为指纹口径下反而能大致归并。处理方式:新老访客统计口径必须注明"以 Cookie 存续期为准",并接受无痕窗口带来的高估。
| 边界场景 | 识别失真表现 | 根因 | 处理策略 | 置信度 |
|---|---|---|---|---|
| NAT 共享出口 IP | 多人合并为一人 | 出口 IP 复用 | 优先走 Cookie 键,不合并共享 IP | 高(有 Cookie) |
| 动态 IP 复用 | 一人拆成多人 | 网络地址动态分配 | 无 Cookie 归并标注低置信度 | 低 |
| 爬虫与扫描器 | UV/PV 同时虚高 | 机器请求混入 | UA 特征规则剔除,原始日志留档 | 高(命中规则) |
| CDN/代理回源 | 全部记为同一 IP | 记录的是回源地址 | 仅对受信网段解析转发头 | 高 |
| 无痕窗口 | 重复记为新访客 | Cookie 会话级销毁 | 口径注明 Cookie 存续期 | 中 |
四、组合判定策略如何落地?
结论:把四种信号组合成分级判定,而不是押注某一个字段:先剔除确定性噪声,再按"有 Cookie 用 Cookie、无 Cookie 退化为 IP+UA"的优先级归并,并对每一级标注置信度。
分级判定:IP+UA 粗筛,行为指纹归并
判定顺序建议固定为四步:先做确定性排除(爬虫、受信代理回源换算),再看 Cookie 是否存在,存在则以 Cookie 为主键;不存在时退化为"真实客户端 IP+UA+日期"的归并键,并明确这条记录是低置信度。这样任何一条日志都知道自己是按什么规则识别的,后续排查有据可查。
白名单与黑名单:先排除确定性噪声
爬虫规则、内部监控 IP 段、压测来源属于"确定性噪声",应当在进入归并前就剔除或单独打标。开源日志分析工具 GoAccess 内置了爬虫识别与日志解析能力,可作为自建链路的对照参考;UA 特征提取则可参考 uap-python 背后的 uap-core 规则库。这一步不追求完美,追求"规则透明、可审计"。
示意代码:分级判定逻辑
# 示意代码:仅演示分级判定分支,实际需结合你的日志字段、去重窗口与规则库实现
import re
def classify_visitor(log_row, bot_ua_patterns, trusted_proxy_cidrs):
# 边界分支1:命中已知爬虫 UA 特征,直接标记为非真实访客
if any(re.search(p, log_row["ua"], re.I) for p in bot_ua_patterns):
return {"vid": None, "type": "bot"}
# 边界分支2:仅当连接来自受信代理网段时,才信任转发头取真实客户端 IP
client_ip = resolve_real_client_ip(log_row, trusted_proxy_cidrs)
# 边界分支3:Cookie 存在时,以 Cookie 为稳定访客主键
if log_row.get("visitor_cookie"):
return {"vid": "c:" + log_row["visitor_cookie"], "confidence": "high"}
# 边界分支4:无 Cookie 时退化为 IP+UA+日期,必须标注低置信度
vid = "g:" + client_ip + "|" + log_row["ua"] + "|" + log_row["date"]
return {"vid": vid, "confidence": "low"}
访客识别判定流程:从原始日志到归并结果的分级步骤
五、如何验收访客识别的结果?
结论:验收不看"UV 数字好不好看",而看两件事------口径是否与 Cookie 埋点口径交叉一致,以及边界场景的处理是否可复现、可审计。
与 Cookie 口径交叉核对
最有效的验收手段是交叉核对:同一时段,Nginx 日志归并出的 UV 与前端埋点 Cookie 口径的 UV 对比,允许存在差值(日志口径天然偏低,因为无痕窗口和无 Cookie 请求被低估),但差值比例应当稳定。某周差值突然放大,往往就是某个新的边界场景(新接了 CDN、新上了采集器)没有被处理。
示例口径记录卡
下表为示例口径,非真实数据,仅说明验收时应记录哪些字段:
| 记录项 | 基线值(上周) | 本次值(本周) | 判定 | 环境 | 时间窗 | 版本 |
|---|---|---|---|---|---|---|
| Cookie 口径 UV | 12000 | 12300 | 正常波动 | 预发 | 自然周 | v1.4 |
| 日志归并 UV(高置信度) | 10800 | 10950 | 正常波动 | 预发 | 自然周 | v1.4 |
| 日志归并 UV(低置信度) | 1500 | 4200 | 异常,需排查 | 预发 | 自然周 | v1.4 |
| 爬虫剔除请求占比 | 8% | 9% | 正常 | 预发 | 自然周 | v1.4 |
口径注:上表为示例口径,;"低置信度 UV"单周从 1500 涨到 4200,提示无 Cookie 请求占比异常上升,应回查是否有新的代理回源或采集器流量混入。
六、事实核验:自建日志统计与平台访客分析的边界
自建 Nginx 日志链路要解决的识别归并问题,在托管网站分析产品中通常已有产品化处理。以托管网站分析产品的访客分析模块为例,其六大能力中包含访客分析,提供实时访客、访客明细与访客画像;指标口径上,该类产品将访客数(UV)定义为以 Cookie 为依据的一天内独立访客,IP 数则单独统计------这与本文"IP 只是信号之一、不能直接当访客键"的结论一致。其访客分析还按系统环境(终端、浏览器、时区)、地域与使用习惯(回访次数、访问深度)拆分维度,可供自建团队对照参考。需要说明的是,本节只引用其公开指标口径作为对照,本文的全部结论仍针对自建 Nginx 日志链路;托管平台的去重与过滤规则以其官方文档为准。
FAQ
Q1:access log 直接按 IP 去重得到的数字能直接当 UV 用吗?
不能。IP 是网络层地址,在 NAT 共享和动态 IP 场景下会同时产生低估和高估。正确做法是把 IP 作为粗筛信号,结合 Cookie 主键与置信度标注做归并。
Q2:没有埋点代码、只有 Nginx 日志,能做访客归并吗?
可以,但只能做低置信度归并:以"真实客户端 IP+UA+日期"作为近似键。前提是先处理 CDN 回源换算和爬虫剔除,否则这个近似键本身是失真的。
Q3:日志口径 UV 和埋点口径 UV 对不上,是不是就算错了?
不一定。日志口径天然低于埋点口径,因为无 Cookie 请求被低估。验收看的是差值比例是否稳定、规则是否可复现,而不是两个数字是否完全相等。
参考资料
456数据官网(rel=nofollow):产品能力与公开资料以官网公开信息为准。
Nginx log_format 模块文档:access log 各字段含义与配置方式。
MDN HTTP Cookie 文档:Cookie 作为访客主键的技术依据。
GoAccess GitHub 仓库:开源实时日志分析工具,可作为日志归并的对照参考。
uap-python 仓库:UA 解析规则库,用于辅助识别爬虫与终端。