首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >淘宝店铺页面结构经常变化,爬虫代码需要定期更新吗?

淘宝店铺页面结构经常变化,爬虫代码需要定期更新吗?

原创
作者头像
小白学大数据
发布于 2026-11-09 17:06:13
发布于 2026-11-09 17:06:13
230
举报

上周三打开定时任务落下来的 CSV,我愣了一下:店铺名和价格两列大半是空的,可任务日志一片绿,一条报错都没有。手上这个小脚本每周跑一次,只取店铺页面上公开可见的五个字段,店铺名、商品标题、价格、销量展示、商品 ID,跑了小半年没出过事。第一反应就是页面又改版了,紧跟着第二个念头更烦:是不是以后每周都得去改一次选择器?简单来说就是,页面结构变了,原来那条 XPath 指向的节点没了,解析函数返回 None,写进 CSV 就是空字符串。程序不崩、不抛异常,它只是安安静静给你一堆空值。这比直接报错麻烦得多,因为下游拿到的是「看起来正常」的数据。先把结论放这儿:代码不用按日历定期更新,但规则一定要跟着页面走。我在本机做的调整只有三件事,把解析规则从代码里挪到配置字典、优先读页面里嵌的结构化数据、给字段完整率加一条告警线。改版真的来了,动的是配置里几行候选路径,解析函数一行没改。这套做法我之前没接触过,一开始也没头绪,网上搜到的多是「改版了就改 XPath」这种一句话答案。就想着在本机跑一遍,把过程记下来,顺便看看「定期更新」到底该更新什么。接下来就先开始环境的安装。依赖装一下:

本机输出:

不出意外,装完就踩了两个坑。第一个是中文全变方块。图一画出来标题和坐标轴都是小方框,我当时还以为是编码问题,翻了半天 CSV。后来才想起 matplotlib 默认字体里没有中文字形,得在画图脚本开头显式指定。第二个是画图函数。本来想用 boxplot 画各阶段分布,报错如下:

查了一下,matplotlib 3.9 以后 labels 这个参数已经挪走了。处理的步骤是:1、改用 ax.set_xticks() 配合 ax.set_xticklabels() 设置分组标签中文字体统一加 plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei"]再加 plt.rcParams["axes.unicode_minus"] = False,负号才不会显示成方块 2、既然只比五个阶段的均值,分组柱状图比箱线图更直观,索性换掉到这里,环境就装好了,下面就可以开始跑解析了。这里简单说一下样本,免得被当成真实数据:下面 30 天、每天 200 个页面的记录,都是脚本用固定随机种子 20261109 生成的脱敏模拟页面,不是任何真实站点的抓取结果。脚本默认离线 self-test,不发起网络请求,只加 --live 才会走联网分支;联网分支里的页面地址是留空的,从浏览器 Network 复制当次请求地址(含当次参数)复用就行,接口随时可能改版,一切以抓包为准,本文不写死任何域名和参数。想换成自己本地存下来的真实页面也行,同目录放一个 real_pages.csv(列 day,html),脚本会自动改读它,不用改代码。实例一:先让旧解析跑一遍,看看改版那天到底发生了什么。旧版解析代码:

五条 XPath 全写死在 v1 版页面的 class 上。样本里第 11 天换成 v2 结构,第 21 天换成 v3 结构,类名和容器都改了,但内嵌的那段结构化 JSON 还在。跑完 30 天的统计输出如下(节选):

可以看到,改版之后旧解析的字段完整率直接从 98.5% 掉到 0.0%,而任务本身没有任何异常抛出,这是一个比较简单的实例应用。这里还有个更值得记一笔的细节:如果只坏一半字段,人往往几天后发现不了,所以光看「有没有报错」完全不够,得看每个字段填上了没有。

实例二:把规则挪进配置,再加一层字段校验。思路不复杂。页面里经常有一段 script[type="application/ld+json"] 之类的结构化数据,它比 DOM 类名稳得多,改版一般先动样式再动数据契约,所以先读它;读不到再按候选 CSS 路径一条条兜底;最后对每个字段做一次类型校验,价格必须能转成数字,标题不能是空串,商品 ID 不能丢。配置字典长这样,改版时只往数组里追加:

弹性解析代码:

代码语言:txt
复制
def parse_resilient(html, cfg):
    s, ld = Selector(html), _read_ld(s)
    out = {}
    for field in FIELDS:
        value = None
        for key in cfg["json"][field]:
            value = _json_value(ld, key)
            if isinstance(value, (str, int, float)) and str(value).strip():
                break
            value = None
        if value is None:
            for rule in cfg["css"][field]:
                value = s.css(rule).get()
                if value and str(value).strip():
                    break
                value = None
        out[field] = value
    return out

脚本里用日期切换 CONFIG_V1、CONFIG_V2、CONFIG_V3,模拟的就是「第 13 天补了一组候选路径」「第 23 天又补一组」这两次配置热更新,解析函数从头到尾没动。补完之后字段完整率回到 99.4%,掉下去那两天是 19.7%,因为结构化 JSON 的键也换了,只剩商品 ID 靠 data-item-id 属性兜住了一个字段。可以看到,改的只是配置字典,代码本身不用重新发版,这是一个比较简单的实例应用。图上柱子标的是保留一位小数的显示值,summary.json 里是四位小数的精确值,正文里的 32.83% 和 88.75% 这两个 30 天均值取自后者。告警这块我设得很朴素:字段完整率低于 95%,或者环比掉超过 15 个百分点,就打一条 STRUCT_CHANGE。30 天里触发了 4 次,正好是第 11、12、21、22 天,也就是两次改版各占两天。这里简单说一下为什么用「字段完整率」而不是「请求成功率」:改版之后 HTTP 照样是 200,请求成功率一点没变,只有字段完整率会先塌下来。接下来是另一个坑,跟页面结构没关系,但同样会让数据悄悄变空。第 15 到 18 天我想把前几天缺的数据补回来,把并发调高了一档,结果 429 抬头,还零星冒出 403。输出如下:

代码语言:txt
复制
 "429占比_高峰4天均值": 0.3337,
  "429占比_其余26天均值": 0.0363,
  "仅200请求_30天平均耗时ms": 1035.0,
  "仅200请求_高峰4天平均耗时ms": 1332.3,
  "403暂停天数": 4,
  "退避秒数_高峰": 16.0,
  "退避秒数_常态": 2.0,

注意耗时的口径:只统计状态码 200 的请求,429、403 和 timeout 都不算进去,不然一条超时就能把均值拉得忽高忽低,图上完全没法解释。高峰那 4 天的 1332.3ms 比 30 天整体的 1035.0ms 高出近 300ms,也是限流已经发生的信号。限流处理代码:

代码语言:txt
复制
def backoff_seconds(consecutive_429):
    return float(min(60, 2 ** min(consecutive_429, 6)))
if status == 429:
    time.sleep(backoff_seconds(consecutive_429))   # 2.0s 起,最高 16.0s
    consecutive_429 += 1
elif status == 403:
    pause_task_and_notify()                       # 暂停,人工到浏览器确认

403 出现就暂停任务、发通知,让人自己在浏览器里确认,不自动重试,更不会去模拟拖动或者还原什么加密参数。原因很实在:这类接口和验证逻辑改得比页面结构还快,逆向出来的东西过两周就失效,维护成本比采集本身高,也容易踩到服务条款的红线。工程上不硬过,只做检测、退避、降频和告警。单 IP 频率压不下来的时候,我后来接了亿牛云代理的隧道代理,把请求摊开到不同出口。配置就是从控制台拿地址、端口、账号密码填进环境变量,不用自己维护 IP 池,每次请求出口自动换。这里要写清楚,它的作用是把单 IP 的请求频率摊薄,不是拿来硬刚验证,该降的频、该退的避一样都不能少。

合规上再啰嗦一句:这套东西只做本地学习统计,只取页面公开可见的字段,控制频率,遵守目标站点服务条款;登录态数据、非公开字段、涉及个人隐私的内容一概不碰。真要长期稳定地拿数据,优先看看站点有没有官方开放接口,比自己扛解析省心太多。回到标题那个问题,把这次跑下来的判断整理成一张表:

现象

要动什么

大概动多少

类名、容器层级、属性名变了

配置里的候选路径

追加几行,不用发版

内嵌 JSON 的键名变了

配置里的候选键

追加几行

页面从服务端渲染改成纯 JS 渲染

采集层代码

得改,要换渲染方式

字段语义本身变了(价格拆成区间、销量换成区间档位)

校验和入库逻辑

得改,属于需求变更

需要登录或会话才能看到

整体方案

别硬做,先看官方接口

所以「定期更新」这个说法不太准确:不用每周打开代码改一遍,但必须让变化自己浮出来。字段完整率监控是眼睛,配置化的候选路径是手,两者都有了,改版来的时候就是几分钟的事。真正需要动代码的是表里后三种情况,那已经不是结构微调了。最怕的是既不监控、又把选择器写死,等下游发现数据不对,中间已经空了好几天。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档