ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

流量我自己掌控:从 nginx 原始日志扒出真实下载量,官网到底谁在用

流量我自己掌控:从 nginx 原始日志扒出真实下载量,官网到底谁在用 先把结论摆在前面。一是这个桌面软件官网日均两千到三千的 PV、一百四到三百一十个独立 IP大头是已安装客户端的更新心跳和扫描器探测真人浏览比看板上的数字小得多搜索来的来源近乎为零。二是下载量这件事我认可的判据是响应状态码 200 且实际发出字节数不低于 30MB按请求次数去数会被断点续传和分段下载虚增好几倍。三是曾经有一天下载量冲到 81 次复盘下来 80 次是 GPTBot 在批量抓历史版本安装包把爬虫和自家备份脚本全剔干净之后两周里真人完整拉走安装包的数量约等于 0 到 1。先如实交代来源。这是一个卖桌面电商合规检测工具的软件官网一台机器上一个 nginx主站加下载目录都在它手里没有埋点没有分析平台所有数字来自同一个地方——access.log主日志加轮转出来的 14 个 gz 文件。热榜上那类自建数据分析平台的方案我没走不是它不好而是它回答不了我较真的那个问题看板说今天有三百人访问那到底是三百个人还是三百台机器这个问题只有原始日志能给你一个不会撒谎的裁决。一、为什么不装分析平台却又离不开日志各类自建分析平台的思路都差不多页面里注入一段脚本浏览器执行了脚本才算一次访问事件、来源、停留时长按会话拼起来。它的长处是省事开箱就有一块能看的板子。但我盯上的指标它天生给不了——这个网站的核心转化动作不是浏览是把一个几十 MB 的安装包完整拿走。浏览器执行脚本和你有没有拿到完整文件中间隔着十万八千里用户点了下载、下到 40% 关掉窗口、换个网络重来分析平台只记得到点击而 nginx 把每一段传输了多少字节都写在了日志里。传输发生在哪里真相就在哪里。还有一条更私心的理由数据主权。日志躺在自己的服务器上格式自己定的保留多久自己说了算不存在分析服务挂了看板归零这种依赖。所以我的选择是不装平台直接面对原始日志。但原始日志有个原始日志的代价它不会主动告诉你任何结论每一行都一视同仁真人的点击、客户端的心跳、扫描器的探测、AI 爬虫的扫库全混在同一个文件里长得几乎一模一样。口径不立起来你从日志里算出来的每一个数都可以是错的而且错得很自信。这篇文章就是把这套口径怎么立起来的完整过程先盘材料再给流量分身份然后立下载量的判据最后复盘那次 81 次的尖峰。二、材料14 天全量日志先得读对2.1 轮转文件的合并顺序是个坑日志在系统默认的日志目录下结构很朴素当天写 access.log每天轮转一次昨天的变 .1前天的变 .2依此类推压成 gz。要统计最近两周得把主文件加十四个压缩包拼成一条流# 统一读取入口zcat -f 对未压缩文件原样透传压缩文件边解边吐# 注意花括号展开要写成 {14..1}轮转序号是越小越新正着拼时间就是乱的zcat-faccess.log.14.gz access.log.13.gz access.log.12.gz/tmp/old.txt# 上面的显式写法太笨实际用大括号展开bashzcat-faccess.log.{14..1}.gz access.log/tmp/two_weeks.txt# 顺手确认几件事总行数、时间跨度、文件是否齐全wc-l/tmp/two_weeks.txthead-1/tmp/two_weeks.txt;tail-1/tmp/two_weeks.txt按天分桶的统计其实不怕乱序时间戳就在每行里但凡是顺着时间线走的分析——比如看某个 IP 的会话行为、判断一次下载是不是中断后续传——就必须先把顺序理顺。我有一次对着乱序文件排下载中断的问题得出的结论完全颠倒重查了半小时才怀疑到合并顺序上。这种坑不致命但专治自信。2.2 字段表先把每个位置是什么钉死日志格式是默认的组合格式只做了两处定制开头加了站点主机名字段因为同一台机器上还挂着别的站统计前必须按 host 过滤另外如果前面挂了 CDN 或反代客户端地址拿的是转发头里的真实 IPnginx 自己记的那一列是边缘节点的地址——这个坑我们踩过后面所有统计的起点都是过滤对了 host、认对了 IP。以这份格式为准把字段的物理位置钉死下文所有 awk 都按这个表取列。一条日志长这个样子协议版本记号和 UA 细节用省略号略过——本文自己有条规矩不贴整串的链接型字符串。字段编号不受影响主机名 203.0.113.7 - - [22/Sep/2026:03:11:42 0800] GET /dl/setup-3.2.1.exe … 206 15728640 - Mozilla/5.0 …位置含义读的时候最容易搞错的地方$1站点主机名不加过滤几站的流量混着算PV 直接翻倍起步$2客户端地址NAT 后面一个地址能顶一个办公室拨号用户一天换好几个它是线路不是人$5日期加时间取日期用 substr($5,2,11)前头有个左方括号$8请求路径含查询串参数全藏在这一列里只看路径不看参数会漏掉关键身份$10响应状态码200 完整、206 分段、304 缓存有效、404 不存在206 不是错误$11实际发出的字节数它是本次传输量不是文件大小206 的一行只有分段的字节$12来源页字段“-” 是直连或机器请求非 “-” 再分站内跳转和站外引荐$13 起UA 字段内部有空格靠空格切列在 UA 上必翻车一律用整行正则匹配这张表里最值钱的是 $11 那一行。凭直觉谁都愿意把有人来下载文件理解成日志里 .exe 那一行的字节数是文件大小——错了。$11 记录的是这一次响应实际吐出去了多少字节一次下载被切成五段就是五行日志每行只有自己的那份字节。这个认知差就是第四节整节的内容。三、先把三千 PV拆开心跳、扫描器、爬虫和真人3.1 一行流先把总量立起来先按天数一遍 PV 和独立 IP跟自己的印象对齐确认读的数据没缺# 按日统计 PV / UV独立地址数awk 按字段表取列zcat-faccess.log.{14..1}.gz access.log\|awk-vh你的host字段值$1h { d substr($5,2,11) pv[d] if (!seen[d,$2]) uv[d] } END { for (k in pv) printf %s pv%-6d ip%-4d\n, k, pv[k], uv[k] }|sort跑出来的量级日均 PV 两千到三千独立 IP 一百四到三百一十。乍看是个还算热闹的小站。但接着把来源页字段一分组凉意就上来了非 “-” 的来源里站外引荐合计接近于零——搜索进来的流量约等于没有。热闹是真的但热闹不是访问。3.2 心跳latest.yml 不是人是装过软件的人在用软件翻请求路径的分布会发现三个眼熟的家伙霸着前排latest.yml、RELEASES、.blockmap。这是客户端自动更新机制的三件套——latest.yml 是更新清单体积几十 KBblockmap 配合增量更新让客户端只下载变动的块RELEASES 是发版素材。已装机的客户端会周期性地来拉这三个文件比对版本没新版本就什么也不干。它们不是人下载但它们在日志里和下载长得几乎一样全是 200全是 GET天天有、夜里也有。它们贡献了日均 PV 里非常可观的一块反过来看拉心跳的独立 IP 数就是这个软件装机后还在保持联网的存量盘子——这倒是意外收获心跳量是还有多少人在用完整下载量才是有多少新人在来两个数完全不是一回事混着看哪个都不对。3.3 扫描器PV 里混着一整支探测队再翻一遍路径分布第二个成分浮出来大量业务上根本不存在的地址在被反复请求——/.env、/.git/config、/wp-config.php、/actuator/env还有 phpmyadmin、备份文件猜解之类。这是互联网的基础噪声自动化扫描器拿着特征库满世界挨家敲门想捡配置泄露和框架漏洞。它们没有 UA 礼貌可言也不看来路请求干脆利落。这部分流量不去掉访问量就是虚的。验证方法很朴素看这些路径的请求量占 PV 的比例再看它们的 IP 分布——十几个地址贡献几十万次探测是常态和真人流量的分布模式完全不同。这里只陈述现象识别出来、从统计口径里剔掉至于防火墙层怎么处置是另一个话题本文不展开。3.4 一张表认全日志里的面孔把三类噪声加上下载相关的几类请求放到一起就是下面这张身份对照表。后来每次有人问这个数是怎么算的我都先甩这张表日志里长这样的请求看上去像实际身份统计处置/dl/setup-x.y.z.exe206多行同 IP 同路径多次下载一次断点续传/多线程分段按去重后的发起计绝不按行计/dl/setup-x.y.z.exe200字节约等于安装包大小一次下载完整下载这才是能作数的形态计入完成下载/dl/setup-x.y.z.exe?cb一串随机值200 整文件用户直连自家备份脚本绕缓存拉包按参数剔除latest.yml / RELEASES / .blockmap200体积很小页面访问/下载已装客户端的更新心跳单独计心跳不算下载不算 PV 主体/.env /.git/config /wp-config.php /actuator/env流量漏洞扫描器探测剔除单列robots.txt / favicon.ico访问浏览器和爬虫的后台行为剔除整站版本被顺序拉完UA 是 GPTBot、ClaudeBot用户活跃大模型爬虫抓训练素材剔除单列见第五节有了这张表任何一天的访问构成都能当场拆干净。拆的方法就是整行正则对 UA 打标先点名 GPTBot、ClaudeBot 这类大模型爬虫判据在第五节再用一张关键词表兜住其他机器——bot、spider、crawl、python、curl、scrapy、zgrab、headless一律先按机器处理再谈其他扫描路径和心跳文件在打标之前就该被截走。这套判据先记在脑子里第七节会把它整段固化进脚本这里不重复贴。两周分类下来的体感是真正值得叫访问的那部分比看板直觉小一个量级剩下的流量各有各的身份只是都没名字而已。日志不会骗人但日志也不会自报家门。四、下载量核算判据立错一切白搭4.1 206 分段一次下载能给你写出五行日志桌面软件的安装包有几十 MB下载过程里断点续传和分段并发太常见了浏览器自己就能把一个大文件拆成几路并行拉下载器更是如此中途断网恢复后再从断点续。每一种情况nginx 都老老实实地记一条 206 的日志——状态码 206 的意思是服务器接受了区间请求只发这一段它是成功响应不是错误。于是一个人下一次完整的安装日志里可能是五六个 206 行外加开头可能的几个探测行。我第一次核算下载量的时候用的就是图省事的判据路径匹配 .exe 且状态码以 2 开头数行。结果比实际大得离谱分段越多错得越多而且错得没规律——取决于用户的网络和下载器心情。判据必须换成能钉死完整拿到文件这一件事的形态。4.2 完整下载的硬判据200 且字节数过阈值安装包一个版本三十 MB 出头。完整下载在日志里的形态只有一种状态码 200整文件一次发出没有区间请求且 $11 不低于一个贴着包体大小的阈值。30MB 这个位置是这么定的比整包小一档、又远大于任何下了开头就断的字节数中间留出的安全余量足够宽同时它天然把心跳文件几十 KB隔离在外——就算哪天有人手滑把 latest.yml 也统计进去这个阈值也能兜住。# 完整下载200 字节数 30MB 安装包后缀剔除带绕缓存参数的自家备份# 再按 UA 把人机分开——GPTBot、ClaudeBot 也满足完整下载的物理判据必须点名剔除zcat-faccess.log.{14..1}.gz access.log\|awk-vh你的host字段值-vT31457280$1h { if ($10200 $11T $8 ~ /\.(exe|dmg)$/ $8 !~ /\?cb/) { ua tolower($0) if (ua ~ /gptbot|claudebot/) { ai; next } if (ua ~ /bot|spider|crawl|python|curl|headless/) { bot; next } d substr($5,2,11) if (!hit[d,$2,$8]) dl[d] # 同人同日同文件去重只算一次 } } END { printf AI爬虫完整拉包%d 其他机器完整拉包%d\n, ai, bot for (k in dl) printf %s 完成下载(去重)%d\n, k, dl[k] }|sort注意两个细节。其一$11 是发出字节200 的时候它约等于整个文件大小这正是判据的依据所以这个方案要求你的日志格式必须记录这个字段别在定制 log_format 的时候把它省了。其二“同人同日同文件去重”——一个人网络抖了重下一次、或者自己清缓存重来按业务意义只算一个完成下载要统计尝试又是另一套键那是发起数的口径放在下一小节。4.3 发起与完成漏斗得两段分开数只看完成会漏掉另一半信息有多少人点了下载但没拉完把 206 也算进来、按IP 文件做键去重就是发起过下载的人数再对照完成数一上一下才是个漏斗。206 的行不能直接数次数——回到 4.1 那个坑——必须按 (IP, 路径) 归并。做产品久了会发现下载类转化里发起和完成的差值比任何单一数字都诚实它同时反映了你的包有多大、用户的网络有多差、以及你的页面有没有把人引到错误的按钮上。4.4 回源参数与缓存日志口径的边界?cb 这个查询串参数要说一下备份脚本拉包的时候故意带一个每次不同的随机值让缓存认不出来、逼着请求回源取完整文件——这是运维脚本保证拿到最新整包的常规手段。副作用是这十次八次的备份拉取在日志里和真人下载一模一样全是 200 加整文件字节破绽只有一处那个参数。识别手段也就这一处按参数剔除。凡是服务器上的自己人发的请求都要给它留个可识别的记号否则它迟早混进你的统计给你难看。另一个方向的问题也要如实交代这套口径统计的是源站实际发出的完整传输。如果前面挂了 CDN用户命中边缘缓存的那些下载压根不会走到源站的日志里源站日志只记回源的那部分。所以下面所有数字的准确说法是源站视角的下载量不是全网真实下载量。要在日志里区分命中还是回源办法是定制 log_format 加一个缓存状态字段$upstream_cache_status加上之后本节的脚本按它再切一刀即可。口径可以简单但口径的边界必须写清楚——这是我看任何一份数据报告的头一条要求。五、单日 81 次下载的复盘AI 爬虫会伪装成增长5.1 尖峰那天差点开庆功14 天窗口里有一天按200 整包字节数出来的下载次数冲到 81。前一个工作日还是个位数。当时的第一反应是很正常的是不是哪篇文章被转了、有没有人在什么群里发了链接我甚至开始盘要不要给服务器加带宽。复盘动作后来固化成了口诀尖峰先别高兴UA 分组、IP 分组、时间分布三刀下去再下结论。三刀砍完81 次里的成分大致是这样80 次来自 GPTBot一个 ClaudeBot 的请求在当天露头它在整个窗口里一共出现过 2 次剩下那次是自家备份脚本——备份在整个两周里一共拉了 10 次赶巧那天也摊上了一回。把机器和自家流量全部剔除那天真人零完成把整个 14 天都洗完真人完成下载合起来约等于 1 次——运气不好就是 0。5.2 GPTBot 那 80 次都抓了什么UA 是 GPTBot 的请求路径分布非常有规律下载列表页里挂着的所有历史版本的安装包按版本号顺序一个不落每个都是 200 整包拉满。这不是行为像用户这是爬虫在做全站抓取只不过它抓的对象恰好是我们最大的静态文件。按一包 31MB 折算那一天它从源站拖走 2.4GB 还多——这不是用户增长这是带宽账单。ClaudeBot 的行为模式同款只是量小。AI 爬虫批量拉二进制包这件事是这两年新长出来的流量成分。它和传统爬虫的区别在于胃口传统爬虫主要抓页面文本成本可忽略大模型爬虫会把可下载文件一并纳入抓取范围一个安装包就是一个几百兆的素材库。识别倒不难——UA 字段规规矩矩写着 GPTBot、ClaudeBot不装人难的是心态这些请求的字节数、状态码和真人下载完全同构不点名剔除的统计软件以及不用功的人都会把它算进下载量。它伪装成增长的方式不是撒谎是长得太像。5.3 真尖峰和假尖峰的指纹不一样那次之后我给自己立了一条判别规则写进了第七节的脚本备注里真实传播带来的尖峰指纹是分散的——来源页多样、IP 分散在不同运营商和地域、UA 五花八门、时间上跟着某个事件起落机器造成的尖峰指纹是集中的——少量 IP、单一 UA、请求间隔均匀得像节拍器、抓取顺序严格跟随列表顺序。两种曲线在请求量维度上可以画出一模一样的山峰在结构维度上一个是烟花一个是探照灯。看量不动脑烟花和探照灯都叫大涨。六、数字背后这站到底是门什么生意把口径立正之后两周日志交代的现状其实挺扎心但值钱搜索引荐约等于零说明官网在这个阶段根本没吃到搜索红利直连流量里还有一大块是机器和心跳真人视角的日均 PV 是个很小的数。存量这边倒是很诚实心跳拉取的 IP 数每天都在说明装机在用的用户是稳定存在的软件本身没有被装完就弃。增量那边也诚实两周约 0 到 1 的完成下载说明官网基本不承担新用户获取。这就是这个站的真实定位它不是一个流量场它是交付和承接的基础设施——老客户升级靠它新客户从别的渠道来最后也经它落地。这个结论看板永远给不了你因为看板的默认单位是访问而访问这个单位本身就需要先审身份。日志不给你结论日志只给你原料结论是口径拧出来的。七、把口径固化成脚本复盘做到这一步所有判据已经齐了最后一步是把它们从我脑子里搬进机器里变成每天自动产出的一行报表。人是靠不住的今天忘剔备份明天把心跳当下载后天又忘了换 host 过滤器——口径这种东西不落进代码就一定会漂移。#!/usr/bin/env bash# traffic.sh —— 官网流量口径日报每天 00:10 由 cron 跑输出追加到报表文件# 口径版本 2026-09host 过滤 | 扫描剔除 | 心跳单列 | 完整下载200且30MB且非cb且非botset-euopipefailLOG_DIR${1:-/var/log/nginx}HOST_FIELD你的host字段值THRESHOLD$((30*1024*1024))cd$LOG_DIRzcat-faccess.log.{14..1}.gz access.log\|awk-vh$HOST_FIELD-vT$THRESHOLD $1 ! h { next } { d substr($5,2,11) } # 漏洞扫描探测单列计数不进 PV $8 ~ /^\/(\.env|\.git|wp-config|actuator|phpmyadmin)/ { scan[d]; next } # 客户端更新心跳按 IP 去重得当日在线存量 $8 ~ /latest\.yml|RELEASES|\.blockmap/ { if (!hb[d,$2]) hb_ip[d]; next } # 完整下载200 整包字节剔备份参数再剔 AI 爬虫和普通 bot $10200 $11T $8 ~ /\.(exe|dmg)$/ $8 !~ /\?cb/ { ua tolower($0) if (ua ~ /gptbot|claudebot/) { ai[d]; next } if (ua ~ /bot|spider|crawl|python|curl|headless/) { mdcm[d]; next } if (!dlv[d,$2,$8]) dl[d] next } # 剩余流量才配叫 PV { pv[d]; if (!uvs[d,$2]) uv[d] } END { for (k in pv) { split(k, a, SUBSEP); d a[1] printf %s pv%-6d ip%-4d 扫描%-5d 心跳IP%-4d AI拉包%-4d 机器拉包%-3d\n, d, pv[k], uv[k], scan[d]0, hb_ip[d]0, ai[d]0, mdcm[d]0 } } |sort配套一行 cron写进 crontab10 0 * * * /opt/tools/traffic.sh /var/log/nginx /var/log/site_daily.txt 21脚本固化下来之后报表字段也就五六个但每一个的口径都经过这一路的毒打指标判据回答的业务问题真人 PV / UV剔除扫描、心跳、备份、机器 UA 后的请求数与去重 IP这站今天到底有没有人来完整下载去重200 且字节 30MB 且无 cb 参数且非 bot按日IP文件去重今天有几个新用户拿到了软件下载发起含分段安装包路径命中按 IP文件归并206 计入有多少人点了但没下完心跳在线存量心跳三件套按 IP 去重装过的人还有多少在用AI 爬虫拉包量完整下载判据 爬虫 UA 点名带宽被谁吃掉了有件小事值得记在最后这套口径的每个阈值和每条正则都对应一次交过的学费——30MB 对应 206 分段那次的离谱虚增cb 参数对应备份脚本混进下载的尴尬GPTBot 点名对应那场 81 次的假庆功。日志里的数据从来不需要美化需要的是审问每一行都要先问一句你是谁再决定它进不进哪个桶。口径是审出来的不是算出来的。这套东西没有平台没有依赖备份就是 gzip 的十几个文件——流量到底是不是自己的看一眼机器上有没有这份原始日志比看任何看板都准。
返回列表