本节是第五讲的第十五小节下,本节主要介绍文本文件和XML文件的生成以及处理等。
文本文件的写入与分析(Writing and Parsing Text Files)
写入文本是容易的,读回时则可能存在不少问题,因此,我们需要认真选择合适的结构,以便对其进行分析不至于太难。如下所示我们将要使用的文本格式展示了一个航空器事故实例。将航空器事故记录写入文件时,我们将在每个记录后附加一个空白行,但对文件进行分析时,我们可以接受在事故记录之间存在0个或多个空白行。
#航空器事故记录的实例文本格式
[20070927022009C]
date=2007-09-27
aircraft_id=1675B
aircraft_type=DHC-2-MKl
airport=MERLE K (MUDHOLE) SMITH
pilot_percent_hours_on_type=46.1538461538
pilot_total_hours=13000
midair=0
.NARRATIVE_START.
ACCORDING TO THE PILOT, THE DRAG LINK FAILED DUE TO AN OVERSIZED TAIL WHEEL
TIRE LANDING ON HARD SURFACE.
.NARRATIVE_END.
写入文本(Writing Text)
每条事故记录都以包含在方括号中的报告ID开始,其后跟随的是所有占据一行的数据项,其格式为key=value,对占据多行的叙述性文本,在文本前以开始标记 (NARRATIVE_START)引导,在文本末尾则以结束标记(NARRATIVE_END)结尾, 我们对两个标记之间的所有文本进行了缩排,以防止文本行与开始或结束标记混淆。
下面给出的是export_text()函数的代码,但没有给出except语句块与finally语句块,因为这两个语句块与前面相应实例中是相同的,不同之处仅在于待处理的异常:
叙述性文本中存在断行并不会有很大影响,因此我们可以按我们的需求对文本进行包裹。通常,我们可以使用textwrap模块的textwrap.wrap()函数,但这里我们同时需要缩排与包裹,因此我们首先创建了一个textwrap.TextWrap对象,并用我们将要使用的缩排(第一行与后续行都是4个空格)进行初始化。默认情况下,该对象可以包裹宽度为70个字符的行,当然,通过使用关键字参数,也可以对其进行改变。
def export_text(self, filename):
wrapper = textwrap.TextWrapper(initial_indent=” “,subsequent_indent=” “)
fh = None
try:
fh = open(filename, “w”, encoding=”utf8”)
for incident in self.values():
narrative = "\n".join(wrapper.wrap(incident.narrative.strip()))
fh.write("[{0.report_id}]\n"
"date={0.date!s}\n"
"aircraft_id={0.aircraft_id}\n"
"aircraft_type={0.aircraft_type}\n"
"airport={airport}\n"
"pilot_percent_hours_on_type="
"{0.pilot_percent_hours_on_type}\n"
"pilot_total_hours={0.pilot_total_hours}\n"
"midair={0.midair:d}\n"
".NARRATIVE_START.\n{narrative}\n"
".NARRATIVE_END.\n\n“.format(incident,
airport=incident.airport.strip(), narrative=narrative))
return True
我们可以使用三引号包含的字符串,但我们更愿意手动加入换行。textwrap.TextWrap 对象提供了一个wrap()方法,该方法以一个字符串作为输入,在这里就是叙述性的文本, 并返回一个字符串列表,带有适当的缩排,并且其中每个字符串的长度不超过wrap宽度。 之后,我们将这些行添加到一个单独的字符串中,并使用换行作为分隔符。事故日期以 datetime.date对象的形式存放,在写入日期数据时,我们强制str.format()使用字符串表示形式——这样可以很便利地产生符合ISO 8601的日期数据格式,也即YYYY-MM-DD格 式。此外,我们告知str.format()将布尔型变量midair写为整数——如果为True,就变为1, 如果为felse,就变为0。通常,str.format()可以使写入文本变得很容易,因为该方法可以 自动处理所有Python数据类型。(也包括自定义数据类型,如果实现了__str__()或 __format__()特殊方法。)
分析文本(Parsing Text)
与进行文本写入的方法相比,用于读取与分析文本格式航空器事故记录的方法要更长一些,也更棘手。读取文件时,我们可能会处于几种状态之间的一种:读取叙述性文本行的过程中;读取key=value行的过程中;读取报告ID行的过程中(在新事故记录的起始处)。我们将分5个部分来査看import_text_manual()方法。
def import_text_manual(self, filename):
fh = None
try:
fh = open(filename, encoding="utf8")
self.clear()
data = {}
narrative = None
该方法首先以“文本读”模式打开文件,之后清空事故字典,创建data字典,以便存放单个事故记录的数据(与二进制读事故记录时的做法相同)。narrative变量用于两个目的:作为一个状态指示器;存储当前事故记录的叙述性文本。如果narrative为 None,就意味着我们当前读取的不是叙述性文本;如果是一个字符串(即便是空字符串),也意味着我们正在读取叙述性文本行。
for lino, line in enumerate(fh, start=1):
line = line.rstrip()
if not line and narrative is None:
continue
if narrative is not None:
if line == ".NARRATIVE_END.":
data["narrative"] = textwrap.dedent(narrative).strip()
if len(data) != 9:
raise IncidentError("missing data on line {0}".format(lino))
incident =Incident(**data)
self[incident.report_id] = incident
data = {}
narrative = None
else:
narrative += line + "\n"
在釆用文本读模式时,数据的读入是逐行进行的,因此我们可以保持对当前行号的追踪,并提供包含信息更多的错误消息(与读取二进制文件相比)。我们首先剥离每行的结尾处的空白字符,如果处理之后剩下的是空行(假定没有处在读取叙述性文本的过程中),就简单地跳到下一行。这意味着,事故记录中的空白行并不会有什么影响, 我们保留叙述性文本中任意的空白行。如果narrative不为None,就说明我们处于读取叙述性文本的过程中。如果当前行表示的是叙述性文本的结尾标记,就说明我们不仅完成了叙述性文本的读取,也完成了整个事故记录的读取。在这种情况下,我们将叙述性文本存放到data字典(之前使用textwrap.dedent()函数移除缩排),假定我们获取了事故记录的9个要素,则创建一 个新事故,并将其存放在字典中。之后,清空data字典,并重置narrative变量,以备读取下一条记录。另一方面,如果当前行不包含叙述性文本标记,我们就将其附加到narrative中并剥离开始处的换行。
elif (not data and line[0] =="[" and line[-1] =="]"):
data["report_id"] = line[1:-1]
narrative为None,说明我们或者在读取新报告ID,或者在读取其他数据。只有在data字典为空(因为该字典最初为空,并且在读取每个事故记录结束后也将其清空) 并且该行以[开始并以]结束时,才可以判断当前在读取新报告ID。如果是这种情况,就将报告ID放置到data字典中。这也意味着,直至data字典下一次被清空,elif条件才会变为True。
elif "=" in line:
key, value = line.split("=”, 1)
if key=="date”:
data[key] = datetime.datetime.strptime(value,"%Y-%m-%d').date()
elif key == "pilot_percent_hours_on_type":
data[key] = float(value)
elif key = "pilot_total_hours":
data[key] = int(value)
elif key = "midair":
data[key] = bool(int(value))
else:
data[key] = value
elif line ==".NARRATIVE_START.":
narrative=""
else:
raise KeyError("parsing error on line {0}".format(lino))
如果当前没有处于读取叙述性文本的状态或读取新报告ID的状态,就只有3种可能:当前正在读取key=value行;当前正在读取叙述性文本开始标记;出错。
如果当前正在读取key=value行,就可以使用第一个=字符分割该行,并指定一次分割的最大值这意味着value中可以安全地包含字符=。读入的所有数据都以Unicode字符串形式存在,因此,对于日期、数值型、布尔型等数据类型,我们必须相应地对值字符串进行转换。
对日期数据,我们使用datetime.datetime.strptime()函数(“字符串分析时间”),该函数以一个格式化字符串为参数,并返回一个datetime.datetime对象。我们使用了匹配ISO 8601日期格式的格式化字符串,并使用datetime.datetime.date()从产生的 datetime.datetime对象中取回一个datetime.date对象,因为我们需要的只是日期,而不是日期/时间。对数值型值得转换,我们依赖于Python内置的类型函数float()与int()。 尽管如此,要注意的是,int("4.0")这种语句会产生ValueError异常,如果接受整数时希望更具字面上的意义,可以使用int(float("4.0"));如果需要四舍五入,而不是截取, 则可以使用round(float("4.0"))。获取bool更加微妙,比如,bool("0")返回True (非空字符串都为True),因此,我们必须首先将字符串转换为int。无效的、丢失的、超出范围的值总是会产生异常。任意其他转换操作失败,都将产生ValueError异常。在数据用于创建相应的Incident对象时,任何值超出了范围, 都会产生IncidentError异常。
return True
except (EnvironmentError, ValueError, KeyError,IncidentError) as err:
print("{0}: import error: {1}".format(
os.path.basename(sys.argv[0]), err))
return False
finally:
if fh is not None:
fh.close()
某行不包含字符=,我们可以检查是否已经读取了叙述性文本开始标记,如果已经读取,就将narrative变量设置为空字符串,这意味着对后继行而言,第一个if条件为True—直至读取到描述性文本结束标记。
if条件或elif条件都不为True,说明有错误产生,因此,在最后的else语句中,产生一个KeyError异常来表示这一情况。读取所有行后,为调用者返回True——除非发生异常,在这种情况下,except语句块将捕获该异常,为用户打印出错误消息,并返回False。最后,不论哪种情况,打开的文件都要关闭。
写入与分析XML文件(Writing and Parsing XML Files)
有些程序将其处理的所有数据都使用XML文件格式,还有些其他程序将XML用作一种便利的导入/导出格式。即便程序的主要格式是文本格式或二进制格式,导入与导出XML的能力也是有用的,并且始终是值得考虑的一项功能。
Python提供了 3种写入XML文件的方法:手动写入XML;创建元素树并使用其 write()方法;创建D0M并使用其write()方法。XML文件的读入与分析则有4种方法: 人工读入并分析XML (不建议釆用这种方法);使用元素树;D0M (文档对象模型);SAX(Simple API for XML,用于 XML 的简单 API)分析器。
元素树(Element Trees)
使用元素树写入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=incldent.aircraft_type,
pilot_percent_hours_on_type=str(
incident.pilot_percent_hours_on_type),
pilot_total_hours=str(inddent.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)
<?xml version="1.0" encoding="UTF-8"?>
<incidents>
<incident report_id=”20070222W8099G” date="2007-02-22"
aircraft_id="80342" aircraft_type="CE-172-M"
pilotpercent_hours_on_type="9.09090909091"
pilot_total_hours="440" midair="0">
<airport>BOWERMAN</airport>
<narrative>
ON A GO-AROUND FROM A NIGHT CROSSWIND LANDING ATTEMPT THE AIRCRAFT HIT A RUNWAY EDGE LIGHT DAMAGING ONE PROPELLER.
</narrative>
</incident>
<incident>
...
</incident>
:
</incidents>
我们从创建根元素(<incidents>)开始,之后对所有事故记录进行迭代。对每条事故记录,我们创建一个元素(<incident>)来存放该事故记录的数据,并使用关键字参数来提供属性。所有属性必须都是文本,因此,我们需要对日期、数值型数据、布尔型数据项进行相应转换。我们不必担心对“&”、"<"、">”(或属性值中的引号)的转义处理,因为元素树模块(以及SOM、SAX模块)会对相关的详细资料进行自动处理。
每个<incident>包含两个子元素,一个用于存放机场名,另一个用于存放叙述性文本。创建子元素时,必须为其提供父元素与标签名。元素的读/写text属性则用于存放其文本。
<incident>及其所有属性、子元素<airport>与<narrative>创建之后,我们将其添加到树体系的根(<incidents>)元素,反复进行这一过程,最终的元素体系中就包含了所有事故记录数据,这些数据可以转换为元素树。
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”。
使用元素树读取XML文件并不比写入难多少,也分为两个阶段:首先读入并分析XML文件,之后对生成的元素树进行遍历,以便读取数据来生成incidents字典。 同样地,如果元素树本身已经是内存中存储的数据结构,第二阶段就不是必要的。下面分两部分给出import_xml_etree()方法。
def import_xml_etree(self, filename):
try:
tree = xml.etree.ElementTree.parse(filename)
except (EnvironmentError,xmLparsers.expat.ExpatError) as err:
print("{0}: import error: {1}".format(
os.path.basename(sys.argv[0]), err))
return False
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 = lncident(**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.fmdall()方法对每个行迭代处理了。每个事故都是以一个xml.etree.Element对象的形式返回的。 在处理元素属性时,我们使用的是与前面import_text_regex()方法中同样的技术。我们首先将所有值存储到data字典中,之后将日期、数字、布尔型值转换到正确的类型。 对机场属性与叙述性文本元素,我们使用xml.etree.Element.find()方法寻找这些值,并读取其text属性。如果某个文本元素不包含文本,那么其text属性将为None,因此, 在读取叙述性文本元素时,我们必须考虑这一点,因为该元素可以为空。在所有情况 下,返回给我们的属性值与文本都不包含XML转义,因为其是自动非转义的。
与用于处理航空器事故数据的所有XML分析器类似,如果航空器或叙述性文本元素丢失,或某个属性丢失,或某个转换过程失败,或任意的数值型数据超出了取值范围,都会产生异常——这将确保无效数据被终止分析并输出错误消息。用于创建并存储事故记录以及处理异常的代码与前面看到的相同。
DOM(Document Object Model)
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,因为 我们没有文档类型。在获取了表示文档的树之后,我们取回根元素,之后对所有事故记录进行迭代。
对每个事故记录,我们创建一个<incident>元素,对事故的每个属性,我们使用该属性名与值调用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 nodelist:
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类使用的默认分析器。
DOM存在后,我们清空当前的事故记录数据,并对所有事故标签进行迭代。每次迭代时,我们都提取其属性,对日期、数值型以及布尔型等数据,我们都将其转换为适当的类型,就像使用元素树时所做的一样。使用DOM与使用元素树之间真正较大的区别是对文本节点的处理过程,我们使用xml.dom.Element.getElementsByTagName()方法获取给定标签名的子元素——<airport>与<narrative>,我们知道总是会有其中的一个,因此我们取每个类型的第一个(唯一的一个),之后使用嵌套的get_text()函数对这些标签的子节点进行迭代, 以便提取其文本。
self.dear()
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 = lncident(**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
手动写入XML
将预存的元素树或DOM写成XML文档可以使用单独的方法调用完成。如果数据本身不是以这两种形式存在,我们就必须先创建元素树或DOM,之后直接写出数据会更加方便。
写XML文件时,我们必须确保正确地对文本与属性值进行了转义处理,并且写的是格式正确的XML文档。下面给出export_xml_manual()方法,该方法用于以XML格式写出事故数据。
正如本章中我们通常所做的一样,我们也忽略了except语句块与finally语句块。
我们使用UTF-8编码写文件,并且必须为内置的open()函数指定该编码方式。严格地说,我们并不需要在<?xml?>声明中指定该编码,因为UTF-8是默认的编码格式, 但我们更愿意清晰地指定。我们选择使用双引号(”)来封装所有属性值,并且,为方便起见,我们使用单引号来封装事故数据中的字符串,以避免对引号进行转义处理的需要。
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
sax.saxutils.quoteattr()函数与 sax.saxutils.escape()函数(我们使用这一函数处理XML文本,因为该函数可以正确地对“&”、"<”、">"等字符进行转义处理)类似, 此外,该函数还可以对引号进行转义(如果需要),并返回已经使用引号包含了的字符串,这也是为什么我们不需要对报告ID以及其他字符串属性值加引号的原因所在。
叙述性文本中插入的换行与文本包裹纯粹是为了装饰用的,其目的是为了使其更便于人的阅读和编辑,但也可以忽略。
以HTML格式写数据与以XML格式并没有太大的差别。convert-incidents.py程序包含的export_html()函数是一个简单的实例,这里没有给出该函数,因为其中没有什么新东西。
使用SAX分析XML
与元素树和D0M在内存中表示整个XML文档不同的是,SAX分析器是逐步读入并处理的,从而可能更快,对内存的需求也不那么明显。然而,性能上的优势不能仅靠假设,尤其是元素树与D0M都使用了快速的expat分析器。
在遇到开始标签、结束标签以及其他XML元素时,SAX分析器宣称“分析事件” 并进行工作。为处理那些我们感兴趣的事件,我们必须创建一个适当的处理者类,并提供某些预定义的方法,在匹配分析事件发生时,就会调用这些方法。最常实现的处理者是内容处理者,当然,如果我们需要更好的控制,提供错误处理者以及其他处理者也是可能的。
下面给出的是完整的import_xml_sax()方法,由于大部分工作都已经由自定义的 IncidentSaxHandler类实现,因此,这一方法的实现代码很短。
def import_xml_sax(selfT 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子类)传递给自定义的Incident SaxHandler类的初始化程序。处理者清空旧的事故记录,之后随着对文件分析的进程建立起一个事故字典。分析完成后,该字典将包含读入的所有事故。
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字典是到IncidentCollection字典(对这一字典,处理者直接对其进行更新操作)的对象引用。(一种替代的设 计方案是将一个独立的字典放置在处理者内部,并在最后使用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 文件,开始标签是<incidents>,我们将忽略该标签;<incident>标签,我们使用其属性来生成 self.__data字典的一部分;<airport>标签与<narrative>标签,两者我们都忽略。在读取到开始标签时,我们总是清空self.__text字符串,因为在航空器事故XML文件格式中, 没有嵌套的文本标签。
在IncidentSaxHandler类中,我们没有进行任何异常处理。如果产生异常,就将传递给调用者,这里也就是import_xml_sax()方法,调用者将捕获异常,并输出适当的错误消息。
def endElement(self, name):
if name == "incident":
if len(self.__data) != 9:
raise IncidentError("missing data")
incident = lncident(**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而言,我们必须使用本章前面描述的某种方法。
语法 文件对象属性与方法
f.close() 关闭文件对象f,并将属性f.closed设置为True
f.closed 文件已关闭,则返回True
f.encoding bytes与str之间进行转换时使用的编码
f.fileno() 返回底层文件的文件描述符(只对那些有文件描述符的文件对象是可用的)
f.flush() 清空文件对象 f
f.isatty() 如果文件对象与控制台关联,就返回True (只有在文件对象应用了真正的文件时才是 可用的)
f.mode 文件对象打开时使用的模式
f.name 文件对象f的文件名(如果有)
f.newlines 文本文件f中的换行字符串的类型
f.__next__() 返回文件对象f的下一行,大多数情况下,这种方法是隐式地使用的,比如对f中的行
f.peek(n) 返回n个字节,而不移动文件指针的位置
f.read(count) 从文件对象f中读取至多count个字节,如果没有指定count,就读取从当前文件指针直 至最后的每个字节。以二进制模式读时,返回bytes对象;以文本模式读时,返回str 对象。如果没有要读的内容(已到文件结尾),就返回一个空的bytes或str对象
f.readable() 如果f已经打开等待读取,就返回True
f.readinto(ba) 将至多len(ba)个字节读入到bytearray ba中,并返回读入的字节数 如果在文件结尾,就为0 (只有在二进制模式时才是可用的)
f.readline(count) 读取下一行(如果指定count,并且在\n字符之前满足这一数值,那么至多读入count 个字节),包括\n
f.readlines(sizehint) 读入到文件结尾之前的所有行,并以列表形式返回。如果给定sizehint,那么读入大概 至多sizehint个字节(如果底层文件对象支持)
fseek(offset,whence) 如果没有给定whence,或其为os.SEEK_SET,就按给定的offset移动文件指针(并作 为下一次读、写得起点);如果whence为os.SEEK_CUR,就相对于当前文件指针位置 将其移动offset (可以为负值)个位置(whence为os.SEEK_END,则是相对文件结尾)。 在追加"a"模式下,写入总是在结尾处进行的,而不管文件指针在何处。在文本模式下,只应该使用tell()方法的返回值作为offset
f.seekable() 如果f支持随机存取,就返回True
f.tell() 返回当前指针位置(相对于文件起始处)
f.truncate(size) 截取文件到当前文件指针所在位置,如果给定size,就到size大小处
f.writable() 如果f是为写操作而打开的,就返回True
f.write(s) 将bytes/bytearray对象s写入到文件(该文件以二进制模式打开),或者将str对象s写入到文件(该文件以文本模式打开)
f.writelines(seq) 将对象序列(对文本文件而言是字符串,对二进制文件而言是字节字符串)写入到文件
以上内容部分摘自视频课程05后端编程Python15文件处理(下),更多实操示例请参照视频讲解。跟着张员外讲编程,学习更轻松,不花钱还能学习真本领。