PV 涨了三倍,全是爬虫:两百行代码搭一套看得懂人的网站流量监控

写于 2026-11-03

上线第一天,我在后台看到「今日独立 IP 58 个」,兴奋了半小时。

第二天把 IP 换算成城市才看清:其中 33 个来自同一个机房(United States · Virginia),UA 全都自称 Chrome、行为模式一模一样——那是 Bing 的爬虫换着 IP 在抓我的站,真人只有三十来个。

所以这篇不是「教你粘一段统计代码」,而是把「有没有人看、谁在看、看什么」这件事一次做对:采集 → 存储 → 聚合 → 阅读。全部代码两百多行,一个人一个周末能跑完,数据 100% 留在自己机器上。


零、一句话结论

流量分析不是在页面上装个计数器就完事,它是一条四段流水线:采集 → 存储 → 聚合 → 阅读。

大多数人只做了第一段(装个统计代码),然后就再也没打开过后台。真正有用的不是「今天 87 次访问」这个数字,而是它能回答的五个问题:

  1. 有没有人来(趋势:在涨还是在掉)
  2. 谁在来(新访客还是回头客、真人还是爬虫、哪个城市)
  3. 从哪来(搜索引擎 / 公众号 / 朋友转发 / 直接输入)
  4. 来了看什么(哪几篇文章、哪个课程页是真正被读的)
  5. 什么时候来(决定你什么时候发新内容最划算)

装统计工具之前先想清楚这五个问题,能省掉后面 80% 的无用功。


一、先搞懂四个词,后面全靠它

把网站想成一家街边小店,门口装了个计数器:

术语 人话 小店类比 常见误区
PV(Page View,页面浏览量) 页面被打开的总次数 一天里进出店门的总人次数 同一个人刷新 10 次就是 10 PV,别当成 10 个人
UV(Unique Visitor,独立访客) 有多少「不同的人」来过 一天里进店的不同顾客数 同一台手机换浏览器、清缓存,会被算成新访客(虚高)
独立 IP 有多少个不同的网络地址 有多少个不同的住址来过 一家公司/一个小区共用一个出口 IP,10 个人只看得到 1 个 IP
会话 / 访问次数(Session) 一个人进来逛了几趟 同一个顾客今天来了 3 次 静置超过 30 分钟算新会话

再加两个「质量」指标,只有前端埋点才拿得到,新手可以先不管:

  • 跳出率:进来只看了一页就走的人占比。高不一定是坏事——如果那一页就是答案(比如一篇查资料的笔记),看完就走很正常。
  • 停留时长:在页面上待了多久。服务端日志看不到这个值(它只知道你请求了页面),必须靠浏览器里跑 JS 上报。

记住一句话:PV 看热度,UV 看人数,独立 IP 看渠道广度,会话看黏性。 四个数字一起看才有意义,单看一个都会被误导。


二、三条技术路线:你到底该选哪条

做流量分析只有三种做法,没有第四种。

路线 A:用现成的第三方统计(最快,5 分钟)

在页面 <head> 里粘一段 JS,数据跑到人家的服务器,你登录人家后台看报表。

  • 国外:Google Analytics 4(GA4,功能最全但界面劝退)、Plausible、Umami Cloud、Cloudflare Web Analytics(免费、无 Cookie、只给基础指标)
  • 国内:百度统计(免费、中文、对国内网络友好)、友盟+、51LA

优点:零开发,功能齐全,有历史对比、来源分析、转化漏斗。 缺点:① 数据在别人手里;② 会被广告拦截插件挡掉一部分(普遍 10%~30%,技术人群更高);③ 纯前端统计拿不到「访问了但页面没渲染完」的那部分;④ 国内已备案的网站用境外统计服务,存在数据出境与合规风险,这点很多教程不提。

适合:微信公众号导流的落地页、不想碰代码的人、需要看广告投放 ROI 的人。

路线 B:分析服务器访问日志(有服务器就用这条)

每个 Web 服务器(Nginx / Apache / Caddy)默认就会把每一次请求写进一个文本文件,一行一条。这是最原始也最真实的数据源——它连爬虫、404、静态资源都记着,而且不依赖浏览器、不怕拦截插件。

看日志有现成工具,一条命令出报表:

# 装一次,之后随时看
apt install goaccess          # 或 brew install goaccess

# 直接对日志文件生成一份自带样式的 HTML 报表,浏览器打开就能看
goaccess /var/log/nginx/access.log \
  --log-format=COMBINED \
  --output=/tmp/report.html --html-prefix=/tmp

GoAccess 能给你:PV、UV、独立 IP、访问时段、来源、被访问最多的 URL、状态码分布、爬虫识别、甚至国家/城市(要配 IP 库)。

优点:免费、数据在本地、极准(服务端看到什么就是什么)。 缺点:① UV 只能靠 IP + UA 粗略估算(日志里没有 Cookie ID);② 日志文件会越滚越大,要做切割和保留策略;③ 云服务商前面套了 CDN/负载均衡时,日志里全是代理 IP,得改日志格式带上真实 IP。

适合:有自己的服务器/VPS、想要「精确 + 私有」的人。

路线 C:自己写一层采集中间件(最灵活,也最有意思)

在 Web 框架里加一个中间件(middleware,意思是「每个请求进出的必经关卡」),请求进来时记一条结构化数据,自己决定记什么、存哪、怎么算、怎么展示。

我自己的站走的就是这条(Django + 一天一个 JSONL 文件),因为它同时满足三件事:数据 100% 在自己机器上、能记业务字段(比如这个请求属于哪个课程栏目)、完全不用第三方 JS。

优点:完全可控,指标可以按自己的业务定义;不受拦截插件影响;可以给接口、脚本、下载都算进去。 缺点:要写代码、要自己做界面、要自己维护。

适合:程序员、想长期拥有数据的人、以及有「栏目/课程/专题」这类业务维度想看的人。

一张表选完

你的情况 选
纯静态站(GitHub Pages / Cloudflare Pages / Vercel) Cloudflare Web Analytics 或 Umami Cloud(路线 A)
WordPress / 有面板的虚拟主机 百度统计 / 插件(A)或 cPanel 自带的 AWStats(B)
有自己的 VPS、会一点 Linux GoAccess 看日志(B),成本最低
已备案 + 面向国内用户 百度统计 或自建(A 的国内服务 / C)
会写代码、想要能按业务维度切数据 自建中间件(C)
团队、要投放、要转化漏斗 GA4 / Plausible / Matomo(A)

别做的事情:同时装三套统计然后对不上数。选一套,信它,看趋势。


三、实战:自己动手搭一套(以我的站为例,步骤可照抄)

我的情况:一台群晖 NAS 上跑 Docker,Django 3.2 的小站,日访问几十到几百,跑着课程页和笔记页。这套方案 2026-11-02 上线,一天数据量 20KB 左右。

第 1 步:采集——为什么我不用纯前端 JS

纯前端统计(在页面里 fetch 一个像素)有两个硬伤:广告拦截器会挡、禁用 JS 的爬虫根本不上报。

所以我选了服务端采集:请求进来时记一条。伪代码就十几行:

class AnalyticsMiddleware:
    def __call__(self, request):
        started = time.time()
        response = self.get_response(request)      # 先把页面正常返回
        try:
            store.append({                          # 再异步/旁路记录,异常全吞
                't': int(started),                  # 时间戳
                'p': request.path,                  # 哪个页面
                'ip': client_ip(request),           # 真实 IP(见第 2 步)
                'ua': request.META.get('HTTP_USER_AGENT', ''),
                'st': response.status_code,
                'ms': int((time.time() - started) * 1000),   # 页面快不快
            })
        except Exception:
            pass        # ★ 统计出任何问题,都不能影响用户看页面
        return response

四条纪律,比代码本身重要:

  1. 记录必须在响应之后,而且异常必须吞掉——统计写失败不能让用户看到 500。
  2. 只记录 GET 的页面:静态资源(css/js/图片)不算 PV,POST 请求也不算。
  3. 别在请求里做重活:写文件用 O_APPEND 单次 write(小于 4KB 是原子的,多线程安全),不要在请求里连数据库做 INSERT。
  4. 别记录敏感内容:密码、token、query 里的隐私参数,能不记就不记。

第 2 步:拿到「真实 IP」——这是第一个大坑

如果你在服务器前面用了 Nginx 反向代理、CDN、Docker 端口映射,那么框架拿到的 REMOTE_ADDR 是代理机地址,不是访客地址。你会看到几千次访问全来自同一个 IP,然后误以为是被爬虫刷了。

正确处理:按优先级从这些头里取第一个合法的公网 IP:

X-Forwarded-For  →  X-Real-IP  →  CF-Connecting-IP  →  REMOTE_ADDR

细节:

  • X-Forwarded-For 可能是 "真实IP, 代理1, 代理2",取第一个。
  • 要剥掉可能的端口号(1.2.3.4:5678)。
  • 要过滤内网地址(10.x、192.168.x、172.16~31.x、127.0.0.1),否则你的后台里会出现一堆「内网访客」。
  • 除了真实 IP,我还额外记一个 REMOTE_ADDR 字段——它就是用来对账的:如果两个值一样、而且都是代理机,说明你的 Nginx 没把 X-Forwarded-For 传下来,一行配置就能修:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;

我上线第一天就用这个方法验证了:记录里 ip=183.6.73.138(真实访客)、ra=172.25.0.1(Docker 网关),说明透传正常,不用改服务器。

第 3 步:数据存哪里——小站千万别急着上数据库

新手第一反应是「建张表存每次访问」。日访问几百的小站这么做有两个副作用:每次请求都写库(写放大、锁竞争,还拖慢页面),而且很快就要处理分表、清理、备份。

我的做法:一天一个文本文件,一行一条 JSON(这个格式叫 JSONL)。

/app/analytics_data/2026-11-02.jsonl
/app/analytics_data/2026-11-03.jsonl
{"t":1793663535,"p":"/courses/","s":"courses","ip":"183.6.73.138","ua":"Mozilla/5.0 ...","st":200,"ms":142,"v":"a1b2c3","b":0}

好处很实在:

  • 追加写,永远不锁表;坏一行不毁全库;grep/python 随便处理。
  • 备份 = 复制文件;迁移 = 复制文件。
  • 保留策略 = 按文件名日期删老文件(我设 180 天)。
  • 攒到 300MB 再考虑迁 ClickHouse 也不迟。

什么时候该上数据库:日 PV 超过几十万、要按几十个维度任意切片、要多人并发看报表——到那时你也不会看这篇文章了:直接用 Plausible / Matomo / ClickHouse。

第 4 步:把流水账算成指标

原始记录只是一行行流水,要聚合成人能看的东西。用标准库就够了(不需要 pandas):

records = list(iter_records(days=7))                    # 读最近 7 天

pv = len(records)                                       # 总浏览量
uv = len({r['v'] for r in records})                     # 按访客 ID 去重
ips = len({r['ip'] for r in records})                   # 按 IP 去重

hourly = [0] * 24                                       # 时段分布
for r in records:
    hourly[localtime(r['t']).hour] += 1

per_page = Counter(r['p'] for r in records)             # 哪个页面被看得最多

四个提醒:

  • 访客 ID 从哪来:第一次访问时下发一个随机 Cookie(vid,一年有效),之后每个请求带上它。这就是 UV 的来源,换设备/清缓存会变成新访客——这是 Cookie 方案的天花板,接受它。
  • 时区统一:服务器可能是 UTC,你的读者在东八区。存时间戳(epoch),展示时统一转成本地时区,否则「凌晨 3 点的高峰」会骗你。
  • 分类要在采集时打标签:我的站有「飞云课程 / 飞云阅读 / IT技术 / AI资讯」四个栏目,每条记录存一个 s 字段记着属于哪个栏目。等你想按栏目拆数据时,这一列就是关键。
  • 爬虫要单独标出来:UA 里含 bot / spider / crawl / curl / python-requests 等词的一律标 b=1,看板里「真人 / 爬虫」分开算。不标的话你的数据一半是噪音。

第 5 步:展示——为什么我用内联 SVG 而不是 Chart.js

后台页就是个普通网页。图表只有两种:柱状图(每日 PV)和折线(UV 趋势)、再加一个 24 小时柱状图。这种量级直接在服务端把坐标算好,输出 <rect> 和 <polyline> 就够了:

  • 不引任何前端图表库(省 200KB JS、省一个外部依赖、省一次加载失败风险);
  • 不写 JS,浏览器禁用脚本也能看;
  • 缺点是交互弱(只有 <title> 悬浮提示)。个人看板不需要更多。

三个图表踩过的坑:

  • SVG 上加 preserveAspectRatio="none" 会让文字横向压扁,别加。
  • 30 天的日期标签必须抽稀(每隔 n/10 显示一个),否则挤成一坨黑线。
  • 单位别乱:柱子的高度是「PV ÷ 纵轴最大值 × 绘图区高度」,纵轴要对齐「好看的数量级」(5/10/20/50/100…),否则柱子永远贴顶或贴底。

第 6 步:只有自己能看

流量数据会暴露「谁在什么时候看你的站」,属于隐私数据。三件事必做:

  1. 加登录:后台页要求登录 + 管理员身份(我的是 is_staff 校验),非管理员返回 403。
  2. 加 noindex:别让搜索引擎收录 /analytics/ 这类页面。
  3. URL 别猜得到:不要放 /stats/ 这种一眼可见的路径(更彻底的做法是换随机前缀 + 登录双保险)。

四、IP 归属地:让「谁在来」变得具体

只看到 183.6.73.138 是没感觉的。加上城市就完全不一样了:广东省 广州市 · 电信。

有两条路:

方案 代表 优点 缺点
在线 API ip-api.com、高德/腾讯地图 IP 定位 API 不用下数据文件、城市库较新 每次查询都要出网,有频率限制、要 key、把访客 IP 发给第三方
离线库 ip2region(11MB 中文库)、MaxMind GeoLite2 零网络请求、微秒级、不外发 IP、离线可跑 要自己下数据文件、一年更新几次

我选了离线库 ip2region,理由:站点在国内、不想把访客 IP 发给任何人、也不想因为第三方接口挂了而报表空白。

实际使用体感(可复用的经验):

  • 数据库文件约 11MB,整库读进内存后一次查询约 23 微秒,几千个 IP 瞬间算完。
  • 中文结果直接就是「省份 / 城市 / 运营商」三段,下面是真实查询结果:
114.114.114.114  →  江苏省 南京市
183.6.73.138     →  广东省 广州市 · 电信
223.5.5.5        →  浙江省 杭州市 · 阿里
8.8.8.8          →  United States · California
43.134.54.246    →  香港特别行政区
  • 历史数据也能补:城市是在「展示时」算的,不是在采集时存的。所以库装上之后,之前所有记录立刻都有城市,不用回填、不用改数据结构。
  • 我拿官方 Python 实现跟自己的实现对 3015 个 IP 交叉验证,结果零差异——换你自己实现时,这是个很值得做的验收动作。
  • 精度预期要放低:机房 IP 显示的是机房所在地(我站上「United States · Virginia Boydton」那 36 次访问,全是 Bing 爬虫的机房);手机网络常常漂到省会;境外只到国家。所以城市是「趋势参考」,不是「精确门牌」。

五、十个真实踩过的坑(能帮你省一整晚)

  1. 真实 IP 全是代理机:反代后面 REMOTE_ADDR 不可信,先查 X-Forwarded-For(见第 2 步)。
  2. 爬虫不标出来:不标 UA 词表,你会对着「访问量暴涨」空欢喜。搜索引擎、AI 抓取器、监控探针全在里面。
  3. UV 当人数用:Cookie 去重,换个浏览器就是新访客。日活 100 可能只有 40 个人。
  4. 时区错 8 小时:UTC 存的,本地看的,高峰时段全错位。
  5. 把静态资源算进 PV:一个页面加载会请求 20 个图片/CSS,如果都记,PV 会虚高 20 倍。只记 HTML 页面。
  6. 统计代码拖垮页面:写日志/入库要放到旁路、要吞异常。我见过有人统计挂了导致整站 500。
  7. CDN 缓存吃掉访问:如果页面被 CDN 缓存,请求根本没到你的服务器,服务端统计会漏计。这种站要么看 CDN 自己的报表,要么用前端统计。
  8. 广告拦截器:前端统计在技术人群里能漏 30%。所以服务端统计的数字通常比 GA 高,这是正常的,别对账对到怀疑人生。
  9. 改样式后忘记重启:如果你的框架开了模板缓存(生产环境默认开),改完页面要重启服务才生效——不然你会以为代码没生效。
  10. 忘了清理:一天一个文件如果不删,两年后磁盘满。按文件名日期保留 90/180 天,凌晨低峰删。

六、有了数据,到底怎么「读」它

这是全文最重要的一节。数据不是拿来看的,是拿来做决定的。

第一层:看趋势(有没有人在来)

只看两件事:7 日 PV 是涨是跌、UV 和 PV 的比值健康吗。

  • UV 稳定而 PV 在涨 = 老访客看得更多了(内容变好)。
  • PV 涨但 UV 没涨、爬虫比例同时猛增 = 是被抓取,不是被喜欢。
  • UV 和 PV 一起跌 = 内容停更 / 渠道断了,去看「来源」。

第二层:看结构(人从哪来、看什么)

  • 来源:如果 90% 是「直接访问」,说明你没有外部渠道,流量全靠自己的公众号/朋友圈导;搜索引擎占比上升是好信号。
  • 页面排行:找出「被真人大量阅读」的 3 个页面,然后在这 3 个页面上加内链指向你希望他们去的地方(比如课程页、订阅入口)。这是转化率最高的一步。
  • 栏目对比:我站四个栏目里,「飞云阅读」的 PV 暂时压过「飞云课程」(42 比 24,样本只有两天,趋势还得再看一周)——如果这个结构稳定,新内容就该往读书笔记那边倾斜,而不是凭感觉决定。

第三层:看个体(哪些访客值得关注)

  • 同一个 IP 一天来 20 次、每次只停 1 秒、UA 是 curl:这是抓取,不是粉丝。
  • 同一个访客 ID 反复回访某个课程页:这是高意向用户,值得在页面上加一个「有问题加微信」的入口。
  • 来自某个城市的访客异常集中:可能是学校/公司集体访问(我的站就出现过「内蒙古 呼和浩特市 · 移动」单一 IP 25 次,一看就是有人在批量打开页面)。

一个反例,提醒你别自我感动

「PV 涨了三倍」是最容易让人误判的一句话。我第一次抓到这种「暴涨」,其实是同一批机房 IP 换了不同地址在抓同一个页面——不做爬虫标记、不看城市,你会把它当成「内容火了」,然后按错误的信号去做下一期内容。

流量分析的价值不是让你开心,是让你不被数字骗。


七、不同规模,方案不一样

你的规模 推荐 理由
个人博客,日 PV < 1000 路线 A(Cloudflare/Umami/百度统计)或 GoAccess 看日志 半小时搞定,别自己造轮子
个人站但要按业务维度切(课程/栏目/专题) 路线 C(自建中间件,就像我这套) 只有自建才能定义自己的「栏目」维度
小团队,要投放和转化 GA4 / Plausible / Matomo 漏斗、归因、UTM 这些别人已经做好了
日 PV 十万以上 ClickHouse + Plausible/Matomo 集群,或买商业服务 文件方案会撑不住

别升级得太早:文件方案在日 PV 一两千以内毫无压力。我实测过两个量级——140 条真实记录聚合一次 9.8 毫秒,1 万条记录(30 天 × 每天 334 条)359 毫秒,也就是「一万条以内页面照样秒开」。真正的拐点是「需要按任意维度实时切片」和「多人并发看报表」。


八、30 分钟最小可行版(照着做就行)

按顺序做,每一步都能独立验证:

  1. 先看清事实(5 分钟):随便挑一天,数一下你的服务器日志有多少行、多少不同 IP。命令: bash wc -l access.log # 总请求数 awk '{print $1}' access.log | sort -u | wc -l # 不同 IP 数
  2. 选一条路线(1 分钟):纯静态 → Cloudflare Web Analytics;有服务器 → GoAccess;有代码能力 → 自建中间件。
  3. 装上并跑通(10 分钟):先把「今天有多少 PV / 多少 UV」算对,其他指标全部先不管。
  4. 加真实 IP 修正(5 分钟):确认反代场景下拿的是访客 IP(第 2 步的 ip vs ra 对账法)。
  5. 加一个业务维度(5 分钟):给你的页面分个类(栏目/系列/课程),每条记录带上这个标签。没这列,数据永远只能看总量。
  6. 加登录 + noindex(2 分钟):数据只给自己看。
  7. 给自己定一个「每周看一次」的闹钟(1 分钟):统计最容易死在「装完不看」。

之后再慢慢加:城市、爬虫过滤、页面排行、时段分布、转化事件。


九、合规与隐私(别忽略,尤其国内站)

  • Cookie:如果你只用它做统计去重、不涉及广告追踪,一般属于「必要 Cookie」范畴,但仍建议在隐私说明里写清用途。
  • 境外统计服务:站已备案、用户在国内时,把访客 IP/行为数据发到境外服务器存在合规风险。这也是我选服务端自建 + 离线 IP 库的原因之一。
  • 日志脱敏:URL 的 query 里常带手机号、邮箱、token,记之前要截断或过滤。
  • 数据保留期:不用的数据别永久留着,90/180 天足够看趋势。
  • 别做「指纹」追踪:用 Cookie ID 已经够用,做浏览器指纹去识别「同一个人」既不必要也不友好。

十、术语小词典

词 一句话解释
PV 页面被打开的总次数(一个人刷新两次算两次)
UV 去重后的访客数(靠 Cookie 认人,换设备算新人)
独立 IP 去重后的网络地址数(一家公司可能只有一个)
会话 一次连续浏览,静置 30 分钟算新会话
跳出率 只看一页就走的人的比例
Referer 浏览器告诉服务器「我是从哪个页面点过来的」
UA(User-Agent) 浏览器自报的身份字符串,也是识别爬虫的主要依据
反代(Nginx/反向代理) 站在你网站前面的门卫,会把访客 IP 藏起来
X-Forwarded-For 门卫帮你传来的「原始访客 IP」小纸条
JSONL 一行一个 JSON 的文本格式,适合追加写
中间件(middleware) 每个请求进出都要经过的一段代码
离线 IP 库 把「IP → 城市」的对照表下载到本地,查询不出网

十一、我的实际结果(2026-11-03,真实数据)

访客流量分析后台:关键指标、各栏目对比与每日趋势

▲ 我自己的统计后台:第一排是 PV / UV / 独立 IP / 真人·爬虫 / 覆盖地区,下面按栏目拆开,再下面是每日趋势。

写这篇文章的这两天,站上一共攒了 170 次访问。按地区拆开是这样(数字每小时都在涨,你看到的时候可能又变了):

PV 170 / UV 145 / 独立 IP 79 / 真人 120 · 爬虫 50 / 覆盖地区 14 个(境外 3 个)

广东省 广州市 · 电信                PV 59  UV 37  IP 1   34.7%
United States · Virginia Boydton  PV 38  UV 38  IP 35  22.4%   ← bingbot 机房
香港特别行政区                      PV 27  UV 26  IP 25  15.9%
内蒙古 呼和浩特市 · 移动             PV 25  UV 25  IP 1   14.7%
上海 / 郑州 / 新加坡 / 赣州 / 杭州 / 北京 / 成都 / 南京 / 澳洲 Newcastle ...

从这几条里能直接读出结论:

  1. 广东广州那个 IP 是我自己(59 次,curl UA,被标成「爬虫」很正常)。
  2. 美国那一坨是 Bing 抓取:35 个 IP 各来一次、38 次里 30 次标着爬虫——这不是读者。把它从分母里去掉,真人只剩几十个。
  3. 香港 25 个 IP、各访问 1 次、UA 全是 iPhone Safari:典型的「分享出去被点开,看一页就走」,说明分享文案还能再优化。
  4. 内蒙古那个单一 IP 来了 25 次:有人(或某个程序)在批量打开页面,值得留意是不是被镜像、被采集。
  5. 「未知」那 3 次是内网/保留地址(我自己的探针),所以它被单独列出来、不混进城市统计——这就是为什么要先过滤 is_global。

这就是「有维度的数据」和「一个总数」的区别。


十二、最后一句

流量分析做到最后,你会发现它其实只服务一件事:把「我觉得」换成「我看到」。

  • 我觉得这篇文章写得好 → 数据说:它 7 天只有 3 个 UV。
  • 我觉得课程页没人看 → 数据说:它是 PV 第二的页面,只是没人往下滚。
  • 我觉得访问量涨了 → 数据说:涨的全是爬虫。

先能回答「今天到底有几个人来、他们是谁、看了什么」,再谈增长。装工具是半小时的事,养成每周看一次的习惯,才是这件事的全部难度。