requests.iter_content误判大文件下载完成未抛异常的问题咨询
根因说明
requests底层依赖urllib3库,默认配置下enforce_content_length参数为False,此时如果传输过程中出现网络波动、服务端主动断开长连接等情况,urllib3不会校验实际接收字节数和响应头Content-Length声明的大小是否匹配,会直接判定响应传输完成,因此不会抛出任何异常就提前退出iter_content循环。你添加的urllib3补丁已经解决了异常检测的问题,现在抛出的IncompleteRead类错误是符合预期的,本质是大文件长距离传输过程中的连接中断问题。
解决方案
1. 断点续传实现
大文件下载过程中连接中断是无法完全避免的,最可靠的方案是实现断点续传逻辑,步骤如下:
- 提前通过HEAD请求确认目标服务端支持范围请求,即响应头包含
Accept-Ranges: bytes - 持久化记录已成功接收的原始压缩文件字节数(注意是未解压的原始字节,
Range头的偏移基于压缩文件的原始大小计算) - 每次重连时携带
Range: bytes={已下载字节数}-请求头,服务端会返回206状态码,从指定偏移位置继续传输 - 连接中断后不需要重置解压对象和残留行变量,直接用原有状态继续处理新传输的块即可
参考实现代码如下(已修正你原代码中的变量名笔误:原代码误用dns_chunk指代循环变量data_chunk):
import requests import zlib import urllib3 def patch_urllib3(): """Set urllib3's enforce_content_length to True by default.""" previous_init = urllib3.HTTPResponse.__init__ def new_init(self, *args, **kwargs): previous_init(self, *args, enforce_content_length = True, **kwargs) urllib3.HTTPResponse.__init__ = new_init def reader(target_url, data_queue, coordinator_queue, chunk_size=4096, max_retry=10): patch_urllib3() # 已下载的原始压缩字节数,可写入本地临时文件持久化,避免进程重启丢失进度 downloaded_bytes = 0 decompressor = zlib.decompressobj(zlib.MAX_WBITS|32) last_line = "" retry_cnt = 0 while retry_cnt < max_retry: req_headers = {} if downloaded_bytes > 0: req_headers['Range'] = f'bytes={downloaded_bytes}-' try: with requests.get( target_url, stream=True, headers=req_headers, # 增加超时配置,避免长时间无响应卡住 timeout=(30, 120) ) as remote_file: # 200为全量响应,说明服务端不支持续传,重置所有状态 if remote_file.status_code == 200: decompressor = zlib.decompressobj(zlib.MAX_WBITS|32) last_line = "" downloaded_bytes = 0 elif remote_file.status_code != 206: raise Exception(f"服务端不支持范围请求,响应码{remote_file.status_code}") for data_chunk in remote_file.iter_content(chunk_size=chunk_size): if not data_chunk: continue # 累计已下载的压缩字节数 downloaded_bytes += len(data_chunk) # 解压、解码、分行逻辑 decompressed_chunk = decompressor.decompress(data_chunk) decompressed_chunk = decompressed_chunk.decode() decompressed_chunk = last_line + decompressed_chunk lines = decompressed_chunk.splitlines() last_line = lines.pop() data_queue.put(lines) coordinator_queue.put('CHUNK_READ') # 正常下载完成,跳出重试循环 break except ( requests.exceptions.ChunkedEncodingError, urllib3.exceptions.IncompleteRead, ConnectionError, TimeoutError ) as e: retry_cnt += 1 print(f"连接中断,第{retry_cnt}次重试,已下载字节数:{downloaded_bytes},错误信息:{str(e)}") continue else: # 超过最大重试次数 coordinator_queue.put('READ_FAILED') raise Exception(f"下载失败,超过最大重试次数{max_retry}") # 推送最后残留行,通知任务完成 print("Sending last line.") data_queue.put([last_line]) coordinator_queue.put('READ_DONE')
2. 优化建议
- 拆分下载和处理逻辑:不要在下载进程中执行解压、解码、分行这类CPU密集操作,直接将原始压缩块丢到队列中,由下游处理进程执行对应操作,可大幅降低下载进程阻塞概率,减少服务端主动断开连接的可能
- 开启TCP Keepalive:通过requests适配器配置TCP长连接保活,避免中间网络设备因为长时间无数据传输断开连接
- 若服务端不支持范围请求:可优先使用wget、curl等内置断点续传能力的工具提前将文件下载到本地磁盘,再通过脚本读取本地文件处理,稳定性更高
内容的提问来源于stack exchange,提问作者Shrout1
相关产品推荐
相关产品推荐

