email.parser.Parser 默认不递归解析嵌套 multipart,需用 .walk() 遍历获取所有叶子节点;须用 policy.default、正确处理字符集、手动解析 filename* 和构建 CID 映射。

遇到嵌套 multipart/mixed 或 multipart/alternative 时,email.parser.Parser 为什么只返回顶层结构?
因为默认解析器不递归展开子部件,它把整个 multipart/* 块当做一个 Message 对象,而 .get_payload() 在未指定 decode=True 且内容是 multipart 时,返回的是子部件列表(可能还是未解码的字符串)。实际要遍历所有层级,必须手动调用 .walk()。
推荐做法是直接用 .walk() 遍历,它天然处理嵌套,跳过容器型部件(如 multipart/mixed),只吐出带真实内容的叶子节点:
from email import policy
from email.parser import BytesParser
<p>msg = BytesParser(policy=policy.default).parsebytes(raw_email_bytes)
for part in msg.walk():
if part.get_content_maintype() == 'text' and part.get_content_subtype() in ('plain', 'html'):
payload = part.get_payload(decode=True)
if payload is not None:
text = payload.decode(part.get_content_charset() or 'utf-8', errors='replace')
-
.walk()自动跳过multipart/*和message/rfc822这类容器,省去手动判断is_multipart() - 务必用
policy=policy.default,否则老式email.parser.Parser会丢弃部分头字段(比如Content-Transfer-Encoding),导致get_payload(decode=True)失败 - 不要依赖
part.get_text()—— 它在 multipart/alternative 场景下行为不稳定,可能只取第一个 text/plain 而忽略后续
Content-Transfer-Encoding: base64 解码后乱码?字符集没对上
base64 解码只是第一步,真正出问题的是解码后的字节流用错了字符集还原成字符串。邮件头里声明的 charset 可能缺失、错误,或与实际内容不符(尤其 Outlook 发的邮件常标 gb2312 却塞了 UTF-8 字节)。
安全策略是:先按头信息尝试解码;失败或结果含大量 ,再用 chardet 或 charset_normalizer 探测:
立即学习“Python免费学习笔记(深入)”;
import charset_normalizer
<p>payload = part.get_payload(decode=True)
if payload is None:
continue
detected = charset_normalizer.from_bytes(payload)
charset = detected[0].confidence > 0.7 and detected[0].encoding or 'utf-8'
try:
text = payload.decode(charset, errors='replace')
except (UnicodeDecodeError, LookupError):
text = payload.decode('utf-8', errors='replace')
- 永远优先信任
part.get_content_charset(),但加 try/except 包裹解码过程 - 避免无条件用
payload.decode('utf-8')—— Latin-1 编码的邮件会直接抛异常 -
errors='replace'比'ignore'更稳妥,至少保留可读位置占位符
附件名含中文(Content-Disposition: attachment; filename*=UTF-8''...)怎么正确提取?
这种 RFC 5987 格式的 filename* 字段不能直接用 part.get_filename()——它只读 filename(旧式 ASCII 名),对 filename* 返回 None。
华为云 Python SDK 查询工具。自动发现已安装 SDK 的服务包、Client类、API 方法、入参详情、类信息(__init__ 参数、属性、类型、描述),支持关键字搜索,快速定位目标 API 使用方法。
必须手动解析 Content-Disposition 头:
from email.header import decode_header
<p>def get_filename(part):
cd = part.get('Content-Disposition', '')
if 'filename*=' in cd:</p><h1>提取 filename*=UTF-8''xxx 部分</h1><pre class="brush:php;toolbar:false;"> for item in cd.split(';'):
if item.strip().startswith('filename*='):
encoded = item.strip()[10:].strip("'")
encoding, _, encoded_name = encoded.partition("''")
if encoding.lower() == 'utf-8':
return bytes.fromhex(encoded_name.replace('%', '')).decode('utf-8')
else:
fname = part.get_filename()
if fname:
decoded_fnames = decode_header(fname)
return ''.join(
s.decode(enc or 'utf-8') if enc else s.decode('utf-8', errors='replace')
for s, enc in decoded_fnames
)
return None
-
decode_header()只处理filename中的 RFC 2047 编码(如=?GBK?B?...?=),对filename*无效 -
filename*的值是 URL 编码 + hex 编码混合体,需先去掉%再转 hex 解码 - 有些邮件同时提供
filename和filename*,优先用后者
内嵌图片(Content-ID)和 HTML 中的 <img src="cid:xxx" alt="如何在Python中使用email标准库解析复杂的MIME多段格式邮件?" > 怎么关联?
关键不是“解析”,而是构建映射关系:Content-ID 头的值(去掉两端的 )就是 HTML 里 cid: 后面的 ID。但标准库不自动做绑定,得自己收集成字典。
遍历时缓存所有 image/* 部件,并按 Content-ID 索引:
cid_map = {}
for part in msg.walk():
cid = part.get('Content-ID', '').strip('<>')
if cid and part.get_content_maintype() == 'image':
payload = part.get_payload(decode=True)
if payload:
cid_map[cid] = payload
<h1>后续处理 HTML 时,用正则替换 src="cid:xxx" 为 data URL 或本地路径</h1><p>import re
html = re.sub(r'src="cid:([^"]+)"', lambda m: f'src="data:{part.get_content_type()};base64,{base64.b64encode(cid_map.get(m.group(1), b"")).decode()}"', html)
-
Content-ID必须严格匹配(大小写敏感),且注意前后可能有空格或换行 - HTML 中的
cid:引用不会出现在msg.walk()的非 multipart 部件里,所以必须提前建好cid_map - 别漏掉
part.get_content_type(),它决定 data URL 的 MIME 类型(如image/png)
实际处理一封典型企业邮件时,最易被忽略的是 multipart/related 和 multipart/alternative 的嵌套顺序——前者要求先找 HTML 主体,再按 Content-ID 补资源;后者则需按 text/plain → text/html 优先级选正文。这两个逻辑一旦混用,就会出现正文缺失或图片全挂的情况。

















