2.2 Request 与 Response:双程车票的两面


文档摘要

2.2 Request 与 Response:双程车票的两面 本节摘要:Request 是去程车票:地址、方法、头、回调、行李舱 meta 全印在票面上,下行途中每一道关卡都有权改写它;Response 是回程凭证:状态、头、体加选择器接口,是解析回调的唯一入口。本节把这对对象的字段摊开讲透,并给出 meta 传话与优先级调度的实战用法。 上一节你在车间里印出了票,这一节把票面放大细看。第 2 章的第二个问题——"解析回调拿到的到底是什么"——答案就是 Response;而理解 Response 的最好方式,是先看清它去程的孪生兄弟 Request。

2.2 Request 与 Response:双程车票的两面

本节摘要:Request 是去程车票:地址、方法、头、回调、行李舱 meta 全印在票面上,下行途中每一道关卡都有权改写它;Response 是回程凭证:状态、头、体加选择器接口,是解析回调的唯一入口。本节把这对对象的字段摊开讲透,并给出 meta 传话与优先级调度的实战用法。

上一节你在车间里印出了票,这一节把票面放大细看。第 2 章的第二个问题——"解析回调拿到的到底是什么"——答案就是 Response;而理解 Response 的最好方式,是先看清它去程的孪生兄弟 Request。

Request 票面全览

一张 Request 上,真正常打交道的信息有七项:

scrapy.Request( url="https://books.example.com/book/101", # 目的地 callback=self.parse_detail, # 到站后谁接待 method="GET", # 动词:GET / POST headers={"Accept-Language": "zh-CN"}, # 票面附言:请求头 meta={"depth": 1, "category": "fiction"}, # 行李舱:跨回调传话 priority=5, # 候车优先级:数字大的先上车 dont_filter=False, # False 则接受去重审查 errback=self.on_error, # 失联时的联系人 )

这些字段在旅程中并非只读——下行途中每一道下载器中间件都能改写它们。第 4 章的 UA 轮换、代理注入,本质上都是在去程关卡里改这张票。理解"票是可改的",你才能解释很多"我明明写的是 A 怎么发出去成了 B"的现象。

有个字段现在就要点破:priority。它决定候车顺序——同样是待发请求,详情页理应比翻页先走,否则深度优先的默认调度会先把第一页的跟进链全部爬完。数值越大越先出站,第 3 章的调度器一节会展开它的队列实现。

Response 回程凭证

响应回到 Spider 手里时,已是一个内容齐全的对象:

def parse_detail(self, response): response.status # 200,状态码 response.url # 最终地址:重定向后的终点 response.headers # 响应头,类似字典但值是字节串 response.body # 响应体原始字节 response.text # 按推断编码解码后的文本 response.encoding # 本次解码使用的编码 response.css("h1") # 选择器接口,2.3 节的主角 response.xpath("//title") response.meta # 去程塞进行李舱的话,原样带回 response.request # 发出此响应的那张票(可反查回调链)

其中最值得停顿的是 url 与 body 的两个细节。url 显示的是"最终地址":若去程遭遇重定向,你手里的 url 已不是票面上的原始地址,需要回溯时得看 response.request.url。text 与 body 则牵出编码这个经典坑:框架按响应头与页面声明推断编码,推断错了中文会变问号——手工校正用 response.encoding 赋值,或以 body 为准自行解码。

双程对照:一张表看对称性

维度 Request(去程) Response(回程)
核心载荷 地址与方法 状态码与响应体
头部 headers 可自由设置 headers 只读
传递容器 meta 行李舱(可写) meta 原样带回(可读可续写)
中间件介入点 process_request 改票 process_response 改凭证
失败表现 errback 收到 Failure 不存在——失败没有回程

"失败没有回程"这句值得画重点:你在回调里永远见不到 404 的 Response(默认重试耗尽后请求直接进入失败通道),处理失败必须走 errback。很多新手在 parse 里写"如果 response.status 是 404",实际上那条分支几乎永远不会执行。

图4 一张车票的双程字段流转

图4 一张车票的双程字段流转

变式:POST 出票与表单数据的票面

GET 之外,POST 请求的票面多了一块 body。构造 POST 票用 FormRequest,表单字段进 formdata:

from scrapy import FormRequest def start_requests(self): yield FormRequest( "https://books.example.com/search", formdata={"keyword": "scrapy", "page": "1"}, # 表单字段进请求体 callback=self.parse_result, )

POST 票与去重机制的互动要特别注意:同一 formdata 的重复提交默认同样会被指纹拦下——搜索翻页这类"每页参数不同"没问题,"每轮都要重查同一关键词"就得 dont_filter(3.1 节的代价计算同样适用)。另外 POST 的失败重试语义与 GET 一致,但对方接口若非幂等,重试可能造成重复提交——对接非幂等接口时,把重试交给业务侧幂等键,而不是盲目依赖框架重试。

实战:depth 控深与 priority 调序

把两个最常用的字段组合起来,看一个"广度优先加限深"的实用模式:

def parse(self, response): depth = response.meta.get("depth", 0) # 引擎自动维护的深度 if depth > 3: # 限深:超过三层不再续票 return for href in response.css("a::attr(href)").getall(): yield response.follow( href, callback=self.parse, priority=(100 - depth * 10), # 越浅优先级越高,近似广度优先 )

depth 是引擎免费送的:每续一张票它自动加一。配合 priority 递减,原本"一条道走到黑"的默认深度优先,就被改造成逐层推进——对"先扫全站骨架、再回头要细节"的采集需求,这个模式几乎零成本。

# 运行日志里可见 depth 的踪迹 DEBUG: Crawled (200) books.example.com/ (referer: None) DEBUG: Crawled (200) books.example.com/fiction (referer: books.example.com/) ... 'page_crawled_count': 48, 'max_depth_reached': 3

排错案例:一次"响应内容不对"的定位

一个真实案例把本节字段全部用上。背景:详情页抓出来的价格字段大量为空,但站点页面明明显示正常。操作:先在 errback 与回调里同时打印 response.status、response.url 与 len(response.body) 三件套——结果发现"空价格"的响应 url 是另一个地址:请求被服务端重定向到了地区选择页,响应体只有几 KB。解读:不是选择器错了,是票被发到了别的终点——response.url 显示最终地址这一特性在这里立功。处置:在请求头补上地区参数,或改用带地区信息的地址族重新出票。变式:同类"响应不对"问题还有编码错乱(用 response.encoding 赋值校正)与压缩内容异常(交给框架处理,别手工解压),定位手法一致——先看票面事实,再怀疑代码。

本节要点回顾

  • Request 七要素:地址、回调、方法、头、meta、优先级、去重开关,去程关卡皆可改写;
  • Response 五类信息:状态、地址(最终地址)、头体、选择器接口、来路反查;
  • 失败无回程:错误处理必须走 errback,别在 parse 里等 404;
  • depth 与 priority 是控制旅程形态的两个把手:一个限深,一个调序。

票面看完了。下一节到站拆包——选择器登场,这是你写得最多、也最容易出错的一段代码。


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