
上周末想给自己做个小工具,看看几个品类里商品的价格大概落在什么区间,再顺手看看价格和评论数之间有没有点关系。简单来说就是把公开的标题、价格、评论数抓下来存成 CSV,用 pandas 算一算、matplotlib 画几张图,活儿不重。方案听着挺顺,httpx 发请求、parsel 提字段、pandas 做聚合,结果真跑起来第一个拦路的不是解析,是滑块验证。我之前没接触过这块,没有头绪,搜了一圈全是「轨迹模拟」「逆向签名」,看着就不太像正经路子,就想着在本机跑一遍,把过程记下来。接下来就先开始环境的安装。我用的是本机隔离出来的 Python 3.13 环境,先建 venv 再装依赖,别往系统环境里糊东西。依赖安装命令:
这里简单说一下为什么用 httpx + parsel:列表页这种量级用不上 Scrapy 那套调度,一个 Client 加几个 CSS 选择器就够了,代码短,改起来也快。下载地址在这里,就是上面那条镜像源,清华源偶尔拉不到新版本,换阿里云这条基本不出意外。到这里依赖就装好了,下面可以先把脚本骨架搭起来。不过动手之前得先把标题里那个问题回答掉:滑块到底怎么过。我一开始也动过念头,要不照着网上的思路试试拖动轨迹。跑下来结论有点反直觉,工程上就别硬过。滑块不是一道技术题,它是站点风控给的信号,意思是这个会话的请求节奏不像正常浏览。真去模拟轨迹、逆向当次签名,短期能通,可接口一改版整套逻辑全废,维护成本比采集本身还高,服务条款那条线也踩得有点明显。我后来把它当成一个降级开关:检测到就退避降频,退避还不行就换个出口,或者让人工在浏览器里完成一次验证,正式项目干脆去申请站点官方的开放接口。出口这块我最后用的是隧道代理,踩坑那段细说。所以下面这个脚本只做识别、退避和暂停告警,不写任何绕过的东西。还有句话得放前面:整套东西只用于本地学习统计,只取公开字段,控制频率,遵守目标站点的服务条款和 robots 约定。实例一:先把采集脚本搭起来。这里有个坑得提前讲。京东的列表接口和页面结构随时可能改版,参数还可能带当次签名,我不能凭印象给你编一个域名或者 sign 算法,脚本里接口地址干脆留空,自己打开浏览器 Network 面板复制当次请求的地址和参数填进去,选择器同理从 Elements 面板复制。留空的话脚本会直接拒绝联网,比写死一个假地址安全得多。配置部分:
这四个常量是给隧道代理留的。像亿牛云代理这类的隧道产品,控制台会把地址、端口、账号密码一并给出来,请求走隧道转发,出口 IP 由服务端轮换,自己不用维护 IP 池。脚本里拼一下 URL,交给 httpx 的 proxy 参数就完事了:
顺手记一个教训:正式用的时候把账号密码挪到环境变量里读,别写死在脚本里提交仓库。配置项一多,这毛病特别容易犯。核心代码,滑块检测、退避、解析这三段:
采集主循环里,命中验证之后那个分支是关键,我特意让它停下来:
接下来就到了踩坑的部分,这段写得比代码久。第一次跑,环境里是老版本 matplotlib,画图中文全变方块,parsel 还装成了别的包,导入就报错。报错如下:
折腾了小半天,按下面几步处理完就好了:1、依赖版本太老、装错包,先确认解释器是不是 venv 里那个,别用系统 Python 跑.venv\Scripts\python.exe -m pip list 看实际装到哪去了-i https://mirrors.aliyun.com/pypi/simple/ 重装,装完再 import 一次 2、matplotlib 中文和负号,画图前统一设一次plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei"]plt.rcParams["axes.unicode_minus"] = False另外 3.9 之后 boxplot(labels=...) 已经不能用了,改成 set_xticks() + set_xticklabels() 3、跑量稍大一点就开始 403,页面里还夹着验证滑块,这条花的时间最多先把间隔拉到 1.5 秒以上,再加指数退避,别一上来就并发我起初自己搭了个免费代理池,定时抓一批 IP、挨个探活、超时的剔掉。跑了两天就烦了,日志里一水儿的超时,光维护这个池子比写采集逻辑还累后来换成亿牛云代理的隧道代理,控制台给的地址、端口、账号密码填进上面那四个常量就行,出口 IP 自动轮换,IP 池那套代码整个删掉。配合前面的限速和退避,403 基本压下去了,采集脚本也终于能连续跑完这里要讲清楚:换出口是把单个 IP 的请求频率摊开,不是拿它去硬刚验证。真弹了滑块,还是按上面的逻辑退避、暂停、人工确认还有个地方我一开始漏了:滑块检测不能只盯着页面里有没有「验证」两个字,403 和 429 本身就是信号,得一起判。脚本默认是离线自测,不加 --live 一个网络请求都不会发,先把流程验一遍比较省心。运行命令:
输出如下:
可以看到,正常页面返回 False,验证页和 403 都返回 True,退避序列按 2 秒、4 秒、8 秒往上走,四条样例数据也解析对了,这是一个比较简单的实例应用。真要联网就加 --live --pages 3,配置填好就行,写出来的 jd_products.csv 直接给下一步用。到这里采集这块就通了。多说一句,出口稳不稳其实会直接反映在后面的图上。我之前被 403 打断过几次,CSV 里整整缺了两页,高价商品少了一截,画出来的价格分布整个往左偏,还一度以为是这个品类本来就便宜。换成亿牛云的隧道代理之后这种缺页就少多了,样本完整了再去做统计才有意义。下面就可以接数据分析了。实例二:把数据读进来做几张图。得先说清楚,下面这 240 条是脚本用固定随机种子生成的脱敏样本,不是真实数据,也不是任何真实行情,纯粹为了让图能跑起来、数字能复现。你要是把上一步采集到的 jd_products.csv 放在脚本同目录,代码会自动改读那个文件,一行都不用改。分析代码:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei"]
plt.rcParams["axes.unicode_minus"] = False
CSV_PATH = Path("jd_products.csv")
df = pd.read_csv(CSV_PATH) if CSV_PATH.exists() else make_sample_data(seed=20261105)
avg_by_cat = df.groupby("category")["price"].mean().sort_values(ascending=False)
corr = df["price"].corr(df["comments"])
corr_log = np.log10(df["price"]).corr(np.log10(df["comments"]))
# 价格跨好几个数量级,直方图横轴取对数才看得清
bins = np.logspace(np.log10(df["price"].min()), np.log10(df["price"].max()), 32)
ax.hist(df["price"], bins=bins, color="#4C78A8", edgecolor="white")
ax.set_xscale("log")
# 横向条形图先排序,让最贵的品类落在顶部
avg = df.groupby("category")["price"].mean().sort_values()
ax.barh(avg.index, avg.values, color=palette[:len(avg)])输出如下:
样本来源:固定随机种子生成的脱敏样本(非真实数据)
记录数:240
品类数:6
整体均价:2013.21 元
价格中位数:965.07 元
评论数中位数:430
均价最高的品类:电脑办公(4926.01 元)
价格与评论数相关系数:-0.478
双对数相关系数:-0.823图表输出目录:charts/(共 3 张)
可以看到,均价 2013.21 元被右边高价那几个品类拉得挺高,中位数才 965.07 元,两根竖线离得老远。这是个右偏的长尾分布,只报一个均价很容易把人带偏,这是一个比较简单的实例应用。再看各品类的均价可以看到,电脑办公 4926.01 元排在最上面,图书文娱只有 65 元,中间差了将近两个数量级。所以后面做对比按品类分组算,比全混在一起合理得多,这是一个比较简单的实例应用。最后观察价格和评论数的输出。可以看到,点整体从左上往右下走,价格越高的商品评论数越少。双对数相关系数到了 -0.823,原始 Pearson 却只有 -0.478,因为两边都是长尾分布,几个极端值就能把线性相关压下去。图用了双对数坐标,标注就也得用双对数口径,不然图上的趋势和标题里的数字对不上,这是一个比较简单的实例应用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。