【问题标题】:Parse XML with (X)HTML entities使用 (X)HTML 实体解析 XML
【发布时间】:2013-01-22 14:07:07
【问题描述】:

尝试使用 ElementTree 解析包含未定义实体(即 )的 XML:

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 实体 dict 仍然存在于 Python 3.x 中,并且可以像在 Python 2.x 中一样进行更新

【问题讨论】:

    标签: python xml python-3.x elementtree


    【解决方案1】:

    这里的问题是,XML 中唯一有效的助记实体是 quotampaposltgt。这意味着几乎所有 (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>
    

    xml.etree.ElementTree 中的XMLParser 使用xml.parsers.expat 进行实际解析。在XMLParser 的初始参数中,有一个空格用于“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 dict,如果找到匹配的键,它将用关联的值替换实体。这维护了问题中提到的 Python-2.x 语法。

    解决方案:

    • 如果数据没有外部 DOCTYPE 并且有 (X)HTML 助记符实体,那么您就不走运了。它不是有效的 XML,expat 抛出错误是正确的。您应该添加一个外部 DOCTYPE。
    • 如果数据具有外部 DOCTYPE,您可以使用旧语法将助记符名称映射到字符。 注意:您应该在 py3k 中使用 chr() - unichr() 不再是有效名称
      • 或者,您可以将 XMLParser.entity 更新为 html.entities.html5 以将所有有效的 HTML5 助记符实体映射到它们的字符。
    • 如果数据是 XHTML,您可以继承 HTMLParser 来处理助记符实体,但这不会根据需要返回 ElementTree

    这是我使用的 sn-p - 它通过 HTMLParser(演示如何通过子类化添加实体处理)使用外部 DOCTYPE 解析 XML,使用实体映射的 ET.XMLParserexpat(只会静默由于外部 DOCTYPE 而忽略未定义的实体)。有一个有效的 XML 实体 (&amp;gt;) 和一个未定义的实体 (&amp;copy;),我使用 ET.XMLParser 映射到 chr(0x24B4)

    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)
    

    【讨论】:

    • 感谢您探索这个问题,但我怀疑 Python 3.x SPL 不允许更新 XML 实体表。至少我找不到这样的公告。抱歉,使用正则表达式来准备远程 XHTML 数据是不可接受的。
    • 我有更多时间来玩这个,并弄清楚为什么 expat 没有传递到 XMLParser 中的 _default() 方法。请参阅我的编辑 - 只要定义了外部 DOCTYPE,您就可以映射实体。
    • 我希望这个问题更笼统,但让我们更“本地”地过滤问题:我有xhtml1-transitional.dtd 的XHTML 数据,并且我预先知道只有未定义的XML 实体是&amp;nbsp;。我默认使用 lxml,如果不可用则回退到 SPL,但返回 ET。
    • 解析失败到底是怎么回事?如果 XMLParser 获取传递给它的未定义实体并且无法映射它,则引发的错误类似于“未定义实体 %s:行 %d 列 %d”,如果 expat 没有传递它,则 %s 不是包括在内。
    • 这已经包含在我的问题中:ParseError: undefined entity &amp;nbsp;
    【解决方案2】:

    我遇到了类似的问题,并通过使用 lxml 解决了这个问题。它的etree.XMLParser 有一个recover 关键字参数,它强制它尝试忽略损坏的XML。

    【讨论】:

      【解决方案3】:
      from xml.etree import ElementTree
      from html.entities import name2codepoint
      from io import StringIO
      import unicodedata
      
      url = "https://docs.python.org/3/library/html.parser.html"
      headers = {
          'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 \
          (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'
      }
      html_response = requests.get(url=url, headers = headers)
      
      def getParser():
          return xp
      
      with open("sample.html", "w", encoding="utf-8") as html_file:
          html_file.write(html_response.text)
      
      xp = ElementTree.XMLParser()
      for k, v in name2codepoint.items(): xp.entity[k] = chr(v)
      
      with open("sample.html", "r", encoding="utf-8") as html_file:
          html = html_file.read()
          b = StringIO(html)
          t = ElementTree.parse(b, xp)
          print(t)
      

      【讨论】:

      • 请添加一些文字来解释代码的作用以及为什么它解决了问题中描述的问题。
      猜你喜欢
      • 2019-08-10
      • 1970-01-01
      • 2016-08-04
      • 1970-01-01
      • 1970-01-01
      • 2011-11-06
      • 1970-01-01
      • 1970-01-01
      • 2011-01-23
      相关资源
      最近更新 更多