解析带有未定义实体的XHTML5

2 投票
1 回答
3604 浏览
提问于 2025-04-18 12:03

请考虑以下内容:

import xml.etree.ElementTree as ET

xhtml = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
        <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
        <head><title>XHTML sample</title></head>
            <body>
                <p>&nbsp;Sample text</p>
            </body>
        </html>
'''
parser = ET.XMLParser()
parser.entity['nbsp'] = '&#x00A0;'
tree = ET.fromstring(xhtml, parser=parser)
print(ET.tostring(tree, method='xml'))

这段代码可以很好地将 xhtml 字符串转换成文本表示。

但是,对于同样的 XHTML 文档,如果使用 HTML5 的文档类型声明:

xhtml = '''<!DOCTYPE html>
        <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
        <head><title>XHTML sample</title></head>
            <body>
                <p>&nbsp;Sample text</p>
            </body>
        </html>
'''

我遇到了一个异常:

xml.etree.ElementTree.ParseError: undefined entity: line 5, column 19

这意味着解析器无法处理这个文档,尽管我已经把 nbsp 加入到实体字典中。

如果我使用 lxml,情况也是一样:

from lxml import etree
parser = etree.XMLParser(resolve_entities=False)
tree = etree.fromstring(xhtml, parser=parser)
print etree.tostring(tree, method='xml')

会抛出:

lxml.etree.XMLSyntaxError: Entity 'nbsp' not defined, line 5, column 26

尽管我已经设置了解析器忽略实体。

这是为什么呢?怎样才能让带有 HTML5 文档类型声明的 XHTML 文件解析成功呢?


对于 lxml 的一个部分解决方案是使用恢复器:

parser = etree.XMLParser(resolve_entities=False, recover=True)

不过我仍在等待更好的解决方案。

1 个回答

4

这里的问题是,背后使用的Expat解析器通常不会报告未知的实体,而是会直接抛出错误。因此,你想要触发的xml.etree.ElementTree中的备用代码根本不会运行。你可以使用UseForeignDTD方法来改变这种行为,这样Expat就会忽略文档类型声明,并将所有实体声明传递给xml.etree.ElementTree。以下代码可以正常工作:

import xml.etree.ElementTree as ET

xhtml = '''<!DOCTYPE html>
        <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
        <head><title>XHTML sample</title></head>
            <body>
                <p>&nbsp;Sample text</p>
            </body>
        </html>
'''
parser = ET.XMLParser()
parser._parser.UseForeignDTD(True)
parser.entity['nbsp'] = u'\u00A0'
tree = ET.fromstring(xhtml, parser=parser)
print(ET.tostring(tree, method='xml'))

这种方法的副作用是:正如我所说,文档类型声明会被完全忽略。这意味着你必须声明所有的实体,即使那些本来应该由文档类型覆盖的实体。

需要注意的是,你放入ElementTree.XMLParser.entity字典中的值必须是普通字符串,也就是实体将被替换成的文本——你不能再在这里引用其他实体。因此,对于&nbsp;,它应该是u'\u00A0'

撰写回答