urllib.request.urlopen 默认一次性加载全部响应体,易致内存溢出;须手动分块读取、流式写入,并健壮处理 content-length 缺失、连接中断、磁盘满三类静默失败场景。

urllib.request.urlopen 会一次性加载整个响应体,导致内存爆炸
直接用 urllib.request.urlopen(url).read() 下载大文件(比如几百 MB 或 GB 级),Python 进程会把全部数据读进内存再写入磁盘,极易触发 MemoryError。这不是网络问题,是设计使然:默认不支持流式读取。
必须手动分块读取,配合 Content-Length 头估算总大小,才能边下边写、实时更新进度。
- 先发 HEAD 请求获取
Content-Length(部分服务器不返回,需 fallback 到未知总长模式) - 用
response.read(chunk_size)循环读取,每次最多读 8192 字节(经验值,太小开销大,太大无明显收益) - 写入文件用
open(..., 'wb')和f.write(chunk),避免字符串解码(二进制模式)
如何安全获取文件总大小并处理无 Content-Length 的情况
不是所有 HTTP 响应都带 Content-Length —— 比如分块传输(Transfer-Encoding: chunked)或动态生成的文件。硬依赖这个 header 会导致进度条卡死或报错。
正确做法是先尝试 response.headers.get('content-length'),转成整数;如果为 None 或无法转换,就设 total_size = None,后续进度显示改为「已下载 X MB」而非百分比。
- HEAD 请求可能被服务器拒绝(返回 405),此时改用 GET 并检查
response.headers后立即中断读取(用response.close()) - 某些 CDN 或反向代理会过滤
Content-Length,需接受 fallback 行为 - 不要用
response.length属性——它不可靠,常为-1或None
用 sys.stdout.write 实现简洁不闪烁的进度刷新
频繁打印 print(f'{percent}%') 会造成多行滚动,视觉混乱。真正实用的进度条只占一行,靠 \r 回车符覆盖重写。
注意 Windows 终端对 \r 支持不稳定,建议统一用 sys.stdout.write() + sys.stdout.flush() 控制输出时机,避免缓冲导致延迟。
- 每下载一个 chunk 就刷新一次,但不要高频刷新(比如每 KB 刷一次)——可设阈值,如每 1024 * 1024 字节(1MB)更新一次显示
- 格式建议:
Downloaded: {downloaded_mb:.1f} MB / {total_mb:.1f} MB ({percent}%),其中total_mb为total_size / 1024 / 1024,整数除法易丢精度 - 下载完成末尾补换行
\n,防止光标悬在行中影响后续命令
完整可运行示例(含异常防护和流式写入)
下面代码处理了连接超时、HTTP 错误、磁盘满、键盘中断等常见失败场景,且不依赖第三方库:
import urllib.request
import sys
import os
<p>def download_with_progress(url, filename):
try:</p><h1>先 HEAD 获取 size</h1><pre class="brush:python;toolbar:false;"> head_req = urllib.request.Request(url, method='HEAD')
with urllib.request.urlopen(head_req, timeout=10) as head_resp:
total = head_resp.headers.get('content-length')
total_size = int(total) if total else None
except Exception:
total_size = None # HEAD 失败,退化为无总长模式
try:
with urllib.request.urlopen(url, timeout=30) as response:
with open(filename, 'wb') as f:
downloaded = 0
chunk_size = 8192
while True:
chunk = response.read(chunk_size)
if not chunk:
break
f.write(chunk)
downloaded += len(chunk)
# 每 1MB 刷新一次进度(减少 I/O 频率)
if downloaded % (1024 * 1024) == 0 or not chunk:
mb = downloaded / 1024 / 1024
if total_size:
percent = min(100, int(100 * downloaded / total_size))
sys.stdout.write(f'\rDownloaded: {mb:.1f} MB / {total_size/1024/1024:.1f} MB ({percent}%)')
else:
sys.stdout.write(f'\rDownloaded: {mb:.1f} MB')
sys.stdout.flush()
sys.stdout.write('\n') # 换行收尾
except KeyboardInterrupt:
sys.stdout.write('\nDownload interrupted.\n')
raise
except OSError as e:
if 'No space left on device' in str(e):
raise RuntimeError('Disk full') from e
raise调用示例
download_with_progress('https://www.php.cn/link/83a62ec9cb817600685ad9e72404a513', 'bigfile.zip')
真正难的不是写出来,而是处理 Content-Length 缺失、连接中途断开、磁盘突然写满这三类情况——它们不会报错到控制台第一行,但会让脚本静默失败或残留损坏文件。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











