如何让Python的ElementTree在XML文件中进行漂亮的打印输出?

82

背景

我正在使用SQLite来访问数据库并检索所需的信息。我正在使用Python 2.6版本中的ElementTree来创建包含该信息的XML文件。

代码

import sqlite3
import xml.etree.ElementTree as ET

# NOTE: Omitted code where I acccess the database,
# pull data, and add elements to the tree

tree = ET.ElementTree(root)

# Pretty printing to Python shell for testing purposes
from xml.dom import minidom
print minidom.parseString(ET.tostring(root)).toprettyxml(indent = "   ")

#######  Here lies my problem  #######
tree.write("New_Database.xml")

尝试

我曾经尝试使用tree.write("New_Database.xml", "utf-8")来替换上面的最后一行代码,但它并没有编辑XML文件的布局 - 它仍然是一团糟。

我还试图摆弄过,并尝试执行如下操作:
tree = minidom.parseString(ET.tostring(root)).toprettyxml(indent = " ")
代替将其打印到Python shell上,但出现了错误 AttributeError: 'unicode' object has no attribute 'write'.

问题

当我将树写入XML文件的最后一行时,有没有办法让XML文件像在Python shell中那样漂亮地打印?

我可以在这里使用toprettyxml() 吗,或者有其他方法可以做到这一点?


8个回答

90

我使用indent()函数解决了这个问题:

xml.etree.ElementTree.indent(tree, space=" ", level=0)会向子树添加空白以在视觉上缩进树形结构。这可用于生成漂亮格式的XML输出。其中tree可以是ElementElementTree。默认情况下,每一级缩进都会插入两个空格字符,space参数确定缩进字符串。对于已缩进树内部的局部子树缩进,请将初始缩进级别作为level参数传递。

tree = ET.ElementTree(root)
ET.indent(tree, space="\t", level=0)
tree.write(file_name, encoding="utf-8")

注意,indent() 函数是在 Python 3.9 中添加的。


33
值得一提的是,indent() 函数是在 Python 3.9 中添加的。 - mzjn
8
你就是那个人,确切地说就是那个人。这绝对是最好的答案。 - rjurney
3
请注意,@Tatarize的答案实际上为此提供了一个polyfill,可在旧版本的Python上运行。 - ntninja
我也非常感激这个答案。 - Drew

85

无论您的XML字符串是什么,您都可以通过打开一个用于写入的文件并将字符串写入该文件来将其写入您选择的文件中。

from xml.dom import minidom

xmlstr = minidom.parseString(ET.tostring(root)).toprettyxml(indent="   ")
with open("New_Database.xml", "w") as f:
    f.write(xmlstr)

在 Python 2 中特别需要注意的一点是 Unicode 字符串处理更加宽松且不够复杂,这可能引发一些意料之外的问题。如果 toprettyxml 方法返回了一个 Unicode 字符串 (u"something"),你可能需要将其转换为适当的文件编码,例如 UTF-8。例如,用以下代码替换原来的一行写入命令:

f.write(xmlstr.encode('utf-8'))

4
如果你加入似乎是必需的 import xml.dom.minidom as minidom 语句,这个答案会更清晰。 - Ken Pronovici
@KenPronovici 可能。该导入项出现在原始问题中,但我将其添加到此处以避免混淆。 - Jonathan Eunice
这个答案在任何类型的问题上都被反复提到,但它绝不是一个好答案:你需要完全将整个XML树转换为字符串,重新解析它,再次打印它,这次只是以不同的方式。这不是一个好的方法。相反,使用lxml并直接使用lxml提供的内置方法进行序列化,从而消除任何中间打印和重新解析。 - Regis May
4
这是一个关于如何将序列化的XML写入文件的答案,而不是对提问者的序列化策略的认可,这个策略无疑很复杂。我喜欢使用“lxml”,但它是基于C语言的,因此并不总是可用。 - Jonathan Eunice
如果您不想要minidom添加的XML版本标签,您可以将其更改为f.write(xmlstr.split('\n', 1)[1]) - Nic Scozzaro
显示剩余3条评论

15
我找到了一种使用ElementTree的直接方法,但它相当复杂。
ElementTree有编辑元素文本和尾部的函数,例如element.text="text"element.tail="tail"。您必须以特定的方式使用它们才能使事情排列整齐,因此请确保您了解转义字符。
以下是一个基本示例:
我有以下文件:
<?xml version='1.0' encoding='utf-8'?>
<root>
    <data version="1">
        <data>76939</data>
    </data>
    <data version="2">
        <data>266720</data>
        <newdata>3569</newdata>
    </data>
</root>

为了将第三个元素插入并保持美观,您需要以下代码:
addElement = ET.Element("data")             # Make a new element
addElement.set("version", "3")              # Set the element's attribute
addElement.tail = "\n"                      # Edit the element's tail
addElement.text = "\n\t\t"                  # Edit the element's text
newData = ET.SubElement(addElement, "data") # Make a subelement and attach it to our element
newData.tail = "\n\t"                       # Edit the subelement's tail
newData.text = "5431"                       # Edit the subelement's text
root[-1].tail = "\n\t"                      # Edit the previous element's tail, so that our new element is properly placed
root.append(addElement)                     # Add the element to the tree.

要缩进内部标签(如内部数据标签),必须将其添加到父元素的文本中。如果您想在元素后缩进任何内容(通常是子元素后),请将其放在tail中。
当您将此代码写入文件时,它会产生以下结果:
<?xml version='1.0' encoding='utf-8'?>
<root>
    <data version="1">
        <data>76939</data>
    </data>
    <data version="2">
        <data>266720</data>
        <newdata>3569</newdata>
    </data> <!--root[-1].tail-->
    <data version="3"> <!--addElement's text-->
        <data>5431</data> <!--newData's tail-->
    </data> <!--addElement's tail-->
</root>

作为另一个注意事项,如果您希望程序统一使用 \t,您可能需要首先将文件解析为字符串,并将所有空格替换为缩进的 \t
此代码是在Python3.7中编写的,但仍可在Python2.7中使用。

4
如果不必手动缩进的话就太好了。 - Sandrogo
2
太棒了! 这就是专注的表现! - Ender
@Sandrogo 我使用相同的方法作为对树进行函数调用的答案。 - Tatarize

15
基于本·安德森的函数回答进行延伸。
def _pretty_print(current, parent=None, index=-1, depth=0):
    for i, node in enumerate(current):
        _pretty_print(node, current, i, depth + 1)
    if parent is not None:
        if index == 0:
            parent.text = '\n' + ('\t' * depth)
        else:
            parent[index - 1].tail = '\n' + ('\t' * depth)
        if index == len(parent) - 1:
            current.tail = '\n' + ('\t' * (depth - 1))

因此在不规整的数据上运行测试:

import xml.etree.ElementTree as ET
root = ET.fromstring('''<?xml version='1.0' encoding='utf-8'?>
<root>
    <data version="1"><data>76939</data>
</data><data version="2">
        <data>266720</data><newdata>3569</newdata>
    </data> <!--root[-1].tail-->
    <data version="3"> <!--addElement's text-->
<data>5431</data> <!--newData's tail-->
    </data> <!--addElement's tail-->
</root>
''')
_pretty_print(root)

tree = ET.ElementTree(root)
tree.write("pretty.xml")
with open("pretty.xml", 'r') as f:
    print(f.read())

我们得到:
<root>
    <data version="1">
        <data>76939</data>
    </data>
    <data version="2">
        <data>266720</data>
        <newdata>3569</newdata>
    </data>
    <data version="3">
        <data>5431</data>
    </data>
</root>

4
与其他答案相比,这个解决方案有几个不错的特点。它不需要额外的库; 它适用于Python 3.9之前的版本,并且非常明确地说明了它在树中添加了哪些空格(这真的有助于理解手头的问题)。哦,它生成的XML与我手工制作的参考文件完全一致 :-) - BertD

10
安装bs4
pip install bs4

使用此代码进行格式化输出:

from bs4 import BeautifulSoup

x = your xml

print(BeautifulSoup(x, "xml").prettify())

4
当我们不想将XML写入文件时,这是一个很好的解决方案。 - FearlessFuture
2
当我尝试这个时,我遇到了一个错误:“找不到具有所请求功能的树构建器:xml。您需要安装解析器库吗?” 我有有效的XML字符串格式。我需要做些什么? - Tim
1
@Tim,你需要安装一个解析器库,例如 lxmlhtml5lib,使用你通常使用的 pipbrewconda 方法。 - PatrickT

8
如果想要使用lxml,可以按照以下方式进行操作:
from lxml import etree

xml_object = etree.tostring(root,
                            pretty_print=True,
                            xml_declaration=True,
                            encoding='UTF-8')

with open("xmlfile.xml", "wb") as writter:
    writter.write(xml_object)`

如果您看到XML命名空间,例如py:pytype="TREE",则可能需要在创建xml_object之前添加。
etree.cleanup_namespaces(root) 

这应该足以适应您代码中的任何变化。

尝试过这个,但根必须是lxml的一部分,而不是ETtree。 - Manabu Tokunaga
@ManabuTokunaga,我不太确定你的意思。我相信我已经使用了objectifyetree进行了测试。我有机会时会再次确认,但最好澄清一下如何直接从lxml创建根对象。 - Nick
4
让我看看能否生成一个孤立的案例。但重点是,我有一个基于导入xml.etree.ElementTree as ETree 的根,在尝试您的建议时出现了一些错误信息。 - Manabu Tokunaga
1
@ManabuTokunaga 是正确的 - ETree 根元素的类型是 xml.etree.ElementTree.Element,但 lxml 根元素的类型是 lxml.etree._Element - 完全不同的类型。另外,在使用 Python 3.8 和 lxml 时,我必须添加以下代码: xmlstr = xmlstr.decode("utf-8")tostring 后面。 - Chris Wolf

2

一行代码(*)用于读取、解析(一次)和漂亮打印名为fname的文件中的XML:

from xml.dom import minidom
print(minidom.parseString(open(fname).read()).toprettyxml(indent="  "))

(不包括导入)


1
为什么这不是正确的答案? - Abhiroj Panwar
写另一个文件可能是不必要的,而且注释也没有说明如何从 ElementTree 中获取字符串。此外,使用(损坏的)minidom 解析器是多余的。 例如,参见此错误 https://bugs.python.org/issue23847。 - Jay-Pi
是的,我知道minidom存在问题,但是在没有现代python或无法安装和使用库的系统上运行时选项不多。@Jay-Pi 不确定“编写另一个文件可能是不必要的”意味着什么,此代码仅打印到stdout。 - qneill
1
"没有现代的Python版本"。最好附上版本兼容性,因为ETree没有破损的漂亮打印。"不确定"写另一个文件可能是不必要的"意味着什么,这段代码只打印到标准输出(stdout)"。我的观点是,除非utf8/utf16编码出现问题,否则写入变量与写入文件相同。 - Jay-Pi

2

类似于 Rafal.Py 的解决方案,但不会修改输入并将 XML 作为格式化字符串返回:

def prettyPrint(element):
    encoding = 'UTF-8'
    # Create a copy of the input element: Convert to string, then parse again
    copy = ET.fromstring(ET.tostring(element))
    # Format copy. This needs Python 3.9+
    ET.indent(copy, space="    ", level=0)
    # tostring() returns a binary, so we need to decode it to get a string
    return ET.tostring(copy, encoding=encoding).decode(encoding)

如果你需要一个文件,可以将最后一行替换为copy.write(...)以避免额外的开销。

你的回答相较于Rafal的ET.indent() 回答,有何优化之处?Rafal的回答是一年前发布的。 - maxschlepzig
@maxschlepzig的回答不再适用于Python 3.9。 - Aaron Digulla
真的吗?当他发布了一个使用Python 3.9中引入的ET.indent()的答案时,这怎么可能呢...我刚刚在Python 3.11下测试了他的原始编辑,它可以正常工作。 - maxschlepzig
1
@maxschlepzig 好的,我不记得具体细节了,但是我花了很长时间才让这段代码正常工作。我的版本不会修改输入内容,在尝试记录子树时这一点非常重要。此外,他只是将结果写入文件,而我则费了很大劲才将新的 XML 转换为字符串。 - Aaron Digulla
@AaronDigulla 你应该在你的回答中添加评论 "不修改输入" 和 "将 XML 作为字符串获取"。 - qneill

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