如何用 Python 解析含 ü 实体的 audio.cdindex?
xml.etree.ElementTree 默认不加载外部 DTD。即使把地址换成本地的 my_private.dtd,其中的实体声明也不会生效。对于这份文件,可以将 uuml 声明放进 DOCTYPE 的内部子集,或把已知的 ü 替换为数值字符引用 ü。
为什么本地 DTD 没有生效?
这条声明引用的是外部 DTD:
<!DOCTYPE CDInfo SYSTEM "my_private.dtd">
ElementTree 不会自动读取它,无论远程 DTD 能否访问、本地文件是否存在。之前其他 CD 能正常解析,是因为它们没有用到需要在外部 DTD 中定义的实体。
XML 预定义的命名实体只有 &、<、>、' 和 "。ü 是常见的 HTML 实体,在 XML 中使用时仍需声明。
可以将声明改为内部子集:
<!DOCTYPE CDInfo [
<!ENTITY uuml "ü">
]>
这样,实体定义就包含在 XML 文档中,ElementTree 可以直接读取。ü 表示 Unicode 字符 U+00FC,也就是 ü,因此 für 会解析为 für。取消内联声明那一行的注释后能解析成功,也是这个原因。
方案一:将外部 DTD 引用替换为内部声明
题目中的 DOCTYPE 只有 SYSTEM 外部引用,没有内部子集,可以在解析前替换这条声明。
建议以二进制方式读取文件,让 XML 解析器根据 encoding="ISO-8859-1" 解码,避免 open(dest) 使用系统默认编码。
from pathlib import Path
import re
import xml.etree.ElementTree as ET
EXTERNAL_DOCTYPE = re.compile(
rb"""<!DOCTYPE\s+CDInfo\s+SYSTEM\s+(?:"[^"]*"|'[^']*')\s*>"""
)
INTERNAL_DOCTYPE = (
b'<!DOCTYPE CDInfo [<!ENTITY uuml "ü">]>'
)
def clean(data):
data, count = EXTERNAL_DOCTYPE.subn(
INTERNAL_DOCTYPE, data, count=1
)
if count != 1:
raise ValueError("未找到预期的 CDInfo SYSTEM 声明")
return data
def readAudioindex(dest):
data = Path(dest).read_bytes()
root = ET.fromstring(clean(data))
album = root.findtext("Title") or ""
artist = root.findtext(".//Artist") or ""
tracks = []
for track in root.iter("Track"):
title = track.findtext("Name") or ""
# 保留原程序按第一个 "/" 拆分艺术家和曲名的约定。
if "/" in title:
track_artist, title = title.split("/", 1)
track_artist = track_artist.strip()
title = title.strip()
else:
track_artist = artist
tracks.append({
"num": int(track.attrib["Num"]),
"tt": title,
"ta": track_artist,
})
return artist, album, tracks
这个匹配规则既适用于原来的 MusicBrainz URL,也适用于示例中的 "my_private.dtd"。它只处理题目展示的外部声明格式,不能用于任意复杂的 DTD。如果文档已有内部子集,应保留原有声明,再补充缺失的实体定义。
原代码中的匹配规则也需要注意:
dtype = re.compile(r"<!DOCTYPE.*musicbrainz.*>")
它只匹配包含 musicbrainz 的单行声明。示例文件改用 "my_private.dtd" 后,clean() 就不会替换这条声明,因此即使启用了内联声明那一行,解析也可能仍然失败。
方案二:只替换已知实体
如果只需处理这一处 ü,可以直接替换为数值字符引用:
from pathlib import Path
import xml.etree.ElementTree as ET
data = Path("audio.cdindex").read_bytes()
data = data.replace(b"ü", b"ü")
root = ET.fromstring(data)
数值字符引用不依赖 DTD,因此可以保留原来的外部 DOCTYPE。ElementTree 仍会忽略外部 DTD,但曲目中的 ü 能正常解析为 ü。
这种做法适用于已确认 ü 出现在普通文本或属性值中的文件。全局替换也会改动注释和 CDATA 中的同名字节串。如果需要完整保留这些内容,使用内部实体声明更合适。
编码和后续处理要注意什么?
不要用默认编码读取声明为 ISO-8859-1 的文件。上述字节读取方式要求文件的实际编码与 XML 声明一致。如果按文本读取,应明确指定编码:
with open(dest, encoding="iso-8859-1") as fd:
text = fd.read()
也不要对整个 XML 调用 html.unescape()。它会同时转换 &、< 等合法的 XML 转义,可能导致正文中的字符被误当成 XML 语法。
添加 uuml 的定义只能解决这一种实体缺失问题。后续如果遇到其他未定义实体,还需要补充对应的声明,或为其指定替换映射。
备注:内容仅供参考。
