PV 涨了三倍,全是爬虫:两百行代码搭一套看得懂人的网站流量监控
上线第一天,我在后台看到「今日独立 IP 58 个」,兴奋了半小时。
第二天把 IP 换算成城市才看清:其中 33 个来自同一个机房(United States · Virginia),UA 全都自称 Chrome、行为模式一模一样——那是 Bing 的爬虫换着 IP 在抓我的站,真人只有三十来个。
所以这篇不是「教你粘一段统计代码」,而是把「有没有人看、谁在看、看什么」这件事一次做对:采集 → 存储 → 聚合 → 阅读。全部代码两百多行,一个人一个周末能跑完,数据 100% 留在自己机器上。
零、一句话结论
流量分析不是在页面上装个计数器就完事,它是一条四段流水线:采集 → 存储 → 聚合 → 阅读。
大多数人只做了第一段(装个统计代码),然后就再也没打开过后台。真正有用的不是「今天 87 次访问」这个数字,而是它能回答的五个问题:
- 有没有人来(趋势:在涨还是在掉)
- 谁在来(新访客还是回头客、真人还是爬虫、哪个城市)
- 从哪来(搜索引擎 / 公众号 / 朋友转发 / 直接输入)
- 来了看什么(哪几篇文章、哪个课程页是真正被读的)
- 什么时候来(决定你什么时候发新内容最划算)
装统计工具之前先想清楚这五个问题,能省掉后面 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
四条纪律,比代码本身重要:
- 记录必须在响应之后,而且异常必须吞掉——统计写失败不能让用户看到 500。
- 只记录 GET 的页面:静态资源(css/js/图片)不算 PV,POST 请求也不算。
- 别在请求里做重活:写文件用
O_APPEND单次write(小于 4KB 是原子的,多线程安全),不要在请求里连数据库做 INSERT。 - 别记录敏感内容:密码、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 步:只有自己能看
流量数据会暴露「谁在什么时候看你的站」,属于隐私数据。三件事必做:
- 加登录:后台页要求登录 + 管理员身份(我的是
is_staff校验),非管理员返回 403。 - 加
noindex:别让搜索引擎收录/analytics/这类页面。 - 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 爬虫的机房);手机网络常常漂到省会;境外只到国家。所以城市是「趋势参考」,不是「精确门牌」。
五、十个真实踩过的坑(能帮你省一整晚)
- 真实 IP 全是代理机:反代后面
REMOTE_ADDR不可信,先查X-Forwarded-For(见第 2 步)。 - 爬虫不标出来:不标 UA 词表,你会对着「访问量暴涨」空欢喜。搜索引擎、AI 抓取器、监控探针全在里面。
- UV 当人数用:Cookie 去重,换个浏览器就是新访客。日活 100 可能只有 40 个人。
- 时区错 8 小时:UTC 存的,本地看的,高峰时段全错位。
- 把静态资源算进 PV:一个页面加载会请求 20 个图片/CSS,如果都记,PV 会虚高 20 倍。只记 HTML 页面。
- 统计代码拖垮页面:写日志/入库要放到旁路、要吞异常。我见过有人统计挂了导致整站 500。
- CDN 缓存吃掉访问:如果页面被 CDN 缓存,请求根本没到你的服务器,服务端统计会漏计。这种站要么看 CDN 自己的报表,要么用前端统计。
- 广告拦截器:前端统计在技术人群里能漏 30%。所以服务端统计的数字通常比 GA 高,这是正常的,别对账对到怀疑人生。
- 改样式后忘记重启:如果你的框架开了模板缓存(生产环境默认开),改完页面要重启服务才生效——不然你会以为代码没生效。
- 忘了清理:一天一个文件如果不删,两年后磁盘满。按文件名日期保留 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 分钟最小可行版(照着做就行)
按顺序做,每一步都能独立验证:
- 先看清事实(5 分钟):随便挑一天,数一下你的服务器日志有多少行、多少不同 IP。命令:
bash wc -l access.log # 总请求数 awk '{print $1}' access.log | sort -u | wc -l # 不同 IP 数 - 选一条路线(1 分钟):纯静态 → Cloudflare Web Analytics;有服务器 → GoAccess;有代码能力 → 自建中间件。
- 装上并跑通(10 分钟):先把「今天有多少 PV / 多少 UV」算对,其他指标全部先不管。
- 加真实 IP 修正(5 分钟):确认反代场景下拿的是访客 IP(第 2 步的
ipvsra对账法)。 - 加一个业务维度(5 分钟):给你的页面分个类(栏目/系列/课程),每条记录带上这个标签。没这列,数据永远只能看总量。
- 加登录 +
noindex(2 分钟):数据只给自己看。 - 给自己定一个「每周看一次」的闹钟(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 ...
从这几条里能直接读出结论:
- 广东广州那个 IP 是我自己(59 次,
curlUA,被标成「爬虫」很正常)。 - 美国那一坨是 Bing 抓取:35 个 IP 各来一次、38 次里 30 次标着爬虫——这不是读者。把它从分母里去掉,真人只剩几十个。
- 香港 25 个 IP、各访问 1 次、UA 全是 iPhone Safari:典型的「分享出去被点开,看一页就走」,说明分享文案还能再优化。
- 内蒙古那个单一 IP 来了 25 次:有人(或某个程序)在批量打开页面,值得留意是不是被镜像、被采集。
- 「未知」那 3 次是内网/保留地址(我自己的探针),所以它被单独列出来、不混进城市统计——这就是为什么要先过滤
is_global。
这就是「有维度的数据」和「一个总数」的区别。
十二、最后一句
流量分析做到最后,你会发现它其实只服务一件事:把「我觉得」换成「我看到」。
- 我觉得这篇文章写得好 → 数据说:它 7 天只有 3 个 UV。
- 我觉得课程页没人看 → 数据说:它是 PV 第二的页面,只是没人往下滚。
- 我觉得访问量涨了 → 数据说:涨的全是爬虫。
先能回答「今天到底有几个人来、他们是谁、看了什么」,再谈增长。装工具是半小时的事,养成每周看一次的习惯,才是这件事的全部难度。