忠实地保留解析后XML中的注释

35

在操作 XML 时,我希望尽可能地保留注释。

我成功地保留了注释,但是内容被转义成了 XML。

#!/usr/bin/env python
# add_host_to_tomcat.py

import xml.etree.ElementTree as ET
from CommentedTreeBuilder import CommentedTreeBuilder
parser = CommentedTreeBuilder()

if __name__ == '__main__':
    filename = "/opt/lucee/tomcat/conf/server.xml"

    # this is the important part: use the comment-preserving parser
    tree = ET.parse(filename, parser)

    # get the node to add a child to
    engine_node = tree.find("./Service/Engine")

    # add a node: Engine.Host
    host_node = ET.SubElement(
        engine_node,
        "Host",
        name="local.mysite.com",
        appBase="webapps"
    )
    # add a child to new node: Engine.Host.Context
    ET.SubElement(
        host_node,
        'Context',
        path="",
        docBase="/path/to/doc/base"
    )

    tree.write('out.xml')
#!/usr/bin/env python
# CommentedTreeBuilder.py

from xml.etree import ElementTree

class CommentedTreeBuilder ( ElementTree.XMLTreeBuilder ):
    def __init__ ( self, html = 0, target = None ):
        ElementTree.XMLTreeBuilder.__init__( self, html, target )
        self._parser.CommentHandler = self.handle_comment

    def handle_comment ( self, data ):
        self._target.start( ElementTree.Comment, {} )
        self._target.data( data )
        self._target.end( ElementTree.Comment )

然而,像这样的评论:

  <!--
EXAMPLE HOST ENTRY:
    <Host name="lucee.org" appBase="webapps">
         <Context path="" docBase="/var/sites/getrailo.org" />
     <Alias>www.lucee.org</Alias>
     <Alias>my.lucee.org</Alias>
    </Host>

HOST ENTRY TEMPLATE:
    <Host name="[ENTER DOMAIN NAME]" appBase="webapps">
         <Context path="" docBase="[ENTER SYSTEM PATH]" />
     <Alias>[ENTER DOMAIN ALIAS]</Alias>
    </Host>
  -->

最终成为:

  <!--
            EXAMPLE HOST ENTRY:
    &lt;Host name="lucee.org" appBase="webapps"&gt;
         &lt;Context path="" docBase="/var/sites/getrailo.org" /&gt;
         &lt;Alias&gt;www.lucee.org&lt;/Alias&gt;
         &lt;Alias&gt;my.lucee.org&lt;/Alias&gt;
    &lt;/Host&gt;

    HOST ENTRY TEMPLATE:
    &lt;Host name="[ENTER DOMAIN NAME]" appBase="webapps"&gt;
         &lt;Context path="" docBase="[ENTER SYSTEM PATH]" /&gt;
         &lt;Alias&gt;[ENTER DOMAIN ALIAS]&lt;/Alias&gt;
    &lt;/Host&gt;
   -->

我也尝试在 CommentedTreeBuilder.py 中使用了 self._target.data( saxutils.unescape(data) ),但好像没有起作用。事实上,我认为问题发生在 handle_comment() 步骤之后。

顺便说一句,这个问题类似于这个


1
这在Python 3.8中要容易得多。请参见https://stackoverflow.com/a/59561426/407651。 - mzjn
6个回答

46

经过使用Python 2.7和3.5进行测试,以下代码应该按预期工作。

#!/usr/bin/env python
# CommentedTreeBuilder.py
from xml.etree import ElementTree

class CommentedTreeBuilder(ElementTree.TreeBuilder):
    def comment(self, data):
        self.start(ElementTree.Comment, {})
        self.data(data)
        self.end(ElementTree.Comment)

然后,在主要代码中使用

parser = ElementTree.XMLParser(target=CommentedTreeBuilder())

使用解析器 lxml 代替当前的解析器。

顺便提一下,lxml 可以直接正常使用注释。也就是说,你只需要这样做:

import lxml.etree as ET
tree = ET.parse(filename)

无需以上任何条件。


两种解决方案似乎都保留了注释,谢谢!但是其他元素会被重新格式化(并且属性可能会被重新排序)。我知道这对于机器可读性来说并不重要,但对于我的目的(人类可读性、版本控制和仅触及明确触摸的元素),这很重要。顺便说一句,我的原始版本恰好保留了其他元素(即按照原始格式进行格式化)。这个答案确实回答了我的明确问题,所以它将获得答案奖励,但我想知道是否也可以保留非注释元素的格式。 - Jamie Jackson
1
就我所见,唯一被修改的是属性排序和标签内的空格(如果我漏掉了什么,请纠正我)。你通常不应该拥有或关心后者。由于属性在xml中存储在字典中,因此它们在输出中的顺序是随机的。为了解决这个问题,您可以使用类似于注释的解决方法,参见https://dev59.com/eHE85IYBdhLWcg3wgDzd。或者像使用`lxml`进行快速测试所示的那样,它似乎保留属性顺序并且似乎是可行的解决方案。无论哪种方式,我认为这值得一个单独的SO问题。 - Martin Valgur
好的,除了属性之外,“xml”还会丢弃处理指令标签(例如“<?xml-stylesheet href="mystyle.css" type="text/css"?>”)并重命名xmlns名称空间。而“lxml”则两者都不会做。 - Martin Valgur
5
默认情况下,xmllxml库也会省略<?xml...?><!DOCTYPE ...>声明。这个网页(https://dev59.com/imMl5IYBdhLWcg3wuY_K)和这个网页(https://dev59.com/Omcs5IYBdhLWcg3wSiJo)提供了相应的解决方案。 - Martin Valgur
适用于Python 3.5.4。 - akhan

18

Python 3.8新增了insert_comments参数于TreeBuilder类,其作用是:

class xml.etree.ElementTree.TreeBuilder(element_factory=None, *, comment_factory=None, pi_factory=None, insert_comments=False, insert_pis=False)

insert_comments和/或insert_pis为真时,若注释或处理指令出现在根元素内(而非外部),则它们将被插入到树中。

示例:

parser = ElementTree.XMLParser(target=ElementTree.TreeBuilder(insert_comments=True))

2
在我的电脑上,Python3.8的工作正常。 - Stephane B.
在根元素之外的情况怎么办?有没有办法保留注释? - Aryaman Gupta

5

马丁的回答是正确的,只是缺少一些代码, 我理解对于更有经验的程序员来说可能很显然,但对于我这样的新手程序员需要花费一分钟才能理解: 马丁的回答:

import xml.etree.ElementTree as ET
from xml.etree import ElementTree

class CommentedTreeBuilder(ElementTree.TreeBuilder):
# This class will retain remarks and comments opposed to the xml parser default
    def comment(self, data):
        self.start(ElementTree.Comment, {})
        self.data(data)
        self.end(ElementTree.Comment)

# the missing part:
def parse_xml_with_remarks(filepath):
    ctb = CommentedTreeBuilder()
    xp = ET.XMLParser(target=ctb)
    tree = ET.parse(filepath, parser=xp)
    return tree

# parsing the file, and getting root
tree=parse_xml_with_remarks(file)
root=tree.getroot()

5

马丁的代码对我不起作用。我进行了修改,下面的代码可以按预期工作。

import xml.etree.ElementTree as ET

class CommentedTreeBuilder(ET.XMLTreeBuilder):
    def __init__(self, *args, **kwargs):
        super(CommentedTreeBuilder, self).__init__(*args, **kwargs)
        self._parser.CommentHandler = self.comment

    def comment(self, data):
        self._target.start(ET.Comment, {})
        self._target.data(data)
        self._target.end(ET.Comment)

这是一个测试

    parser=CommentedTreeBuilder()
    tree = ET.parse(filename, parser)
    tree.write('out.xml')

1
这是Python 3.52及以上版本。 - Michael Biniashvili

3

看起来@Martin和@sukhbinder的两个答案对我都不起作用...所以我在Python 3.x上制作了一个可行的完整解决方案

from xml.etree import ElementTree

string = '''<?xml version="1.0"?>
<data>
    <!--Test
    -->
    <country name="Liechtenstein">
        <rank>1</rank>
        <year>2008</year>
        <gdppc>141100</gdppc>
        <neighbor name="Austria" direction="E"/>
        <neighbor name="Switzerland" direction="W"/>
    </country>
</data>'''

class CommentedTreeBuilder(ElementTree.TreeBuilder):
    def comment(self, data):
        self.start(ElementTree.Comment, {})
        self.data(data)
        self.end(ElementTree.Comment)

parser = ElementTree.XMLParser(target=CommentedTreeBuilder())
tree = ElementTree.fromstring(string, parser)
print(tree.find("./*[0]").text)
# or ElementTree.parse(filename, parser)

1
以下方法对我来说直接即可使用(Python 3.9):
from xml.etree.ElementTree import XMLParser, TreeBuilder, parse

ctb = TreeBuilder(insert_comments=True) # This does the trick :)
xp = XMLParser(target=ctb)
mytree = parse('sample.xml', parser=xp)
root = mytree.getroot()
mytree.write('sample_modified.xml')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接