xml.etree.ElementTree因严格遵循XML规范而对非法Unicode字符(如U+0000–U+0008、代理对等)直接抛ParseError;必须在decode后、parse前用正则预清洗,推荐re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]', '', text)并补充代理对处理。

为什么xml.etree.ElementTree会因非法字符报ParseError
XML标准只允许特定Unicode范围内的字符:U+0009(Tab)、U+000A(LF)、U+000D(CR)和U+0020–U+D7FF、U+E000–U+FFFD等。但现实数据常混入控制字符(如U+0000–U+0008、U+000B–U+000C、U+000E–U+001F)、代理对(surrogate pairs)或U+FFFE/U+FFFF等非法码点——这些在Python 3.8+的xml.etree.ElementTree中直接触发ParseError: not well-formed (invalid token)。
用正则预清洗再解析是最稳妥的做法
别指望xml.etree.ElementTree自动跳过或替换非法字符;它严格遵循XML规范,遇到就炸。必须在parse()或fromstring()前清洗字符串。核心是保留合法XML字符,剔除或替换其余。
- 推荐用
re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]', '', text)清除常见控制字符(注意:\x00-\x08含空字节,\x7f-\x9f含C1控制符) - 补充处理代理对:
re.sub(r'[\ud800-\udfff]', '', text)(Python内部用UTF-16表示,代理对无法构成有效Unicode) - 若需保留换行/制表符,不要删
\x09\x0a\x0d;误删会导致格式错乱 - 清洗后建议用
text.encode('utf-8').decode('utf-8')验证是否仍为合法UTF-8,避免BOM或编码残留
lxml能自动容错但默认不开启
lxml.etree底层用libxml2,支持更宽松的解析模式,但需显式启用。它不会静默跳过非法字符,而是提供recover=True参数让解析器尽力恢复。
- 用
parser = lxml.etree.XMLParser(recover=True)构造解析器,再传给lxml.etree.parse()或fromstring() - 注意:
recover=True会吞掉非法字符并警告(可通过parser.error_log捕获),但结果树可能缺失内容 - 性能略低于
xml.etree,且lxml需额外安装:pip install lxml - 若原始XML来自不可信来源(如用户上传、老旧系统导出),优先选预清洗+
xml.etree,可控性更强
清洗位置错了会导致二次错误
常见误区是先用open()读文件再清洗——如果文件本身含BOM或编码声明(如<?xml version="1.0" encoding="gbk"?>),而Python以UTF-8打开,就会提前解码失败,根本到不了XML解析阶段。
立即学习“Python免费学习笔记(深入)”;
- 务必在
decode()之后、parse()之前清洗:即raw_bytes → decoded_str → cleaned_str → parse() - 若文件有声明encoding,用对应编码打开(如
open(path, encoding='gbk')),再清洗 - 网络响应体(
response.content)必须先.decode(response.encoding or 'utf-8'),再清洗,不能对bytes直接正则 - 清洗函数别写成“替换为空格”,某些场景(如CDATA内)空格语义敏感,直接删更安全
非法字符位置越靠前,报错越早;但真正麻烦的是藏在文本节点中间的U+0000或U+FFFD——它们不报错却让后续XPath匹配失效,所以清洗不能只看是否抛异常,得全程覆盖所有输入源。


















