独立站后台一般只告诉你两件事:这个页面来了多少人,这个月收到几条询盘。它不会告诉你这两件事之间的关系,也不会告诉你钱该往哪个页面投。
访问日志里其实有答案。把日志和询盘记录按落地页对齐,就能算出每个页面的询盘密度,然后一眼看到哪些页面在空转。
下面这套脚本大约六十行,只用标准库,跑在 Python 3.8 以上。
准备三样东西
第一样是 Nginx 或 Apache 的访问日志,combined 格式。默认配置下长这样。
203.0.113.47 - - [22/Oct/2026:14:03:11 +0800] "GET /products/stainless-flange HTTP/1.1" 200 5120 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
第二样是询盘记录,导成 CSV,至少要有落地页这一列。多数建站工具的表单后台能导,字段名不一定叫 path,统一成 path 就行。
path,company,date
/contact,buyer1,2026-10-03
/products/,buyer2,2026-10-07
第三样是一个口径决定:询盘算在哪个页面上。表单提交发生在 /contact,但客户是在产品页看完才点的。这套脚本按表单页面统计,也就是最后一步。要按来源页统计,得另外记 referer,那是另一件事。
解析日志:三个过滤条件
原始日志不能直接用,至少要做三件事。
LOG_RE = re.compile(
r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] '
r'"(?P<method>[A-Z]+) (?P<path>\S+)[^"]*" '
r'(?P<status>\d{3}) (?P<size>\S+) '
r'"[^"]*" "(?P<ua>[^"]*)"'
)
BOT_RE = re.compile(r'bot|spider|crawl|slurp|facebookexternalhit|headless', re.I)
正则要把 UA 单独抓出来,别拿整行去匹配 bot 关键词。整行匹配会把 referer 里带「robot」字样的正常请求也误杀,我第一版就是这么写的,少统计了大约一成的真实访问。
三个过滤条件:状态码只留 200,UA 命中 bot 关键词的丢掉,静态资源后缀的丢掉。第三条容易被忘,一个页面带二十张图,日志里就是二十一条记录,全算进去 PV 会虚高好几倍。
def parse_log(path):
rows = []
with open(path, encoding='utf-8', errors='replace') as f:
for line in f:
m = LOG_RE.match(line)
if not m:
continue
d = m.groupdict()
if d['status'] != '200':
continue
if BOT_RE.search(d['ua']):
continue
clean = norm_path(d['path'])
if clean.endswith(STATIC_EXT):
continue
rows.append((d['ip'], clean))
return rows
聚合与计算
按路径聚合 PV 和独立 IP,然后和询盘数对齐,算出每千次访问的询盘数。用千次而不是百分比,是因为小站的分母太小,百分比会显示成一堆零点几,看不清差别。
def build_report(pv, ips, inq, min_pv=250):
out = []
for p, n in pv.items():
if n < min_pv:
continue
q = inq.get(p, 0)
rate = q / n * 1000
out.append((rate, n, len(ips[p]), q, p))
out.sort()
return out
min_pv 这个阈值是必须的。访问量太小的页面,一次询盘就能把密度拉到很高,排序会失真。样本只有几百 PV 的站,阈值给 50 到 100;一天几千 PV 的站,给 250 以上。
排序用升序。排在前面的是「来了不少人但没询盘」的页面,那才是要看的。
跑一遍样本数据
我自己造了一份样本日志,7295 行,混了 600 条爬虫和 120 条 404,加上 22 条真实询盘记录。写完先跑了一遍,两个版本的输出逐条对过。阈值给 250。
有效请求 4975 条,页面 6 个,参与统计 6 个(PV>=250)
path PV UV 询盘 每千次询盘
/products/stainless-flange 405 203 0 0.00
/about 620 224 0 0.00
/ 2140 240 6 2.80
/products 830 236 3 3.61
/blog/dnv-flange-standard 530 213 2 3.77
/contact 450 205 11 24.44
第一行就是答案。一个产品详情页,一天 405 次访问、203 个独立 IP,零询盘。首页的询盘密度是 2.8,这个页面是 0。
这类页面通常有三种情况,处理方式完全不一样。
**第一种,页面承接不了。**客户搜具体规格进来,页面上只有一张白底图和一句欢迎咨询。他要的尺寸表、材质、交期、检测记录一样没有。这种页面最值得改,改完见效也最快。
**第二种,词和页面对不上。**页面被一个泛词带流量,来的都是同行和比价的。日志看不出这一点,要去看搜索词报告,那是另一个数据源。
**第三种,这个页面本来就不该承担转化。**像 /about 那种,620 PV 零询盘很正常,客户看完公司介绍不会立刻下单,他会回产品页。这类页面不要拿询盘密度去考核,否则你会把有用的页面优化坏。
把三种分开之后,广告该投哪儿也就清楚了。第一种先改再投,第二种先换词,第三种根本不投。
一个把最重要页面藏起来的坑
上面那个结果能出来,是因为我先修了一个 bug。原始版本跑同样的数据,结果是这个。
有效请求 4975 条,页面 7 个,参与统计 5 个(PV>=250)
path PV UV 询盘 每千次询盘
/about 620 224 0 0.00
/ 2140 240 6 2.80
/products/ 830 236 3 3.61
/blog/dnv-flange-standard 530 213 2 3.77
/contact 450 205 11 24.44
注意看,/products/stainless-flange 没了。页面数写着 7 个,参与统计只有 5 个,中间那两个去哪了。
原因是路径末尾带不带斜杠被当成了两个页面。/products/stainless-flange 有 220 条,/products/stainless-flange/ 有 185 条,各自都低于 250 的阈值,双双被过滤掉。合并起来是 405 条,本来该排第一的页面,就这样消失了。
这个 bug 特别容易漏,因为程序不报错。它不报错,它只是安静地少给你一行,而少的恰好是你最该看的那一行。
修法是一行。
def norm_path(p):
p = p.split('?')[0].rstrip('/')
return p or '/'
去查询串,去末尾斜杠,空字符串兜回根路径。三件事都得做,少一件就会出别的错。
修完又冒出第二个坑
把日志这边规范化之后,我重跑,结果 /products 的询盘从 3 变成 0 了。
因为询盘表里写的是 /products/,带斜杠,日志聚合出来的键是 /products,不带斜杠,两边对不上,q 取不到值,全算成零。它的每千次询盘从 3.61 掉到 0.00,看上去像是个新发现的问题页面。
规范化只做一半,等于把错误从一个地方搬到另一个地方。两处都要过同一个函数。
def load_inquiries(path_csv):
inq = defaultdict(int)
with open(path_csv, encoding='utf-8') as f:
for row in csv.DictReader(f):
inq[norm_path(row['path'])] += 1
return inq
这类问题在数据对齐里很常见。两份数据来自两个系统,看起来字段同名,实际格式总有一点差别。斜杠、大小写、末尾空格、URL 编码、http 与 https,任何一个不一致都会让关联失败,而且失败的样子是「值为零」,不是「报错」。
所以跑完之后要先做一次 sanity check:询盘总数对不对。日志里关联到的询盘数加起来,应该等于询盘表的总行数。不等就说明有关联不上的,先修对齐,别看结论。
这套脚本的三个局限
**看不到「看了但没提交」。**日志只有请求,没有注意力。一个页面可能被认真读了五分钟然后关掉,也可能打开两秒就跳走,日志里都是 1 条 200。要区分得引入前端埋点,那是另一套东西。
**UV 是拿 IP 去重,不精确。**同一个公司的采购部可能共用一个出口 IP,算成 1 个人。移动网络切换基站又会把一个人算成好几个。要准确得用 cookie 或者设备指纹,日志层面做不到。
**单页应用会失真。**如果站点是 SPA,路由在前端切换,日志里只有一条 /,所有子页面都不出现。这种情况要先确认日志里有没有子页面的请求记录,没有的话这套方法用不了,得从别的地方拿数据。
跑之前先做一件事
别急着改脚本,先确认你的日志里到底有没有 UA 字段。有些服务器的日志格式被改过,只留了 IP、时间、路径、状态码。没有 UA 就过滤不了爬虫,结论会严重偏差。可以先 head 几行看一眼,格式不对的话先去 nginx.conf 里把 log_format 补上,等新日志攒几天再跑。
脚本全文在上面各段里,拼起来就能跑。样本数据和脚本我放在同一个目录,改两个路径参数就能换成自己的数据。跑出来的第一行如果是个高 PV 零询盘的产品页,那基本就找对地方了。
顺带说一句,这类日志分析在独立站上价值比在平台店铺上大,因为平台不给你原始日志,你只能用它的后台报表,而报表的维度是它定的。自己的站,数据在自己手里,能问的问题也就多一些。
你跑完之后排在第一行的是哪个页面?如果是个 PV 不低、询盘为零的产品页,可以把页面链接贴在评论里,我帮你看问题出在承接、词,还是这个页面本来就不该承担转化。