必须先检查 response.status 再读取数据,显式解码 bytes 并处理异常:捕获 HTTPError 后 URLError,从 headers 提取 charset 或 fallback utf-8,用 errors='replace' 避免解码失败。

能直接用 urllib.request 从 URL 读取数据,但不加异常处理和编码判断,十次有八次会卡在乱码、404、超时或重定向上。
用 urllib.request.urlopen() 读取响应内容前必须检查状态码
很多人直接 urlopen(url).read(),结果遇到 403、404 或 502 时程序直接抛异常中断。正确做法是先检查 response.status:
import urllib.request
<p>url = "<a href="https://www.php.cn/link/4d2fe2e8601f7a8018594d98f28706f2">https://www.php.cn/link/4d2fe2e8601f7a8018594d98f28706f2</a>"
try:
response = urllib.request.urlopen(url)
if response.status == 200:
data = response.read()</p><h1>后续处理</h1><pre class="brush:python;toolbar:false;">else:
print(f"HTTP {response.status}: {response.reason}")
except urllib.error.HTTPError as e: print(f"HTTP error: {e.code} {e.reason}") except urllib.error.URLError as e: print(f"Network error: {e.reason}")
-
HTTPError是URLError的子类,必须先捕获前者,否则会被后者吞掉具体错误码 - 有些服务器返回 200 但实际是 HTML 错误页(比如 Cloudflare 的 502 页面),这时需额外检查
response.headers.get('Content-Type')是否含application/json或text/plain - 不要依赖
response.getcode()—— 它和response.status等价,但前者在某些代理环境下可能返回None
读取文本数据时,response.read() 返回 bytes,必须显式解码
urlopen() 永远返回 bytes,不会自动按 Content-Type 中的 charset 解码。常见坑是直接 .decode() 不指定编码,导致 UnicodeDecodeError:
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
# 错误:没指定编码,且没 fallback
# text = response.read().decode()
<h1>正确:优先从 headers 提取 charset,否则 fallback 到 utf-8</h1><p>content_type = response.headers.get('Content-Type', '')
charset = 'utf-8'
if 'charset=' in content_type:
charset = content_type.split('charset=')[-1].split(';')[0].strip()
text = response.read().decode(charset, errors='replace')</p>
- 很多响应头不带
charset(尤其是 API),此时默认用utf-8是合理选择 -
errors='replace'比'strict'更健壮,避免因个别字节损坏整个读取失败 - 如果目标是 JSON,建议跳过手动 decode,直接用
json.load(response)—— 它内部会处理编码
保存文件时别漏掉 response.headers 里的原始信息
直接 open(..., 'wb').write(response.read()) 能存二进制(如图片、PDF),但丢失了服务端声明的类型和推荐文件名。更稳妥的做法是:
filename = response.headers.get('Content-Disposition')
if filename and 'filename=' in filename:
import re
filename = re.search(r'filename="(.+)"', filename)
filename = filename.group(1) if filename else "download.bin"
else:
from urllib.parse import urlparse
parsed = urlparse(url)
filename = parsed.path.split('/')[-1] or "download.bin"
<p>with open(filename, 'wb') as f:
f.write(response.read())</p>
-
Content-Disposition头里的filename*(RFC 5987 编码)需额外处理,简单场景可忽略,用filenamefallback - 如果 URL 路径末尾无文件名(如
/api/v1/data),不设默认名会导致保存为.bin或空名,Windows 下会报错 - 写入前建议检查磁盘空间和路径权限 ——
urlopen不负责这些,但生产脚本必须考虑
真正麻烦的不是怎么读,而是怎么判断“读到的内容可信”。比如 Content-Length 和实际 body 长度不符、gzip 响应没被自动解压(urlopen 默认不解压)、HTTPS 证书验证绕过等——这些不会报错,但数据已经不对了。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










