背景

Python有三种方法解析xml:SAX,DOM,Elementree。本文记录ElementTree方法解析xml。
目前自己用的是Python3.6,但在该版本中并没有xml的缩进函数ET.indent,不过就我所知3.9版本是有的,所以当前3.6写出来的xml是无法调用函数来美化xml排版,文中的xml排版是手动挡 :)。当然,也可以写个函数来自动优化。

正文

xml是一种固有的分层数据格式,最好的描述方式就是使用树形结构。在ElementTree模块中,使用ElementTree对象来表示一棵树,Element对象来表示树中的一个单一结点。读取、写入一个xml文件一般都是在ElementTree层面上操作,而对xml元素(结点)及其子元素(子结点)的操作是在Element层面上进行。

说明:下面的内容有时候使用明确的node名称来代替Element进行操作,有时候使用Element泛指一个结点。ElementTree和Element是一个类,创建一棵树tree或者一个结点node相当于类的实例化。

解析xml文件

import xml.etree.ElementTree as ET   # 导入ElementTree模块
tree = ET.parse(xml_file_path)       # 解析xml文件,得到树形结构
root = tree.getroot()                # 获取根节点

结点基础:node.tag,node.attrib,node.get(),node.text

每个结点都有标签(tag)和属性(attrib),标签名一般不为空,属性可为空,比如:

<data, attrib=[]>
	<daughter_node, name="child_1", age="20">
		<chichild_node>
		...
		</chichild_node>
	</daughter_node>
	
	...
	
	<daughter_node>2021</daughter_node>
	
	<son_node>
		...
	<son_node>
	
	...
	
	<son_node>
		...
	<son_node>
</data>

上面为一个根结点及其子结点的例子,根结点的标签名为data,没有属性(attrib为空:[])。注意区分结点的属性和结点的子结点,属性包含在结点的括号<>中,而子结点是夹在一对标签内。获取一个结点的标签和属性(此处结点为根节点):

root.tag              # 返回结点的标签名data
root.atrrib			  # 返回结点的属性,此时根节点的属性为[]

而对于非空属性的结点,其属性可以单个或者多个,比如下面这个结点child node,它带有两个属性nameage

<child_node, name="child_1" age="20">
	...
</child_node>

可以使用如下代码来访问child_node属性:

child_node.attrib                    # 返回一个字典,字典包含每个属性名和属性内容
child_node.get("name")               # 返回name属性的属性内容:child_1

假定C结点(node结点)没有子结点,但夹有文本信息:

<node>2021</node>

则要获取node结点的文本信息,有:

node.text            # 返回“2021”

结点拔高:遍历,索引,遍历指定结点Element.iter(),查找指定结点Element.findall()

  • 有子结点的父结点是可迭代循环的,可以用for循环遍历父结点的所有子结点
for child in root:
	print((child.tag, child.attrib))
  • 通过索引的方式获取结点
node = root[0][1]

此处表示返回root结点(A)的第0个子结点(B)的第1个子结点(C),ABC三个的关系是,A是B的父节点,A是C的爷爷节点,B是C的父节点。

  • 指定遍历某一类结点
    假定要遍历上面例子中data结点下的所有daughter_node,则可以使用Element.iter()来指定遍历结点:
for daughter in data.iter("daughter_node"):
	print(daughter.tag)
	print(daughter.attrib)

ps:可以用data.findall()替换data.iter(),进行同样的迭代,但区别在哪里自己探索吧。

创建xml并保存

  • 创建结点:ElementTree.Element()
  • 创建树形结构:ElementTree.ElementTree()
  • 保存为xml文件:ElementTree.write()
  • 添加子结点:ElementTree.append()

要创建xml文件,那首先就要创建一个树形结构,对于树形结构,肯定是在ElementTree这个层次上创建,相当于创建了一个树架子,而Element是创建结点,创建树架子和结点后,你需要将Element结点挂到树架子ElementTree上,因此:

xml_2_path = r"path\to\save\your\xml\file.xml"
root = ET.Element("data", {"year":"2021", "age":"21"})				# 创建根节点
new_tree = ET.ElementTree(element=root)								# 创建树形结构,再将根节点传递到树中
child = ET.Element("child_1")										# 创建一个结点
child.text = "Anya"													# 创界该结点的text内容
root.append(child)													# 将该结点连接到根结点root,此时该结点便成了root的子结点
new_tree.write(xml_2_path)

修改xml

  • 修改文本:node.text = 2022
  • 修改属性:node.set()
  • 移除结点:Element.remove()
  • 查找特定结点:Element.findall()

假定目前已有xml文件如下:

<data, attrib=[]>
	<daughter_node, name="child_1", age="20">
		<chichild_node>
		...
		</chichild_node>
	</daughter_node>
	
	...
	
	<daughter_node>2021</daughter_node>
	
	<son_node>
		...
	<son_node>
	
	...
	
	<son_node>
		...
	<son_node>
</data>
  • 对于创建的Element对象,可以通过直接对其结点域(fields)赋值,达到修改的目的,例如:
daughter_node.text = 2022             # 原值为2021,经赋值后,当前值为2022
  • 对于一个结点的属性,可以使用Element.set()来新增或修改结点属性:
data[0].set("age", "21")
data[0].set("where", "home")

data[0]代表data的第一个子结点daughter_node,有属性nameage,没有属性where,因此上面的代码第一条修改了age属性,将20修改为21;新增了属性where,其属性值为home。此时该节点有三个属性。

  • 假定当前for循环遍历所有son_node,删掉满足判断条件(此处为True)的son_node,那么:
for one_son_node in data.findall("son_node"):
	if True:
		data.remove(one_son_node)

注意,此处不能用data.iter()替代data.findall(),因为后者只是查找,返回的是查找的结果;而前者是迭代,如果在迭代的过程中修改,会导致迭代发生错误

保存修改后的xml树

在经过上面的一系列修改后,此时的xml文件里的内容并没有修改,因此需要将修改后的树重写进文件中:

ElementTree.write(r"path\to\save\your\xml\file.xml")

结语

写到到这里,自己完全明白了怎么建立树形结构、结点及两者的相关操作,能准确区分ElementTree和Element到底是什么。本文只讲了一些基础的操作,看完后完全可以自行进官网查看两者的文档,发觉更多其他更操作: