bigywater.tech
爬虫工程 · 8

流式读取:chunked 响应与提前中止下载

爬虫要抓的东西有时很大,有时是慢速的长连接。stream=True 到底什么时候返回、iter_content 什么时候产出、提前放弃一个响应要付出什么,以及服务器中途断开时拿到的是什么。这篇用本机服务器逐个试。

2 ms
stream=True 的 requests.get 只拿响应头就返回(不用 stream 的版本要等 1518 ms)
0.6 ms
HEAD 读 200 MiB 响应的头:连接还能复用(stream 加 close 要 1.2 ms 且连接被丢弃)
512
被截断的响应,stream 逐块读时交给你的字节数(服务器实发 600)

chunked 与 stream=True

实测 · 本机执行输出
  响应头:Transfer-Encoding=chunked,Content-Length=None
  iter_content(chunk_size=None):每块到达的时刻与大小 0ms/1000B, 300ms/1000B, 604ms/1000B, 909ms/1000B, 1215ms/1000B
  iter_content(chunk_size=4096):0ms/1000B, 305ms/1000B, 610ms/1000B, 913ms/1000B, 1218ms/1000B
  iter_content(chunk_size=300):共 20 次,前 6 次 0ms/300B, 0ms/300B, 0ms/300B, 0ms/100B, 301ms/300B, 301ms/300B
  不用 stream:requests.get 返回时已经过了 1518 ms,r.content 共 5000 字节
  stream=True:requests.get 返回(只拿到响应头)用了 2 ms

服务器用 Transfer-Encoding: chunked 分块发送,没有 Content-Length。stream=True 让 requests.get 只读响应头就返回(2 ms),iter_content(chunk_size=None) 每收到一块就产出一块。把 chunk_size 设成 4096 也不会等凑满 4096 字节——只要底层有数据到达就先给你,这对实时流很重要。不用 stream 则要等全部读完(1518 ms)。

iter_lines

实测 · 本机执行输出
  服务器发的块:[b"line1\nli", b"ne2\nline3\n", b"lin", b"e4"]
  iter_lines() 得到:[b'line1', b'line2', b'line3', b'line4']

iter_lines() 按换行切分,而块边界和行边界无关:被切开的两半会被拼起来,最后一行没有换行符也会被产出。

只想看头,别读 body

实测 · 本机执行输出
  stream=True 看一眼就 close():1.2 ms(状态码 200 Content-Length=209715200);随后同一个 Session 再发一个小请求,服务器累计连接数 2
  HEAD:0.6 ms(状态码 200 Content-Length=209715200);随后同一个 Session 再发一个小请求,服务器累计连接数 1
  普通 get 读完:178.6 ms(r.content 200 MiB);随后同一个 Session 再发一个小请求,服务器累计连接数 1
  (本机回环速度;真实网络上差距按带宽放大)

要 Content-Length 或状态码,又不想下载内容时,HEAD 最划算:0.6 ms,而且连接可以继续复用。stream=True 再 close() 也很快(1.2 ms),但尚未读完的连接被丢弃,下一个请求要新建连接(与第 2 篇的结论一致)。前提是服务器对 HEAD 返回的头和 GET 一致,这点我只在自己的服务器上验证。

被截断的响应

实测 · 本机执行输出
  Content-Length: 1000,实发 600 字节 → ChunkedEncodingError:('Connection broken: IncompleteRead(600 bytes read, 400 more expected)', IncompleteRead(600 bytes re
    stream 逐块读 → 读到 512 字节后 ChunkedEncodingError
  chunked,第二块声明 1000 字节只发了 300 → ChunkedEncodingError:('Connection broken: IncompleteRead(300 bytes read, 700 more expected)', IncompleteRead(300 bytes re
    stream 逐块读 → 读到 1256 字节后 ChunkedEncodingError

服务器声称的长度和实际发送的不一致时,requests 抛 ChunkedEncodingError(消息里有 IncompleteRead,写明已读和还差多少字节)。普通模式下你拿不到 r.content;stream 模式下,已经完整交付的块你拿到了,最后不完整的那一块(88 字节)没有交给你。爬虫里应当把这种异常当作可重试的失败,而不是把半截内容当成功。

分步演示

流式读取
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务器用 HTTP/1.1 分块发送(每块间隔 0.3 秒)、声明 200 MiB 的 Content-Length 并持续写出、故意少发字节后关闭连接来模拟截断;到达时刻用 time.perf_counter 在客户端逐块记录;连接数是服务器记录的累计 accept 次数
    • 没有验证:真实网络与慢速客户端下的背压;服务器端写入被客户端关闭打断的具体字节数(取决于套接字缓冲区,各次不同,所以没有写进文章);iter_content 的 decode_unicode;aiohttp 的 iter_chunked 等对应接口(后面单独讲);HTTP/2 下的流式读取;HEAD 响应与 GET 响应头不一致的真实站点
    京ICP备2026067502号-1京公网安备11010502063487号