爬虫要抓的东西有时很大,有时是慢速的长连接。stream=True 到底什么时候返回、iter_content 什么时候产出、提前放弃一个响应要付出什么,以及服务器中途断开时拿到的是什么。这篇用本机服务器逐个试。
响应头: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)。
服务器发的块:[b"line1\nli", b"ne2\nline3\n", b"lin", b"e4"] iter_lines() 得到:[b'line1', b'line2', b'line3', b'line4']
iter_lines() 按换行切分,而块边界和行边界无关:被切开的两半会被拼起来,最后一行没有换行符也会被产出。
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 字节)没有交给你。爬虫里应当把这种异常当作可重试的失败,而不是把半截内容当成功。