《Python 3程序开发指南(第2版•修订版)》——7.3 写入与分析XML文件

简介:

本节书摘来自异步社区《Python 3程序开发指南(第2版•修订版)》一书中的第7章,第7.3节,作者[英]Mark Summerfield,王弘博,孙传庆 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

7.3 写入与分析XML文件

有些程序将其处理的所有数据都使用XML文件格式,还有些其他程序将XML用作一种便利的导入/导出格式。即便程序的主要格式是文本格式或二进制格式,导入与导出XML的能力也是有用的,并且始终是值得考虑的一项功能。

Python提供了3种写入XML文件的方法:手动写入XML;创建元素树并使用其write()方法;创建DOM并使用其write()方法。XML文件的读入与分析则有4种方法:人工读入并分析XML(不建议采用这种方法,这里也没有进行讲述——正确处理某些更晦涩和更高级的可能是非常困难的);使用元素树;DOM(文档对象模型);SAX(Simple API for XML,用于XML的简单API)分析器。

图7-5给出了航空器事故记录的XML格式。在本节中,我们就来展示如何手动写入XML格式与如何使用元素树、DOM写入,以及如何使用元素树、DOM、SAX分析器读入并分析XML文件。如果你并不关心采用哪种方法读、写XML文件,就可以在阅读完“元素树”小节,直接跳到本章的7.4节(随机存取二进制文件)。

7.3.1 元素树

使用元素树写入XML数据分为两个阶段:首先,要创建用于表示XML数据的元素树;之后,将元素树写入到文件中。有些程序可能使用元素树作为其数据结构,这种情况下,第一阶段可以省略,只需要直接写入数据。我们分两个部分来查看export_xml_etree()方法:

def export_xml_etree(self, filename):
    root = xml.etree.ElementTree.Element("incidents")
    for incident in self.values():
        element = xml.etree.ElementTree.Element("incident",
                 report_id=incident.report_id,
                 date=incident.date.isoformat(),
                 aircraft_id=incident.aircraft_id,
                 aircraft_type=incident.aircraft_type,
                 pilot_percent_hours_on_type=str(
                         incident.pilot_percent_hours_on_type),
                 pilot_total_hours=str(incident.pilot_total_hours),
            midair=str(int(incident.midair)))
    airport = xml.etree.ElementTree.SubElement(element,
                                                "airport")
    airport.text = incident.airport.strip()
    narrative = xml.etree.ElementTree.SubElement(element,
                                                  "narrative")
    narrative.text = incident.narrative.strip()
    root.append(element)
tree = xml.etree.ElementTree.ElementTree(root)


screenshot

我们从创建根元素()开始,之后对所有事故记录进行迭代。对每条事故记录,我们创建一个元素()来存放该事故记录的数据,并使用关键字参数来提供属性。所有属性必须都是文本,因此,我们需要对日期、数值型数据、布尔型数据项进行相应转换。我们不必担心对“&”、“<”、“>”(或属性值中的引号)的转义处理,因为元素树模块(以及SOM、SAX模块)会对相关的详细资料进行自动处理。

每个包含两个子元素,一个用于存放机场名,另一个用于存放叙述性文本。创建子元素时,必须为其提供父元素与标签名。元素的读/写text属性则用于存放其文本。

及其所有属性、子元素与创建之后,我们将其添加到树体系的根()元素,反复进行这一过程,最终的元素体系中就包含了所有事故记录数据,这些数据可以转换为元素树。

try:
     tree.write(filename, "UTF-8")
except EnvironmentError as err:
     print("{0}: import error: {1}".format(
          os.path.basename(sys.argv[0]), err))
     return False
return True

写入XML数据来表示一个完整的元素树,实际上只是使用给定的编码格式将元素树本身写入到文件中。

到现在为止,在指定编码格式时,我们几乎总是使用字符串"utf8",这对Python内置的open()函数而言是可以正常工作的,该函数可以接受很多种编码方式以及这些编码方式名称的变种,比如“UTF-8”、“UTF8”、“utf-8”以及“utf8”。但对XML文件而言,编码方式名称只能是正式名称,因此,“utf8”是不能接受的,这也是为什么我们严格地使用“UTF-8”。1

使用元素树读取XML文件并不比写入难多少,也分为两个阶段:首先读入并分析XML文件,之后对生成的元素树进行遍历,以便读取数据来生成incidents字典。同样地,如果元素树本身已经是内存中存储的数据结构,第二阶段就不是必要的。下面分两部分给出import_xml_etree()方法。

def import_xml_etree(self, filename):
    try:
        tree = xml.etree.ElementTree.parse(filename)
    except (EnvironmentError,
            xml.parsers.expat.ExpatError) as err:
        print("{0}: import error: {1}".format(
             os.path.basename(sys.argv[0]), err))
        return False

默认情况下,元素树分析器使用expat XML分析器,这也是为什么我们必须做好捕获expat异常的准备。

self.clear()
for element in tree.findall("incident"):
    try:
        data = {}
        for attribute in ("report_id", "date", "aircraft_id",
               "aircraft_type",
               "pilot_percent_hours_on_type",
               "pilot_total_hours", "midair"):
            data[attribute] = element.get(attribute)
        data["date"] = datetime.datetime.strptime(
                           data["date"], "%Y-%m-%d").date()
        data["pilot_percent_hours_on_type"] = (
                float(data["pilot_percent_hours_on_type"]))
        data["pilot_total_hours"] = int(
                data["pilot_total_hours"])
        data["midair"] = bool(int(data["midair"]))
        data["airport"] = element.find("airport").text.strip()
        narrative = element.find("narrative").text
        data["narrative"] = (narrative.strip()
                           if narrative is not None else "")
        incident = Incident(**data)
        self[incident.report_id] = incident
    except (ValueError, LookupError, IncidentError) as err:
        print("{0}: import error: {1}".format(
            os.path.basename(sys.argv[0]), err))
        return False
return True

准备好元素树之后,就可以使用xml.etree.ElementTree.findall()方法对每个进行迭代处理了。每个事故都是以一个xml.etree.Element对象的形式返回的。在处理元素属性时,我们使用的是与前面import_text_regex()方法中同样的技术——我们首先将所有值存储到data字典中,之后将日期、数字、布尔型值转换到正确的类型。对机场属性与叙述性文本元素,我们使用xml.etree.Element.find()方法寻找这些值,并读取其text属性。如果某个文本元素不包含文本,那么其text属性将为None,因此,在读取叙述性文本元素时,我们必须考虑这一点,因为该元素可以为空。在所有情况下,返回给我们的属性值与文本都不包含XML转义,因为其是自动非转义的。

与用于处理航空器事故数据的所有XML分析器类似,如果航空器或叙述性文本元素丢失,或某个属性丢失,或某个转换过程失败,或任意的数值型数据超出了取值范围,都会产生异常——这将确保无效数据被终止分析并输出错误消息。用于创建并存储事故记录以及处理异常的代码与前面看到的相同。

7.3.2 DOM

DOM是一种用于表示与操纵内存中XML文档的标准API。用于创建DOM并将其写入到文件的代码,以及使用DOM对XML文件进行分析的代码,在结构上与元素树代码非常相似,只是稍长一些。

我们首先分两个部分查看export_xml_dom()方法。这一方法分为两个阶段:首先创建一个DOM来表示事故记录数据,之后将该DOM写入到文件。就像使用元素树写入时一样,有些程序可能使用DOM作为其数据结构,在这种情况下可以省略第一步,直接写入数据。

def export_xml_dom(self, filename):
    dom = xml.dom.minidom.getDOMImplementation()
    tree = dom.createDocument(None, "incidents", None)
    root = tree.documentElement
    for incident in self.values():
        element = tree.createElement("incident")
        for attribute, value in (
               ("report_id", incident.report_id),
               ("date", incident.date.isoformat()),
               ("aircraft_id", incident.aircraft_id),
               ("aircraft_type", incident.aircraft_type),
               ("pilot_percent_hours_on_type",
                str(incident.pilot_percent_hours_on_type)),
               ("pilot_total_hours",
                str(incident.pilot_total_hours)),
               ("midair", str(int(incident.midair)))):
        element.setAttribute(attribute, value)
    for name, text in (("airport", incident.airport),
                     ("narrative", incident.narrative)):
        text_element = tree.createTextNode(text)
        name_element = tree.createElement(name)
        name_element.appendChild(text_element)
        element.appendChild(name_element)
    root.appendChild(element)

该方法从获取一个DOM实现开始,默认情况下,DOM实现是由expat XML分析器提供的,xml.dom.minidom模块提供了一个比xml.dom模块所提供的更简单、更短小的DOM实现,尽管该模块使用的对象来自于xml.dom模块。获取了DOM实现后,我们可以创建一个文档。xml.dom.DOMImplementation.createDocument()的第一个参数是名称空间URI——我们并不需要,因此将其赋值为None;第二个参数是一个限定名(根元素的标签名);第三个参数是文档类型,同样,也将其赋值为None,因为我们没有文档类型。在获取了表示文档的树之后,我们取回根元素,之后对所有事故记录进行迭代。

对每个事故记录,我们创建一个元素,对事故的每个属性,我们使用该属性名与值调用setAttribute()。就像元素树中一样,我们也不需要担心“&”、“<”与“>”(或属性值中的引号)的转义问题。对机场与叙述性文本元素,我们必须创建一个文本元素来存放文本,并以一个通常的元素(带有适当的标签名)作为文本元素的父亲——之后,我们将该通常元素(及其包含的文本元素)添加到当前的事故元素中。事故元素完整后,就将其添加到根。

fh = None
try:
    fh = open(filename, "w", encoding="utf8")
    tree.writexml(fh, encoding="UTF-8")
    return True

我们没有给出except语句块以及finally语句块,因为这与我们前面已经看到的都是相同的。从上面的代码中可以清晰看到的是,内置的open()函数使用的编码字符串与用于XML文件的编码字符串之间的差别,这一点在前面也已讨论。

将XML文档导入到DOM中与导入到元素树中是类似的,但与从元素树中导出类似,导入到DOM也需要更多的代码。我们将分3个部分来查看import_xml_dom()函数,下面先给出其def行以及嵌套的get_text()函数。

def import_xml_dom(self, filename):
    def get_text(node_list):
        text = []
        for node in node_list:
            if node.nodeType == node.TEXT_NODE:
                text.append(node.data)
        return "".join(text).strip()

get_text()函数在一个节点列表(比如某节点的子节点)上进行迭代,对每个文本节点,提取该节点的文本并将其附加到文本列表中。最后,该函数返回已收集到一个单独的字符串中的所有文本,并且剥离掉两端的空白字符。

try:
    dom = xml.dom.minidom.parse(filename)
except (EnvironmentError,
        xml.parsers.expat.ExpatError) as err:
    print("{0}: import error: {1}".format(
         os.path.basename(sys.argv[0]), err))
    return False

使用DOM分析XML文件是容易的,因为模块为我们完成了所有困难的工作,但是我们必须做好处理expat错误的准备,因为就像元素树一样,expat XML分析器也是DOM类使用的默认分析器。

self.clear()
for element in dom.getElementsByTagName("incident"):
    try:
        data = {}
        for attribute in ("report_id", "date", "aircraft_id",
               "aircraft_type",
               "pilot_percent_hours_on_type",
               "pilot_total_hours", "midair"):
            data[attribute] = element.getAttribute(attribute)
        data["date"] = datetime.datetime.strptime(
                           data["date"], "%Y-%m-%d").date()
        data["pilot_percent_hours_on_type"] = (
                float(data["pilot_percent_hours_on_type"]))
        data["pilot_total_hours"] = int(
                data["pilot_total_hours"])
        data["midair"] = bool(int(data["midair"]))
        airport = element.getElementsByTagName("airport")[0]
        data["airport"] = get_text(airport.childNodes)
        narrative = element.getElementsByTagName(
                                              "narrative")[0]
        data["narrative"] = get_text(narrative.childNodes)
        incident = Incident(**data)
        self[incident.report_id] = incident
    except (ValueError, LookupError, IncidentError) as err:
        print("{0}: import error: {1}".format(
             os.path.basename(sys.argv[0]), err))
        return False
return True

DOM存在后,我们清空当前的事故记录数据,并对所有事故标签进行迭代。每次迭代时,我们都提取其属性,对日期、数值型以及布尔型等数据,我们都将其转换为适当的类型,就像使用元素树时所做的一样。使用DOM与使用元素树之间真正较大的区别是对文本节点的处理过程,我们使用xml.dom.Element.getElementsByTagName()方法获取给定标签名的子元素——对与,我们知道总是会有其中的一个,因此我们取每个类型的第一个(唯一的一个),之后使用嵌套的get_text()函数对这些标签的子节点进行迭代,以便提取其文本。

与通常一样,如果有任何错误产生,我们就将捕获相关的异常,为用户打印错误消息,并返回False。

DOM与元素树方法之间的差别并不大,由于两者都使用同样的expat分析器,因此两者都非常快。

7.3.3 手动写入XML

将预存的元素树或DOM写成XML文档可以使用单独的方法调用完成。如果数据本身不是以这两种形式存在,我们就必须先创建元素树或DOM,之后直接写出数据会更加方便。

写XML文件时,我们必须确保正确地对文本与属性值进行了转义处理,并且写的是格式正确的XML文档。下面给出export_xml_manual()方法,该方法用于以XML格式写出事故数据。

def export_xml_manual(self, filename):
    fh = None
    try:
        fh = open(filename, "w", encoding="utf8")
        fh.write('<?xml version="1.0" encoding="UTF-8"?>\n')
        fh.write("<incidents>\n")
    for incident in self.values():
        fh.write('<incident report_id={report_id} '
                'date="{0.date!s}" '
                'aircraft_id={aircraft_id} '
                'aircraft_type={aircraft_type} '
                'pilot_percent_hours_on_type='
                '"{0.pilot_percent_hours_on_type}" '
                'pilot_total_hours="{0.pilot_total_hours}" '
                'midair="{0.midair:d}">\n'
                '<airport>{airport}</airport>\n'
                '<narrative>\n{narrative}\n</narrative>\n'
                '</incident>\n'.format(incident,
        report_id=xml.sax.saxutils.quoteattr(
                             incident.report_id),
        aircraft_id=xml.sax.saxutils.quoteattr(
                             incident.aircraft_id),
        aircraft_type=xml.sax.saxutils.quoteattr(
                             incident.aircraft_type),
        airport=xml.sax.saxutils.escape(incident.airport),
        narrative="\n".join(textwrap.wrap(
                xml.sax.saxutils.escape(
                    incident.narrative.strip()), 70))))
fh.write("</incidents>\n")
return True

正如本章中我们通常所做的一样,我们也忽略了except语句块与finally语句块。

我们使用UTF-8编码写文件,并且必须为内置的open()函数指定该编码方式。严格地说,我们并不需要在<?xml?> 声明中指定该编码,因为UTF-8是默认的编码格式,但我们更愿意清晰地指定。我们选择使用双引号(")来封装所有属性值,并且,为方便起见,我们使用单引号来封装事故数据中的字符串,以避免对引号进行转义处理的需要。

sax.saxutils.quoteattr()函数与sax.saxutils.escape()函数(我们使用这一函数处理XML文本,因为该函数可以正确地对“&”、“<”、“>”等字符进行转义处理)类似,此外,该函数还可以对引号进行转义(如果需要),并返回已经使用引号包含了的字符串 ,这也是为什么我们不需要对报告ID以及其他字符串属性值加引号的原因所在。

叙述性文本中插入的换行与文本包裹纯粹是为了装饰用的,其目的是为了使其更便于人的阅读和编辑,但也可以忽略。

以HTML格式写数据与以XML格式并没有太大的差别。convert-incidents.py程序包含的export_html()函数是一个简单的实例,这里没有给出该函数,因为其中没有什么新东西。

7.3.4 使用SAX分析XML

与元素树和DOM在内存中表示整个XML文档不同的是,SAX分析器是逐步读入并处理的,从而可能更快,对内存的需求也不那么明显。然而,性能上的优势不能仅靠假设,尤其是元素树与DOM都使用了快速的expat分析器。

在遇到开始标签、结束标签以及其他XML元素时,SAX分析器宣称“分析事件”并进行工作。为处理那些我们感兴趣的事件,我们必须创建一个适当的处理者类,并提供某些预定义的方法,在匹配分析事件发生时,就会调用这些方法。最常实现的处理者是内容处理者,当然,如果我们需要更好的控制,提供错误处理者以及其他处理者也是可能的。

下面给出的是完整的import_xml_sax()方法,由于大部分工作都已经由自定义的IncidentSaxHandler类实现,因此,这一方法的实现代码很短。

def import_xml_sax(self, filename):
    fh = None
    try:
        handler = IncidentSaxHandler(self)
        parser = xml.sax.make_parser()
        parser.setContentHandler(handler)
        parser.parse(filename)
        return True
    except (EnvironmentError, ValueError, IncidentError,
            xml.sax.SAXParseException) as err:
        print("{0}: import error: {1}".format(
             os.path.basename(sys.argv[0]), err))
        return False

我们首先创建了要使用的处理者,之后创建一个SAX分析器,并将其内容处理者设置为我们刚创建的那个。之后,我们将文件名赋予分析器的parse()方法,如果没有分析错误产生,就返回True。

我们将self(也就是说,这个IncidentCollection dict子类)传递给自定义的IncidentSaxHandler类的初始化程序。处理者清空旧的事故记录,之后随着对文件分析的进程建立起一个事故字典。分析完成后,该字典将包含读入的所有事故。

class IncidentSaxHandler(xml.sax.handler.ContentHandler):

    def __init__(self, incidents):
        super().__init__()
        self.__data = {}
        self.__text = ""
        self.__incidents = incidents
        self.__incidents.clear()

自定义的SAX处理者类必须继承自适当的基类,这将确保对于任何我们没有重新实现的方法(因为我们不关心这些方法处理的分析事件),都会调用该方法的基类版本,并且实际上不做任何处理。

我们首先调用基类的初始化程序。对所有子类而言,这通常是一种好的做法,尽管对直接的object子类而言这样做没有必要(但也没有坏处)。字典self.__data用于保存某个事故的数据,self.__text字符串用于存放机场名的文本信息或叙述性文本的文本信息,这依赖于我们当前正在读入的具体内容,self.__incidents字典是到IncidentCollec-tion字典(对这一字典,处理者直接对其进行更新操作)的对象引用。(一种替代的设计方案是将一个独立的字典放置在处理者内部,并在最后使用dict.clear()将其复制到IncidentCollection,之后调用dict.update()。)

def startElement(self, name, attributes):
    if name == "incident":
        self.__data = {}
        for key, value in attributes.items():
            if key == "date":
                self.__data[key] = datetime.datetime.strptime(
                                       value, "%Y-%m-%d").date()
            elif key == "pilot_percent_hours_on_type":
               self.__data[key] = float(value)
            elif key == "pilot_total_hours":
               self.__data[key] = int(value)
            elif key == "midair":
               self.__data[key] = bool(int(value))
            else:
               self.__data[key] = value
    self.__text = ""

在读取到开始标签及其属性的任何时候,都会以标签名以及标签属性作为参数来调用xml.sax.handler.Content-Handler.startElement()方法。对航空器事故XML文件,开始标签是,我们将忽略该标签;标签,我们使用其属性来生成self.__data字典的一部分;标签与标签,两者我们都忽略。在读取到开始标签时,我们总是清空self.__text字符串,因为在航空器事故XML文件格式中,没有嵌套的文本标签。

在IncidentSaxHandler类中,我们没有进行任何异常处理。如果产生异常,就将传递给调用者,这里也就是import_xml_sax()方法,调用者将捕获异常,并输出适当的错误消息。

def endElement(self, name):
    if name == "incident":
        if len(self.__data) != 9:
           raise IncidentError("missing data")
        incident = Incident(**self.__data)
        self.__incidents[incident.report_id] = incident
    elif name in frozenset({"airport", "narrative"}):
       self.__data[name] = self.__text.strip()
    self.__text = ""

读取到结束标签时,将调用xml.sax.handler.ContentHandler.endElement()方法。如果已经到达某条事故记录的结尾,此时应该已具备所有必要的数据,因此,此时创建一个新的Incident对象,并将其添加到事故字典。如果已到达文本元素的结尾,就像self.__data 字典中添加一个项(其中包含迄今为止累积的文本)。最后,我们清空self.__text字符串,以备后面使用。(严格地说,我们也没必要对其进行清空,因为在获取开始标签时也可以清空该字符串,但对有些XML格式,清空该字符串会有一定的作用,比如对标签可以嵌套的情况。)

def characters(self, text):
    self.__text += text

读取到文本时,SAX分析器将调用xml.sax.handler.ContentHandler.characters()方法,但并不能保证对所有文本只调用一次该方法,因为文本可能以分块的形式出现,这也是为什么我们只是简单地使用该方法来累积文本,而只有在读取到相关的结束标签后才真正将文本放置到字典中。(一种更高效的实现方案是将self.__text作为一个列表,这一方法的主体部分则使用self.__text.append(text),其他方法也相应调整。)

与使用元素树或DOM相比,使用SAX API是非常不同的,但确实也是很有效的。我们可以提供其他处理者,并在内容处理者中重新实现额外的方法,以便按我们的需要施加更多的控制。SAX分析器本身并不保存XML文档的任意表示形式——这使得SAX适合于将XML读入到我们的自定义数据组合中,也意味着没有SAX“文档”以XML格式写出,因此,对写XML而言,我们必须使用本章前面描述的某种方法。

相关文章
|
8天前
|
XML Java 数据库连接
mybatis中在xml文件中通用查询结果列如何使用
mybatis中在xml文件中通用查询结果列如何使用
9 0
|
10天前
|
XML JavaScript 前端开发
xml文件使用及解析
xml文件使用及解析
|
16天前
|
Python
【python】python跨文件使用全局变量
【python】python跨文件使用全局变量
|
16天前
|
Python
【python】爬楼梯—递归分析(超级详细)
【python】爬楼梯—递归分析(超级详细)
|
7天前
|
机器学习/深度学习 人工智能 算法
图像处理与分析:Python中的计算机视觉应用
【4月更文挑战第12天】Python在计算机视觉领域广泛应用,得益于其丰富的库(如OpenCV、Pillow、Scikit-image)和跨平台特性。图像处理基本流程包括获取、预处理、特征提取、分类识别及重建生成。示例代码展示了面部和物体检测,以及使用GAN进行图像生成。
机器学习/深度学习 算法 Python
13 0
|
1天前
|
算法 数据可视化 Python
Python中LARS和Lasso回归之最小角算法Lars分析波士顿住房数据实例
Python中LARS和Lasso回归之最小角算法Lars分析波士顿住房数据实例
|
2天前
|
机器学习/深度学习 数据采集 数据可视化
Python数据处理与分析
【4月更文挑战第13天】Python在数据处理与分析中扮演重要角色,常用库包括Pandas(数据处理)、NumPy(数值计算)、Matplotlib和Seaborn(数据可视化)、SciPy(科学计算)、StatsModels(统计建模)及Scikit-learn(机器学习)。数据处理流程涉及数据加载、清洗、探索、特征工程、模型选择、评估与优化,以及结果展示。选择哪个库取决于具体需求和数据类型。
13 1
|
2天前
|
数据采集 NoSQL 搜索推荐
五一假期畅游指南:Python技术构建的热门景点分析系统解读
五一假期畅游指南:Python技术构建的热门景点分析系统解读
|
4天前
|
算法 数据可视化 Python
使用Python实现主成分分析(PCA)
使用Python实现主成分分析(PCA)
22 4