解析XML与(X)HTML实体

问题描述:

试图解析XML,与ElementTree中,包含未定义的实体(即 )提出:解析XML与(X)HTML实体

ParseError: undefined entity  

在Python 2.x的XML实体字典可以通过更新创建解析器(documentation):

parser = ET.XMLParser() 
parser.entity["nbsp"] = unichr(160) 

,但如何做到与Python 3.x中一样吗?


更新:有一个从我身边误解,因为我忽视的是,我试图更新XML实体字典,这是造成误差与分析器之前调用parser.parser.UseForeignDTD(1)。幸运的是,@ m.brindley很有耐心,并指出XML实体字典在Python 3.x中仍然存在,并且可以像在Python 2.x中一样进行更新。

这里的问题是,XML中唯一有效的助记符实体是quot,amp,apos,ltgt。这意味着几乎所有(X)HTML命名实体都必须使用XML 1.1 spec中定义的entity declaration markup在DTD中定义。如果文件是必须是独立的,这应该与内联DTD来完成,像这样:

<?xml version="1.1" ?> 
<!DOCTYPE naughtyxml [ 
    <!ENTITY nbsp "&#0160;"> 
    <!ENTITY copy "&#0169;"> 
]> 
<data> 
    <country name="Liechtenstein"> 
     <rank>1&nbsp;&gt;</rank> 
     <year>2008&copy;</year> 
     <gdppc>141100</gdppc> 
     <neighbor name="Austria" direction="E"/> 
     <neighbor name="Switzerland" direction="W"/> 
    </country> 
</data> 

XMLParserxml.etree.ElementTree使用一个xml.parsers.expat做实际的解析。在XMLParser的init参数中,有一个'predefined HTML entities'的空间,但该参数尚未实现。在init方法中创建一个名为entity的空字典,这是用来查找未定义的实体的。

我不认为expat(通过扩展,ET XMLParser)能够处理切换命名空间到像XHMTL这样的东西来解决这个问题。可能是因为它不会获取外部名称空间定义(我试图使xmlns="http://www.w3.org/1999/xhtml"为数据元素的默认名称空间,但它不能很好地播放),但我无法证实这一点。默认情况下,expat会针对非XML实体引发错误,但您可以通过定义外部DOCTYPE来解决该问题 - 这会导致expat解析器将未定义的实体条目传递回ET.XMLParser_default()方法。

_default()方法在XMLParser实例中查找entity字典,如果它找到匹配键,它将用关联值替换该实体。这将维护问题中提到的Python-2.x语法。

解决方案:

  • 如果数据不具有外部DOCTYPE并具有(X)HTML助记符实体,你的运气了。这是无效的XML和expat是正确的错误。您应该添加一个外部DOCTYPE。
  • 如果数据具有外部DOCTYPE,则可以使用旧语法将助记符名称映射到字符。 注:你应该在py3k使用chr() - unichr()是不是一个有效的名称了
    • 或者,你可以更新XMLParser.entityhtml.entities.html5所有有效的HTML5记忆实体映射到自己的角色。
  • 如果数据是XHTML,您可以继承HTMLParser的子类以处理助记符实体,但不会根据需要返回ElementTree

这里是我使用的片段 - 它通过HTMLParser解析XML与外部DOCTYPE(演示如何添加由子类实体处理),ET.XMLParser与实体映射和expat(这将只是静静地忽略由于未定义的实体外部DOCTYPE)。有一个有效的XML实体(&gt;)和一个未定义的实体(&copy;),我将其映射到chr(0x24B4)ET.XMLParser

from html.parser import HTMLParser 
from html.entities import name2codepoint 
import xml.etree.ElementTree as ET 
import xml.parsers.expat as expat 

xml = '''<?xml version="1.0"?> 
<!DOCTYPE data PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 
<data> 
    <country name="Liechtenstein"> 
     <rank>1&gt;</rank> 
     <year>2008&copy;</year> 
     <gdppc>141100</gdppc> 
     <neighbor name="Austria" direction="E"/> 
     <neighbor name="Switzerland" direction="W"/> 
    </country> 
</data>''' 

# HTMLParser subclass which handles entities 
print('=== HTMLParser') 
class MyHTMLParser(HTMLParser): 
    def handle_starttag(self, name, attrs): 
     print('Start element:', name, attrs) 
    def handle_endtag(self, name): 
     print('End element:', name) 
    def handle_data(self, data): 
     print('Character data:', repr(data)) 
    def handle_entityref(self, name): 
     self.handle_data(chr(name2codepoint[name])) 

htmlparser = MyHTMLParser() 
htmlparser.feed(xml) 


# ET.XMLParser parse 
print('=== XMLParser') 
parser = ET.XMLParser() 
parser.entity['copy'] = chr(0x24B8) 
root = ET.fromstring(xml, parser) 
print(ET.tostring(root)) 
for elem in root: 
    print(elem.tag, ' - ', elem.attrib) 
    for subelem in elem: 
     print(subelem.tag, ' - ', subelem.attrib, ' - ', subelem.text) 

# Expat parse 
def start_element(name, attrs): 
    print('Start element:', name, attrs) 
def end_element(name): 
    print('End element:', name) 
def char_data(data): 
    print('Character data:', repr(data)) 
print('=== Expat') 
expatparser = expat.ParserCreate() 
expatparser.StartElementHandler = start_element 
expatparser.EndElementHandler = end_element 
expatparser.CharacterDataHandler = char_data 
expatparser.Parse(xml) 
+1

感谢您探索问题,但我怀疑Python 3.x SPL不允许更新XML实体表。至少我找不到这样的声明。对不起,但使用正则表达式来准备远程XHTML数据是不可接受的想法。 – theta

+0

我有更多的时间来玩这个,并找出为什么expat不会传递给'XMLParser'中的'_default()'方法。查看我的编辑 - 您可以使用提供外部DOCTYPE的地图实体。 –

+0

我希望这个问题更一般,但让我们更“过滤本地”的问题:我有XHTML数据和'xhtml1-transitional.dtd',我知道只有未定义的XML实体是' '。我默认使用lxml,如果不可用则回退到SPL,但返回ET。 – theta

我有类似的问题,并通过使用lxml绕过它。它的etree.XMLParser有一个recover关键字参数,它强制它试图忽略破损的XML。