背景
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
,它带有两个属性name
和age
:
<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
,有属性name
和age
,没有属性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到底是什么。本文只讲了一些基础的操作,看完后完全可以自行进官网查看两者的文档,发觉更多其他更操作: