5.2 动态内容:判断与三条获取路径


文档摘要

5.2 动态内容:判断与三条获取路径 本节摘要:响应里没有数据,先判断是渲染问题还是请求问题,再从"解析渲染后的接口数据、直连隐藏数据接口、上渲染方案"三条路径里按成本排序选择。本节给出判断方法与三条路径的完整实操,核心主张:渲染方案是最后选项,不是默认选项。 5.1 节拆了登录墙,本节拆另一堵墙。症状很典型:浏览器里看得一清二楚,scrapy shell 里选择器全落空。这时多数人的第一反应是"上无头浏览器"——先打住,这个决定值得用一整套判断流程来审查,因为三条路径的成本差着一个数量级。 先判断:数据到底在不在响应里 两步判断,在 shell 里完成: 响应体偏小加关键特征缺失,基本可断定数据由脚本在浏览器里二次请求渲染。

5.2 动态内容:判断与三条获取路径

本节摘要:响应里没有数据,先判断是渲染问题还是请求问题,再从"解析渲染后的接口数据、直连隐藏数据接口、上渲染方案"三条路径里按成本排序选择。本节给出判断方法与三条路径的完整实操,核心主张:渲染方案是最后选项,不是默认选项。

5.1 节拆了登录墙,本节拆另一堵墙。症状很典型:浏览器里看得一清二楚,scrapy shell 里选择器全落空。这时多数人的第一反应是"上无头浏览器"——先打住,这个决定值得用一整套判断流程来审查,因为三条路径的成本差着一个数量级。

先判断:数据到底在不在响应里

两步判断,在 shell 里完成:

>>> response.status 200 >>> len(response.body) 5821 # 几 KB 的"详情页"很可疑,正常详情页远大于此 >>> "product-name" in response.text False # 关键类名不在正文里,数据确实不是服务端直出的

响应体偏小加关键特征缺失,基本可断定数据由脚本在浏览器里二次请求渲染。为进一步确认,打开开发者工具的网络面板,刷新页面,按数据特征过滤请求——大多数站点会有一条返回 JSON 的接口,正是数据源本身。

图11 动态内容判断与三条路径决策图

图11 动态内容判断与三条路径决策图

案例展开:一个动态站点的完整排查记

背景:某商品站,详情页价格与库存用脚本填充,选择器全落空。按决策图走完全程。

操作一,确认动态:shell 里看响应体不到六 KB,"price"关键字缺失,判定数据在二次请求里。操作二,网络面板找接口:按关键字过滤 XHR,找到一条返回 JSON 的列表接口,参数带时间戳与一串签名。操作三,先试嵌入数据:详情页 HTML 的脚本标签里有初始化状态,正则提取成功,详情数据先通了。操作四,攻签名:列表接口的签名参数在页面脚本里生成,逆向成本高——改走路径二,仅对列表页开渲染、详情页仍走嵌入提取。

# 混合策略:列表页渲染,详情页嵌取 def parse(self, response): # 列表页为渲染产物,直接拆成品 HTML for href in response.css("a.item::attr(href)").getall(): yield response.follow(href, callback=self.parse_detail) def parse_detail(self, response): import json, re m = re.search(r"__INITIAL_STATE__\s*=\s*(\{.*?\});", response.text) if not m: self.logger.warning("嵌入数据缺失: %s", response.url) return row = json.loads(m.group(1))["detail"] yield {"title": row["name"], "price": row["price"]}

结果:渲染请求只占总量一成,整任务耗时在预算内;嵌入提取覆盖九成详情页,日志里的告警行给出嵌取失效的清单供人工跟进。解读:三条路径不是三选一,是按页面类型混编的套餐——渲染预算花在刀刃上。变式:接口签名若可离线复现(固定盐值、简单拼接),仍应回头直连接口,渲染只是过渡方案。

路径一:直连数据接口(首选)

网络面板里那条 JSON 接口,通常可以直接请求。把接口地址、参数、返回结构摸清,Spider 里就是普通的 JSON 解析:

import json def parse(self, response): # 页面脚本先行渲染的列表数据,实际来自这条接口 data = json.loads(response.text) for row in data["items"]: yield { "title": row["name"], "price": row["price"], "stock": row["inventory"] > 0, } next_cursor = data.get("next_cursor") if next_cursor: yield scrapy.Request( f"https://api.example.com/list?cursor={next_cursor}", callback=self.parse, )

接口路径的收益巨大:跳过 HTML,返回即结构化数据,解析代码缩到几行。代价是接口属于站点的私有契约,改版无预告——所以走这条路的 Spider 要对返回结构做防御性校验,缺字段即告警而不是崩掉。

路径三:解析页面内嵌数据

有时接口不便直连(签名复杂、参数加密),但页面脚本的初始状态里已经内嵌了整份数据——一大段 JSON 藏在 script 标签里。用正则或 JSON 解析把它挖出来:

import json, re def parse(self, response): m = re.search(r"window\.__INITIAL_STATE__\s*=\s*(\{.*?\});", response.text) if not m: return state = json.loads(m.group(1)) for row in state["list"]["items"]: yield {"title": row["title"], "price": row["price"]}

这条路径不需要渲染、又比猜接口稳,是动态站点的实用折中。

路径二:渲染方案(兜底)

前两条都走不通时才上渲染。框架有配套的渲染服务方案:爬虫请求发往渲染代理,由远端无头浏览器渲染完成后把成品 HTML 送回,Spider 侧几乎无感。也可在 Spider 内嵌浏览器内核接管请求。无论哪种,工程账要算清:渲染一份页面的时间与资源是普通请求的数十倍,并发能力骤降——只对真正需要的地址开渲染,能省则省:

# 只对详情页开渲染的分流示例(示意) def parse(self, response): for href in response.css("a.item::attr(href)").getall(): yield scrapy.Request( response.urljoin(href), callback=self.parse_detail, meta={"render": True}, # 交由渲染方案识别的标记 )

⚠️ 常见坑:判断"是否动态"时别拿手机端页面当参照——部分站点对无脚本 UA 返回服务端渲染的简化版,同一地址两种 UA 的响应结构可能完全不同。先固定 UA 再做判断,结论才可复现。

渲染方案的运维账

选定渲染路径后,运维成本要提前入账:无头浏览器实例占内存大户,并发数与实例数要对齐——十个并发配两个浏览器实例,排队等待会吃掉渲染换来的收益;页面超时与实例崩溃要有重启预案,渲染服务是整条流水线里最容易"悄悄死掉"的组件;监控上单独为渲染请求记一个成功率,低于阈值先查渲染层再查站点。把渲染当"外包给浏览器的下游服务"来管理,而不是当成框架的透明能力,是这条路径平稳运行的认知前提。

本节要点回顾

  • 先判断再动手:响应体长度加关键特征,两步确认数据是否需要二次渲染;
  • 三条路径按成本排序:解析嵌入数据与直连接口优先,渲染方案兜底;
  • 接口直连要做防御:返回结构是私有契约,校验加告警不可省;
  • 渲染是重装备:只对必要地址开启,并发预算另算。

到站难题清了。下一节回到动手之前:面对陌生站点,专业的分析动作与合规边界。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U