Requests流式请求:chunked传输编码实战
【免费下载链接】requests 项目地址: https://gitcode.com/gh_mirrors/req/requests
在现代Web应用开发中,处理大型文件传输、实时数据流或长时间运行的进程时,传统的一次性请求-响应模式往往无法满足需求。想象一下,当你尝试下载一个GB级别的文件或实时获取服务器推送的数据流时,如果等待整个数据传输完成才进行处理,不仅会占用大量内存,还会导致用户体验下降。分块传输编码(Chunked Transfer Encoding) 正是解决这类问题的关键技术,而Requests库通过其流式请求功能,为开发者提供了简洁而强大的实现方式。
本文将深入探讨Requests中的流式请求机制,从理论基础到实际应用,帮助你掌握chunked传输编码的实战技巧。读完本文后,你将能够:
- 理解分块传输编码的工作原理及适用场景
- 熟练使用Requests发送和接收流式数据
- 处理流式传输中的异常情况和性能优化
- 实现文件下载、实时日志监控等常见流式应用
分块传输编码(Chunked Transfer Encoding)解析
什么是分块传输编码?
分块传输编码(Chunked Transfer Encoding)是HTTP协议中的一种数据传输机制,它允许服务器将响应数据分成多个独立的"块"(chunk)进行发送,而不需要预先知道整个响应的大小。这种方式特别适用于:
- 动态生成的内容,无法预先确定内容长度
- 大型文件传输,避免长时间占用内存
- 实时数据流,如视频流、日志流等
在HTTP响应中,分块传输通过设置Transfer-Encoding: chunked头部来启用。每个块包含一个十六进制的长度值和数据部分,最后以一个长度为0的块表示传输结束。
分块传输与传统传输的对比
| 特性 | 传统传输(Content-Length) | 分块传输(Chunked) |
|---|---|---|
| 内容长度 | 必须预先知道并在Content-Length头部指定 | 无需预先知道,动态分块 |
| 内存占用 | 通常需要完整接收后处理,内存占用大 | 可流式处理,内存占用小 |
| 实时性 | 需等待全部数据传输完成 | 数据块可即时处理,响应更快 |
| 适用场景 | 静态资源、小文件传输 | 动态内容、大文件、实时流 |
分块传输的工作流程
分块传输的工作流程可以用以下流程图表示:
在Requests库中,这一机制通过stream=True参数启用,允许我们以迭代方式处理响应数据。
Requests流式请求核心机制
Requests中的流式处理实现
Requests库通过其Response对象的iter_content()和iter_lines()方法提供了对流式响应的支持。这些方法的实现位于src/requests/models.py文件的Response类中。
关键的实现代码如下:
def __iter__(self):
"""Allows you to use a response as an iterator."""
return self.iter_content(128)
def iter_content(self, chunk_size=1, decode_unicode=False):
"""Iterates over the response data. When stream=True is set on the
request, this avoids reading the content at once into memory for
large responses.
"""
# ... 实现细节 ...
if self._content_consumed and isinstance(self._content, bool):
raise StreamConsumedError()
# ... 实现细节 ...
这段代码表明,当设置stream=True时,Response对象可以作为一个迭代器,通过iter_content()方法逐块返回数据,而不是一次性加载到内存中。
流式请求的核心参数
使用Requests发送流式请求时,有几个核心参数需要理解:
stream: 布尔值,默认为False。设置为True时启用流式响应,Response对象的content属性将不可用,需通过iter_content()或iter_lines()方法获取数据。chunk_size: 指定每次迭代返回的数据块大小,单位为字节。decode_unicode: 布尔值,是否自动解码Unicode响应。
流式请求的生命周期
发送流式请求:从理论到实践
基本流式请求示例
使用Requests发送流式请求非常简单,只需在调用请求方法时设置stream=True参数,然后通过iter_content()或iter_lines()方法处理响应数据:
import requests
url = 'https://example.com/large-file'
response = requests.get(url, stream=True)
# 迭代处理响应数据块
for chunk in response.iter_content(chunk_size=8192):
if chunk: # 过滤掉保持连接的空块
# 处理数据块,例如写入文件
with open('large_file', 'ab') as f:
f.write(chunk)
# 关闭响应连接
response.close()
注意:使用流式请求后,应确保正确关闭连接以释放资源。推荐使用上下文管理器(
with语句)来自动管理连接。
使用上下文管理器的最佳实践
import requests
url = 'https://example.com/stream'
with requests.get(url, stream=True) as response:
# 检查响应状态码
response.raise_for_status()
for line in response.iter_lines():
if line: # 过滤掉空行
decoded_line = line.decode('utf-8')
print(f"Received: {decoded_line}")
使用上下文管理器(with语句)是处理流式请求的推荐方式,它会在代码块执行完毕后自动关闭连接,即使发生异常也能确保资源正确释放。
自定义分块大小与性能优化
chunk_size参数的设置对性能有显著影响。太小的块会增加I/O操作次数,太大的块则会占用较多内存。以下是不同场景下的推荐设置:
# 大型文件下载 - 较大的块大小
with requests.get(large_file_url, stream=True) as r:
for chunk in r.iter_content(chunk_size=1024*1024): # 1MB块
# 处理大块数据
# 实时日志流 - 较小的块大小
with requests.get(log_stream_url, stream=True) as r:
for line in r.iter_lines(): # 按行处理
# 处理单行日志
流式上传数据
Requests不仅支持流式响应,还可以发送流式请求体。这对于上传大型文件或动态生成的数据非常有用:
def generate_large_data():
"""生成大型数据流的生成器函数"""
for i in range(1000):
yield f"Chunk {i}: {'x' * 1024}\n".encode('utf-8')
# 流式上传
response = requests.post(
'https://example.com/upload',
data=generate_large_data(), # 传入生成器
headers={'Content-Type': 'application/octet-stream'}
)
print(f"Upload completed with status: {response.status_code}")
在这个例子中,我们使用生成器函数generate_large_data()来动态生成上传数据,避免将整个文件加载到内存中。
接收流式响应:高级技巧与工具
iter_content() vs iter_lines():选择合适的迭代器
Requests提供了两种主要的流式迭代方法,适用于不同场景:
iter_content()
iter_content()方法返回原始的字节流数据块,适合处理二进制文件或需要精确控制数据处理的场景:
with requests.get(image_url, stream=True) as r:
r.raise_for_status()
with open('image.jpg', 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
# 过滤掉可能的空块
if chunk:
f.write(chunk)
# 可以在这里添加进度条更新逻辑
iter_lines()
iter_lines()方法会自动将响应数据按行分割,适合处理文本流、日志文件等按行组织的数据:
with requests.get(log_url, stream=True) as r:
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if line: # 过滤掉空行
print(f"Log entry: {line}")
# 可以在这里添加日志解析和处理逻辑
iter_lines()还支持delimiter参数来自定义行分隔符,以及keepends参数来保留行结束符。
实时进度监控实现
对于文件下载等场景,实时显示进度非常重要。以下是一个结合tqdm库实现进度条的示例:
import requests
from tqdm import tqdm
url = 'https://example.com/large-file.zip'
filename = 'large-file.zip'
response = requests.get(url, stream=True)
# 获取文件总大小(如果服务器提供)
total_size = int(response.headers.get('content-length', 0))
chunk_size = 1024*1024 # 1MB块
# 创建进度条
progress_bar = tqdm(total=total_size, unit='iB', unit_scale=True)
with open(filename, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
progress_bar.update(len(chunk))
f.write(chunk)
progress_bar.close()
if total_size != 0 and progress_bar.n != total_size:
print("警告:下载的文件大小与预期不符,可能存在问题")
响应数据的即时处理与转换
流式处理的一大优势是可以在数据接收过程中进行即时处理,而不必等待整个响应完成。以下是一个实时JSON流解析的示例:
import requests
import json
def process_json_stream(url):
"""处理JSON流数据,假设每个JSON对象占一行"""
with requests.get(url, stream=True) as r:
r.raise_for_status()
for line in r.iter_lines(decode_unicode=True):
if line:
try:
data = json.loads(line)
# 即时处理JSON数据
process_data(data)
except json.JSONDecodeError as e:
print(f"解析JSON失败: {e},行内容: {line}")
def process_data(data):
"""处理单个JSON对象"""
# 根据实际需求实现数据处理逻辑
print(f"处理数据: {data.get('id')}")
# 使用示例
process_json_stream('https://example.com/json-stream')
异常处理与错误恢复
常见流式传输异常及处理
在流式传输过程中,可能会遇到各种异常情况,如网络中断、连接超时等。Requests定义了多种异常类型来处理这些情况,位于src/requests/exceptions.py文件中:
from requests.exceptions import (
ChunkedEncodingError, ConnectionError,
ReadTimeout, StreamConsumedError
)
def safe_stream_process(url):
try:
with requests.get(url, stream=True, timeout=10) as r:
for chunk in r.iter_content(chunk_size=8192):
# 处理数据块
process_chunk(chunk)
except ChunkedEncodingError as e:
print(f"分块编码错误: {e}")
# 实现恢复逻辑,如重新请求或记录错误位置
except ConnectionError as e:
print(f"连接错误: {e}")
# 实现重连逻辑
except ReadTimeout as e:
print(f"读取超时: {e}")
# 实现超时处理逻辑
except StreamConsumedError as e:
print(f"流已被消费: {e}")
except Exception as e:
print(f"发生未预期错误: {e}")
断点续传实现
对于大型文件下载,断点续传是一项重要功能,它允许从上次中断的位置继续下载,而不必重新下载整个文件:
import os
import requests
def resume_download(url, filename, chunk_size=1024*1024):
"""断点续传下载文件"""
file_size = 0
# 检查文件是否已部分下载
if os.path.exists(filename):
file_size = os.path.getsize(filename)
print(f"发现部分下载文件,大小: {file_size} bytes")
# 设置请求头,从上次中断处继续下载
headers = {'Range': f'bytes={file_size}-'} if file_size > 0 else {}
try:
with requests.get(url, stream=True, headers=headers) as r:
# 检查是否支持断点续传
if file_size > 0 and r.status_code != 206:
print("服务器不支持断点续传,将从头开始下载")
# 删除部分文件,从头开始
os.remove(filename)
return resume_download(url, filename)
total_size = file_size + int(r.headers.get('content-length', 0))
with open(filename, 'ab') as f:
# 如果是续传,移动到文件末尾
if file_size > 0:
f.seek(file_size)
for chunk in r.iter_content(chunk_size=chunk_size):
if chunk: # 过滤掉保持连接的空块
f.write(chunk)
# 可以在这里添加进度更新逻辑
print(f"下载完成,文件大小: {os.path.getsize(filename)} bytes")
except Exception as e:
print(f"下载出错: {e}")
print("可以再次运行此函数恢复下载")
连接中断后的恢复策略
对于长时间运行的流式传输,连接中断是常见问题。实现断点续传或恢复机制至关重要:
def resilient_stream_process(url, retry_limit=3):
"""带重试机制的流式处理函数"""
retry_count = 0
last_position = 0 # 记录上次处理位置
while retry_count < retry_limit:
try:
headers = {}
if last_position > 0:
# 如果有上次处理位置,尝试从该位置继续
headers['Range'] = f'bytes={last_position}-'
with requests.get(url, stream=True, headers=headers) as r:
# 检查响应状态码
r.raise_for_status()
# 处理响应数据
for chunk in r.iter_content(chunk_size=8192):
if chunk:
# 处理数据块
process_chunk(chunk)
# 更新处理位置
last_position += len(chunk)
# 如果成功完成,跳出循环
break
except (ConnectionError, ReadTimeout) as e:
retry_count += 1
print(f"连接错误: {e},重试次数: {retry_count}/{retry_limit}")
if retry_count >= retry_limit:
print("达到最大重试次数,无法继续")
# 保存当前状态,以便后续恢复
save_progress(last_position)
raise
# 指数退避策略
time.sleep(2 ** retry_count)
except Exception as e:
print(f"处理错误: {e}")
save_progress(last_position)
raise
超时设置与性能平衡
在流式请求中,超时设置需要特别注意。Requests允许为不同阶段设置超时:
# 设置连接超时和读取超时
response = requests.get(
'https://example.com/stream',
stream=True,
timeout=(5, 30) # 连接超时5秒,读取超时30秒
)
# 或者为整个请求设置统一超时
response = requests.get(
'https://example.com/stream',
stream=True,
timeout=30 # 连接和读取总超时30秒
)
对于长时间运行的流式传输,可能需要禁用超时或设置非常长的超时时间,但这会增加资源占用风险。需要根据具体场景权衡:
# 对于无限流(如实时日志),可能需要禁用读取超时
try:
response = requests.get(
'https://example.com/infinite-stream',
stream=True,
timeout=(5, None) # 连接超时5秒,读取不超时
)
# 处理无限流...
except ConnectionError:
print("连接失败")
实战案例:构建实时日志监控系统
系统架构设计
我们将构建一个实时日志监控系统,该系统能够:
- 从远程服务器流式获取日志数据
- 实时解析和过滤日志
- 展示关键信息和告警
系统架构如下:
完整实现代码
import requests
import json
import time
from datetime import datetime
from requests.exceptions import ChunkedEncodingError, ConnectionError
class LogMonitor:
"""实时日志监控器"""
def __init__(self, log_url, alert_patterns=None):
"""
初始化日志监控器
:param log_url: 日志流URL
:param alert_patterns: 需要触发告警的模式列表
"""
self.log_url = log_url
self.alert_patterns = alert_patterns or []
self.error_count = 0
self.retry_count = 0
self.max_retries = 5
self.alert_callback = None
def set_alert_callback(self, callback):
"""设置告警回调函数"""
self.alert_callback = callback
def monitor(self):
"""开始监控日志流"""
print(f"开始监控日志流: {self.log_url}")
print(f"监控开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
while self.retry_count < self.max_retries:
try:
with requests.get(
self.log_url,
stream=True,
timeout=(10, 30)
) as response:
response.raise_for_status()
# 重置重试计数
self.retry_count = 0
# 迭代处理日志行
for line in response.iter_lines(decode_unicode=True):
if line:
self.process_log_line(line)
# 如果正常退出循环,说明流结束
print("日志流结束")
break
except (ChunkedEncodingError, ConnectionError) as e:
self.retry_count += 1
print(f"\n连接错误: {str(e)}")
print(f"重试 {self.retry_count}/{self.max_retries}...")
if self.retry_count >= self.max_retries:
print("达到最大重试次数,监控失败")
break
# 指数退避
sleep_time = 2 ** self.retry_count
print(f"等待 {sleep_time} 秒后重试...")
time.sleep(sleep_time)
except Exception as e:
print(f"\n发生未预期错误: {str(e)}")
break
print(f"监控结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"错误总数: {self.error_count}")
def process_log_line(self, line):
"""处理单行日志"""
try:
# 假设日志是JSON格式
log_entry = json.loads(line)
timestamp = log_entry.get('timestamp', datetime.now().isoformat())
level = log_entry.get('level', 'INFO').upper()
# 打印日志(可以替换为更复杂的处理)
print(f"[{timestamp}] [{level}] {log_entry.get('message', '')}")
# 检查错误日志
if level in ['ERROR', 'CRITICAL']:
self.error_count += 1
self.handle_alert(log_entry, f"错误日志: {log_entry.get('message', '')}")
# 检查告警模式
for pattern in self.alert_patterns:
if pattern in log_entry.get('message', ''):
self.handle_alert(log_entry, f"匹配告警模式: {pattern}")
except json.JSONDecodeError:
# 非JSON格式日志,直接处理
print(f"[RAW] {line}")
if any(pattern in line for pattern in self.alert_patterns):
self.handle_alert({'raw_line': line}, f"原始日志匹配告警模式")
except Exception as e:
print(f"处理日志行错误: {str(e)}, 行内容: {line}")
def handle_alert(self, log_entry, message):
"""处理告警"""
print(f"\n=== 告警: {message} ===")
# 调用告警回调函数
if self.alert_callback:
try:
self.alert_callback(log_entry, message)
except Exception as e:
print(f"告警回调执行失败: {str(e)}")
# 使用示例
if __name__ == "__main__":
# 初始化日志监控器
monitor = LogMonitor(
"https://example.com/log-stream",
alert_patterns=["数据库错误", "连接超时", "认证失败"]
)
# 设置告警回调函数
def send_alert(log_entry, message):
"""发送告警通知(示例实现)"""
# 在实际应用中,可以发送邮件、短信或集成到监控系统
print(f"[告警通知] {message}")
monitor.set_alert_callback(send_alert)
# 开始监控
monitor.monitor()
性能优化建议
对于生产环境的流式应用,考虑以下性能优化建议:
- 调整块大小:根据网络条件和数据特性调整
chunk_size参数 - 使用连接池:对于多个流式请求,使用
requests.Session()复用连接 - 异步处理:考虑使用
aiohttp等异步HTTP库处理高并发流 - 数据压缩:启用
gzip压缩减少传输带宽 - 缓冲区管理:合理设置缓冲区大小,平衡内存占用和处理延迟
高级应用:流式请求与异步处理
结合异步框架使用流式请求
虽然Requests本身是同步库,但可以与异步框架结合使用,通过线程池执行阻塞I/O操作:
import asyncio
from concurrent.futures import ThreadPoolExecutor
import requests
class AsyncStreamProcessor:
"""异步流式处理器"""
def __init__(self, max_workers=4):
self.executor = ThreadPoolExecutor(max_workers=max_workers)
async def process_stream(self, url, chunk_processor):
"""异步处理流式响应"""
loop = asyncio.get_event_loop()
# 使用线程池执行阻塞的请求操作
future = loop.run_in_executor(
self.executor,
self._fetch_stream,
url,
chunk_processor
)
await future
def _fetch_stream(self, url, chunk_processor):
"""在线程池中执行的阻塞函数"""
with requests.get(url, stream=True) as response:
response.raise_for_status()
for chunk in response.iter_content(chunk_size=8192):
if chunk:
# 处理数据块(可以是另一个异步函数)
chunk_processor(chunk)
def shutdown(self):
"""关闭线程池"""
self.executor.shutdown()
# 使用示例
async def main():
processor = AsyncStreamProcessor()
# 定义数据块处理函数
def handle_chunk(chunk):
"""处理单个数据块"""
# 实现数据处理逻辑
print(f"处理数据块,大小: {len(chunk)} bytes")
# 处理多个流
await asyncio.gather(
processor.process_stream("https://example.com/stream1", handle_chunk),
processor.process_stream("https://example.com/stream2", handle_chunk)
)
processor.shutdown()
if __name__ == "__main__":
asyncio.run(main())
多流并行处理
在某些场景下,需要同时处理多个数据流。以下是一个多流并行处理的示例:
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
def process_single_stream(url, stream_id):
"""处理单个数据流"""
print(f"开始处理流 {stream_id}: {url}")
try:
with requests.get(url, stream=True) as response:
response.raise_for_status()
count = 0
for chunk in response.iter_content(chunk_size=8192):
if chunk:
# 处理数据块
process_chunk(stream_id, count, chunk)
count += 1
print(f"流 {stream_id} 处理完成,共 {count} 个块")
return {
'stream_id': stream_id,
'status': 'completed',
'chunks_processed': count
}
except Exception as e:
print(f"流 {stream_id} 处理错误: {e}")
return {
'stream_id': stream_id,
'status': 'error',
'error': str(e)
}
def process_chunk(stream_id, chunk_num, data):
"""处理单个数据块"""
# 实现数据块处理逻辑
pass
def parallel_stream_process(streams, max_workers=4):
"""并行处理多个数据流"""
results = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 提交所有流处理任务
futures = {
executor.submit(process_single_stream, url, stream_id): stream_id
for stream_id, url in streams.items()
}
# 处理完成的任务
for future in as_completed(futures):
stream_id = futures[future]
try:
result = future.result()
results.append(result)
except Exception as e:
print(f"获取流 {stream_id} 结果时出错: {e}")
return results
# 使用示例
if __name__ == "__main__":
# 定义要处理的数据流
streams = {
1: 'https://example.com/stream1',
2: 'https://example.com/stream2',
3: 'https://example.com/stream3',
4: 'https://example.com/stream4'
}
# 并行处理数据流
results = parallel_stream_process(streams, max_workers=2)
# 打印结果摘要
print("\n===== 处理结果摘要 =====")
for result in results:
print(f"流 {result['stream_id']}: {result['status']}")
if result['status'] == 'completed':
print(f" 处理块数: {result['chunks_processed']}")
else:
print(f" 错误信息: {result['error']}")
总结与最佳实践
流式请求适用场景总结
分块传输编码和流式请求适用于多种场景,但也有其局限性。以下是主要适用场景和不适用场景的总结:
适用场景:
- 大型文件下载或上传
- 实时数据流(日志、监控数据、社交媒体流)
- 动态生成的内容,无法预先确定长度
- 内存受限环境,无法加载整个响应到内存
- 需要即时处理数据的应用(实时分析、即时通讯)
不适用场景:
- 小文件或短响应(分块 overhead 可能超过收益)
- 需要随机访问响应数据的场景
- 对延迟非常敏感的应用(分块有一定 overhead)
- 不支持 HTTP/1.1 的老旧系统
性能优化 checklist
为确保流式请求应用的最佳性能,请遵循以下 checklist:
- 始终使用上下文管理器(
with语句)处理流式响应 - 根据数据特性合理设置
chunk_size(通常 8KB-1MB) - 实现适当的异常处理和重试机制
- 对长时间运行的流设置合理的超时
- 监控并优化内存使用,避免累积未处理数据
- 考虑使用连接池复用HTTP连接
- 对大型部署,考虑使用异步处理或多线程
- 实现进度追踪和监控机制
未来发展趋势
随着Web技术的发展,流式传输也在不断演进:
-
HTTP/2 和 HTTP/3 的影响:这些新协议对数据传输方式进行了优化,支持多路复用和服务器推送,可能会改变传统的流式处理模式。
-
Server-Sent Events (SSE):作为HTML5标准的一部分,SSE提供了一种更标准化的服务器到客户端的流式通信方式。
-
WebSockets:对于需要双向实时通信的场景,WebSockets提供了全双工通信通道,是流式HTTP的重要补充。
Requests库也在不断发展以适应这些变化。通过关注其官方文档和源码(如src/requests/sessions.py和src/requests/adapters.py),可以了解最新的功能和最佳实践。
掌握流式请求和分块传输编码技术,将为你构建高性能、可扩展的Web应用提供强大支持。无论是处理大型文件、实现实时数据更新,还是构建复杂的数据流处理系统,Requests的流式功能都能帮助你以高效、优雅的方式解决问题。
参考资料
- Requests官方文档: docs/user/advanced.rst
- Requests源码: src/requests/models.py
- HTTP分块传输编码规范: RFC 7230
- Requests异常处理: src/requests/exceptions.py
【免费下载链接】requests 项目地址: https://gitcode.com/gh_mirrors/req/requests
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



