luvhl 2020-04-07
VOC数据集的标注文件都是xml格式,最近需要频繁处理xml文件的读取和更新,整理下python处理xml文件的方法。
XML 指可扩展标记语言(eXtensible Markup Language), 被设计用来传输和存储数据。python中有三个模块解析xml文件:DOM, ElementTree,SAX
1. DOM(Document Object Model)
DOM是个跨平台的标准模型,W3C定义了DOM模型里的概念。DOM将XML数据在内存中解析成一个树,通过对树的操作来操作XML。python的xml.dom.minimom模块实现了DOM
1.1 DOM写入xml文件
DOM写入xml文件主要是创建dom树,然后创建根结点,创建子节点并加入到根节点,最后将整个dom树写入文件,相关API如下:
DOM写入相关API doc = minidom.Document() #创建树 doc.createElement("folder") #创建名为folder的结点 doc.createTextNode(‘user‘) #创建文本结点,文本为 user root_node.appendChild(folder_node) #root_node结点添加folder_node为子节点book_node.setAttribute(‘price‘,‘199‘) #book_node设置属性值 # 每一个结点对象(包括dom对象本身)都有输出XML内容的方法,如:toxml()--字符串, toprettyxml()--美化树形格式。 doc.toxml(encoding="utf-8") # 输出字符串 doc.toprettyxml(indent=‘‘, addindent=‘\t‘, newl=‘\n‘, encoding="utf-8")) #输出带格式的字符串 doc.writexml() #将prettyxml字符串写入文件
下面为写入一个VOC标注文件示例代码
def write_xml(): #1. 创建dom树对象 doc = minidom.Document() #2. 创建根结点,并用dom对象添加根结点 root_node = doc.createElement("annotation") doc.appendChild(root_node) #3. 创建结点,结点包含一个文本结点, 再将结点加入到根结点 folder_node = doc.createElement("folder") folder_value = doc.createTextNode(‘user‘) folder_node.appendChild(folder_value) root_node.appendChild(folder_node) filename_node = doc.createElement("filename") filename_value = doc.createTextNode(‘0000001.jpg‘) filename_node.appendChild(filename_value) root_node.appendChild(filename_node) path_node = doc.createElement("path") path_value = doc.createTextNode(‘/home‘) path_node.appendChild(path_value) root_node.appendChild(path_node) source_node = doc.createElement("source") database_node = doc.createElement("database") database_node.appendChild(doc.createTextNode("Unknown")) source_node.appendChild(database_node) root_node.appendChild(source_node) size_node = doc.createElement("size") for item, value in zip(["width", "height", "depth"], [1920, 1080, 3]): elem = doc.createElement(item) elem.appendChild(doc.createTextNode(str(value))) size_node.appendChild(elem) root_node.appendChild(size_node) seg_node = doc.createElement("segmented") seg_node.appendChild(doc.createTextNode(str(0))) root_node.appendChild(seg_node) obj_node = doc.createElement("object") name_node = doc.createElement("name") name_node.appendChild(doc.createTextNode("boat")) obj_node.appendChild(name_node) pose_node = doc.createElement("pose") pose_node.appendChild(doc.createTextNode("Unspecified")) obj_node.appendChild(pose_node) trun_node = doc.createElement("truncated") trun_node.appendChild(doc.createTextNode(str(1))) obj_node.appendChild(trun_node) trun_node = doc.createElement("difficult") trun_node.appendChild(doc.createTextNode(str(0))) obj_node.appendChild(trun_node) bndbox_node = doc.createElement("bndbox") for item, value in zip(["xmin", "ymin", "xmax", "ymax"], [103, 1, 634, 402]): elem = doc.createElement(item) elem.appendChild(doc.createTextNode(str(value))) bndbox_node.appendChild(elem) obj_node.appendChild(bndbox_node) root_node.appendChild(obj_node) with open("0000001.xml", "w", encoding="utf-8") as f: # 4.writexml()第一个参数是目标文件对象,第二个参数是根节点的缩进格式,第三个参数是其他子节点的缩进格式, # 第四个参数制定了换行格式,第五个参数制定了xml内容的编码。 doc.writexml(f, indent=‘‘, addindent=‘\t‘, newl=‘\n‘, encoding="utf-8") # 每一个结点对象(包括dom对象本身)都有输出XML内容的方法,如:toxml()--字符串, toprettyxml()--美化树形格式。 # print(doc.toxml(encoding="utf-8")) # 输出字符串 # print(doc.toprettyxml(indent=‘‘, addindent=‘\t‘, newl=‘\n‘, encoding="utf-8")) #输出带格式的字符串 # doc.writexml() #将prettyxml字符串写入文件
写入xml文件
1.2 读取和更新xml文件
解析xml文件为DOM树,获取树的根节点,随后即可通过根节点寻找相关的子节点,并获取相关的属性和文本,相关API如下:
读取xml的API doc = minidom.parse(xml_path) #解析xml文件(句柄或文件路径) doc = minidom.parseString() #解析xml字符串 root_node = doc.documentElement #获得根节点 print(root_node.nodeName) #结点名称 print(root_node.nodeType) #结点类型 (元素结点,文本结点,属性结点) print(root_node.childNodes) #所有子节点,为列表 print(node.parentNode) # 获取父节点 filename_node = root_node.getElementsByTagName(‘filename‘)[0] #通过结点名称寻找结点,返回列表#文本结点 filename = filename_node.childNodes[0].data #子节点为文本结点,文本结点有data属性即为文本值 #属性结点 # node.getAttribute(‘price‘) #属性结点node,获取其price属性
下面为一个读取xml文件并更新指定结点文本值的代码:
def read_xml(xml_path): with open(xml_path, "r", encoding="utf-8") as f: doc = minidom.parse(xml_path) #解析xml文件(句柄或文件路径) #doc = minidom.parseString() #解析xml字符串 root_node = doc.documentElement #获得根节点 #找到xmin结点并更新其对应的文本值 xmin_node = root_node.getElementsByTagName("xmin")[0] print(xmin_node.childNodes[0].data) xmin_node.childNodes[0].data = str(200) print(xmin_node.childNodes[0].data) with open(xml_path, "w", encoding="utf-8") as f: doc.writexml(f)
2. ElementTree
ElementTree就像一个轻量级的DOM, Python专有,使用起来更加简单,常用API如下:
2.1 读取和解析xml文件
支持遍历结点,查找结点和访问结点,如下所示:
def element_read_xml(xml_path): #1. 获取root结点 tree = ET.parse(xml_path) #方式一 root = tree.getroot() # tree = ET.ElementTree(file=xml_path) # 方式二 # root = tree.getroot() # with open(xml_path, "r", encoding="utf-8") as f: # 方式三 # root = ET.fromstring(f.read()) print(root) #2.访问特定结点属性 (属性包括tag, text, attrib) #遍历结点, 每一个结点都是一个迭代器,能遍历其子节点 for i in root: print(i.tag, i.attrib) for j in i: print(j, j.attrib) #3.下标方式访问子节点 print(root[0].tag, root[0].attrib, root[0].text) #4. 查找结点,支持tag名字和xpath语法 print(tree.find("folder")) #在当前结点的子节点中寻找标签为folder的子节点 print(tree.find(".//name")) #寻找所有子孙结点中第一个标签为name的子节点 print(tree.findall(".//name")) #寻找所有子孙结点中标签为name的子节点(返回列表) print(tree.findtext(".//name")) #寻找所有子孙结点中第一个标签为name的子节点,并返回其text属性
2.2 创建xml文件并写入
需要创建根结点,然后添加子节点,最后创建节点树并写入,如下所示:
def write_xml(): root = ET.Element("node") folder_node = ET.Element("folder") folder_node.text = "/home" folder_node.tail = "\n" print(dir(folder_node)) root.append(folder_node) #添加子节点 #extend(subments) #添加多个子节点 elem3 = ET.Element("test_extend") elem3.text = "elem 3" elem3.tail = "\n" #结点尾部添加换行 elem4 = ET.Element("test_extend") elem4.text = "elem 4" elem4.tail = "\n" root.extend([elem3, elem4]) #insert(index, subment) #插入子节点 #remove(subment) #删除子节点 folder_node = ET.SubElement(root, "folder") # 为root添加子节点 folder_node.text = "/home" tree = ET.ElementTree(root) tree.write("output.xml", encoding="utf-8", xml_declaration=True) #保存时无缩进,添加缩进需要借用dom #借用dom,添加缩进 # rawtext = ET.tostring(root) # dom = minidom.parseString(rawtext) # with open("output.xml", "w") as f: # dom.writexml(f, indent="\t", newl="", encoding="utf-8")
2.3 读取并更新结点
除了下面修改结点的名称外,还可以进行添加子节点,删除子节点等操作
def update_xml(): #查找节点并更新 root = ET.parse("output.xml") for node in root.findall(".//folder"): if node.text == "/home": node.tag = "path" ET.dump(root) #打印xml root.write("output.xml")
3. SAX(Simple API for XML)
而SAX是一种基于事件的流式处理模型,可以在只读入部分XML的情况下进行处理, 比较快,占用内存少。DOM与etree一般都比SAX简单, 但其将XML数据映射到内存中的树,比较慢,且较耗内存;如果XML文件比较大,或要求速度快时,SAX比较适合;
利用SAX解析XML文档牵涉到两个部分:解析器和事件处理器。解析器负责读取XML文档,并向事件处理器发送事件,如元素开始跟元素结束事件;而事件处理器则负责对事件作出相应,对传递的XML数据进行处理
3.1 解析器:
#创建解析器 #1 xml.sax.parse( xmlfile, contenthandler[, errorhandler]) xmlfile - xml文件名 contenthandler - 必须是一个ContentHandler的对象 errorhandler - 如果指定该参数,errorhandler必须是一个SAX ErrorHandler对象 #2 xml.sax.parseString(xmlstring, contenthandler[, errorhandler]) xmlstring - xml字符串 contenthandler - 必须是一个ContentHandler的对象 errorhandler - 如果指定该参数,errorhandler必须是一个SAX ErrorHandler对象 #3 xml.sax.make_parser()
3.2 事件处理器:
class EventHandler(sax.ContentHandler): def __init__(self): pass #文档启动的时候调用 def startDocument(self): pass #解析器到达文档结尾时调用 def endDocument(self): pass # 遇到XML开始标签时调用,name是标签的名字,attrs是标签的属性值字典。 def startElement(self, name, attrs): pass #遇到XML结束标签时调用。 def endElement(self, tag): pass #标签之间的内容处理时调用 def characters(self, content): pass
使用示例参考:
#!/usr/bin/python # -*- coding: UTF-8 -*- import xml.sax class MovieHandler( xml.sax.ContentHandler ): def __init__(self): ? self.CurrentData = "" ? self.type = "" ? self.format = "" ? self.year = "" ? self.rating = "" ? self.stars = "" ? self.description = "" # 元素开始事件处理 def startElement(self, tag, attributes): ? self.CurrentData = tag ? if tag == "movie": ? print "*****Movie*****" ? title = attributes["title"] ? print "Title:", title # 元素结束事件处理 def endElement(self, tag): ? if self.CurrentData == "type": ? print "Type:", self.type ? elif self.CurrentData == "format": ? print "Format:", self.format ? elif self.CurrentData == "year": ? print "Year:", self.year ? elif self.CurrentData == "rating": ? print "Rating:", self.rating ? elif self.CurrentData == "stars": ? print "Stars:", self.stars ? elif self.CurrentData == "description": ? print "Description:", self.description ? self.CurrentData = "" # 内容事件处理 def characters(self, content): ? if self.CurrentData == "type": ? self.type = content ? elif self.CurrentData == "format": ? self.format = content ? elif self.CurrentData == "year": ? self.year = content ? elif self.CurrentData == "rating": ? self.rating = content ? elif self.CurrentData == "stars": ? self.stars = content ? elif self.CurrentData == "description": ? self.description = content if ( __name__ == "__main__"): # 创建一个 XMLReader parser = xml.sax.make_parser() # turn off namepsaces parser.setFeature(xml.sax.handler.feature_namespaces, 0) # 重写 ContextHandler Handler = MovieHandler() parser.setContentHandler( Handler ) parser.parse("movies.xml")
参考博客:
https://www.zhihu.com/question/21824329