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

7.2 文本文件的写入与分析

写入文本是容易的,读回时则可能存在不少问题,因此,我们需要认真选择合适的结构,以便对其进行分析不至于太难。图7-4以我们将要使用的文本格式展示了一个航空器事故实例。将航空器事故记录写入文件时,我们将在每个记录后附加一个空白行,但对文件进行分析时,我们可以接受在事故记录之间存在0个或多个空白行。

7.2.1 写入文本

每条事故记录都以包含在方括号中的报告ID开始,其后跟随的是所有占据一行的数据项,其格式为key=value,对占据多行的叙述性文本,在文本前以开始标记(NARRATIVE_START)引导,在文本末尾则以结束标记(NARRATIVE_END)结尾,我们对两个标记之间的所有文本进行了缩排,以防止文本行与开始或结束标记混淆。

下面给出的是export_text()函数的代码,但没有给出except语句块与finally语句块,因为这两个语句块与前面相应实例中是相同的,不同之处仅在于待处理的异常:

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()函数,但这里我们同时需要缩排与包裹,因此我们首先创建了一个textwrap.TextWrap对象,并用我们将要使用的缩排(第一行与后续行都是4个空格)进行初始化。默认情况下,该对象可以包裹宽度为70个字符的行,当然,通过使用关键字参数,也可以对其进行改变。

我们可以使用三引号包含的字符串,但我们更愿意手动加入换行。textwrap.TextWrap对象提供了一个wrap()方法,该方法以一个字符串作为输入,在这里就是叙述性的文本,并返回一个字符串列表,带有适当的缩排,并且其中每个字符串的长度不超过wrap宽度。之后,我们将这些行添加到一个单独的字符串中,并使用换行作为分隔符。事故日期以datetime.date对象的形式存放,在写入日期数据时,我们强制str.format()使用字符串表示形式——这样可以很便利地产生符合ISO 8601的日期数据格式,也即YYYY-MM-DD格式。此外,我们告知str.format()将布尔型变量midair写为整数——如果为True,就变为1,如果为false,就变为0。通常,str.format()可以使写入文本变得很容易,因为该方法可以自动处理所有Python数据类型。(也包括自定义数据类型,如果实现了__str__()或__format__()特殊方法。)

7.2.2 分析文本

与进行文本写入的方法相比,用于读取与分析文本格式航空器事故记录的方法要更长一些,也更棘手。读取文件时,我们可能会处于几种状态之间的一种:读取叙述性文本行的过程中;读取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异常。

某行不包含字符=,我们可以检查是否已经读取了叙述性文本开始标记,如果已经读取,就将narrative变量设置为空字符串,这意味着对后继行而言,第一个if条件为True——直至读取到描述性文本结束标记。

if条件或elif条件都不为True,说明有错误产生,因此,在最后的else语句中,产生一个KeyError异常来表示这一情况。

    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()

读取所有行后,为调用者返回True——除非发生异常,在这种情况下,except语句块将捕获该异常,为用户打印出错误消息,并返回False。最后,不论哪种情况,打开的文件都要关闭。

7.2.3 使用正则表达式分析文本

对不熟悉正则表达式(“regexes”)的读者,建议在阅读本小节之前先阅读第13章——或先跳到下一节,并在需要的时候再回到本小节。

与手动完成一切分析工作(如前面小节中所做的)相比,使用正则表达式分析文本文件通常需要更少的代码量,但这种方式下产生较好的错误报告会更加困难。我们将分两个部分来查看import_text_regex()方法,首先查看正则表达式,之后查看其分析过程——但忽略了except语句块与finally语句块,因为没有什么新东西可以学习。

def import_text_regex(self, filename):
    incident_re = re.compile(
                    r"\[(?P<id>[^]]+)\](?P<keyvalues>.+?)"
                    r"^\.NARRATIVE_START\.$(?P<narrative>.*?)"
                    r"^\.NARRATIVE_END\.$",
                    re.DOTALL|re.MULTILINE)
    key_value_re = re.compile(r"^\s(?P<key>[^=]+)\s=\s*"
                              r"(?P<value>.+)\s*$", re.MULTILINE)

正则表达式写成原始字符串的形式,这使得我们不再需要双写每个反斜杠(将每个\写成\)——比如,没有合适的原始字符串,则第二个正则表达式必须写为"^\s(?P[^=]+)\s=\s(?P.+)\s$"。本书中,对于正则表达式,我们总是使用原始字符串形式表示。

第一个正则表达式incident_re用于匹配完整的事故记录,该表达式的一个效果是事故记录之间任何伪造的文本将不会被注意。该表达式实际上包含两个组成部分,第一部分是[(?P[^]]+)](?P.+?),用于匹配[,之后寻找尽可能多的非]字符,并将其与id匹配组匹配,再之后匹配字符](上面操作的整体效果是匹配一个报告ID),之后再寻找尽可能少(但至少一个)的任意字符(包括换行,因为re.DOTALL标记的存在),并将其与keyvalues匹配组进行匹配。与keyvalues匹配组进行匹配的字符只要求是能过渡到正则表达式第二部分的必要的最小值。

第一个正则表达式的第二部分是^.NARRATIVE_START.$(?P.*?) ^.NARRATIVE_END.$,用于与文本NARRATIVE_START进行匹配,之后将尽可能少的字符与narrative匹配组进行匹配,再之后就是文本NARRATIVE_END,实际上也就到了每条记录的结尾。re.MULTILINE标记意味着,在这一正则表达式中,^匹配每一行的起始处(而不仅仅是在字符串的起始处),$匹配每一行的结尾处(而不仅仅是在字符串的结尾处),因此,叙述性文本的开始标记与结束标记只在行起始处进行匹配。

第二个正则表达式key_value_re用于捕获key=value行,该表达式在给定文本的每一行的起始处进行匹配,匹配行以任意数量(也可以没有)的空白字符开始,其后跟随非-=字符(被捕获到key匹配组),再其后跟随一个=字符,最后是该行所有余下的字符(不包括开始的与结尾的空白字符),并将这些内容捕获到value匹配组。

使用正则表达式对文件进行分析的基本逻辑与前面讲述的人工分析的逻辑是相同的,区别只在于这里是使用正则表达式来提取事故记录以及记录内的数据,而不再逐行读取。

fh = None
try:
    fh = open(filename, encoding="utf8")
    self.clear()
    for incident_match in incident_re.finditer(fh.read()):
        data = {}
        data["report_id"] = incident_match.group("id")
        data["narrative"] = textwrap.dedent(
                    incident_match.group("narrative")).strip()
        keyvalues = incident_match.group("keyvalues")
        for match in key_value_re.finditer(keyvalues):
            data[match.group("key")] = match.group("value")
        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"]))
        if len(data) != 9:
            raise IncidentError("missing data")
        incident = Incident(**data)
        self[incident.report_id] = incident
    return True

re.finditer()方法返回一个迭代子,该迭代子依次产生每个非交叠的匹配。与前面所做的一样,我们创建一个data字典来存放每个事故的数据,但这一次,我们是从正则表达式incident_re的每个匹配中获取报告ID与叙述性文本,之后使用keyvalues匹配组一次提取所有key=value字符串,并使用正则表达式key_value_re的re.finditer()方法对每个单独的key=value行进行迭代。对发现的每个(key, value)对,我们将其放置在data字典中——因此所有的值都以字符串的形式存在。之后,对那些应该不是字符串的值,我们使用适当类型的值对其进行替代(并像我们在手动分析文本时一样对其进行字符串转换)。

我们添加了相应的检测机制,以确保data字典包含9个项,因为如果某个事故记录损坏,迭代子key_value.finditer()就可能匹配过多或过少的key=value行。结尾部分与前面一样——我们创建一个新的Incident对象,并将其放置在事故字典中,之后返回True。如果有任何一处出错,那么except suite将产生一个适当的错误消息,并返回False,并且由finally suite关闭文件。

我们看到,无论是手动进行的文本分析器,还是使用正则表达式进行的文本分析器都很短小,之所以会这样,原因之一就是Python的异常处理机制。文本分析器不需要检测字符串到日期、数字或布尔型的任何转换,也不需要进行区间检测(Incident类做这一工作),如果这些转换或区间失败,就会产生一个异常,而所有这些异常都将在结尾处统一处理。使用异常处理机制而不使用显式检测的另一个好处是,代码具有良好的可扩展性——即便记录格式发生变化、包含更多的数据项,异常处理代码也不需要进行改变。

时间: 2024-09-21 07:00:32

《Python 3程序开发指南(第2版•修订版)》—— 7.2 文本文件的写入与分析的相关文章

《Python 3程序开发指南(第2版•修订版)》——导读

前 言 在应用广泛的各种语言中,Python或许是最容易学习和最好使用的.Python代码很容易阅读和编写,并且非常清晰,而没有什么隐秘的.Python是一种表达能力非常强的语言,这意味着,在设计同样的应用程序时,使用Python进行编码所需要的代码量要远少于使用其他语言(比如C++或Java)的代码量. Python是一种跨平台的语言:一般来说,同样的Python程序可以同时在Windows平台与UNIX类平台(比如Linux.BSD与Mac OS X)上运行--只需要将构成Python程序的

《Python 3程序开发指南(第2版•修订版)》——2.2 Integral类型

2.2 Integral类型 Python提供了两种内置的Integral类型,即int与bool1.整数与布尔型值都是固定的,但由于Python提供了增强的赋值操作符,使得这一约束极少导致实际问题.在布尔表达式中,0与False表示False,其他任意整数与true都表示true.在数字表达式中,True表示1,False表示0.这意味着,有些看起来很怪异的表达式也是有效的.例如,我们可以使用表达式i += True来对整型变量i进行递增操作,当然,最自然的方法还是i+=1. 2.2.1 整数

《Python 3程序开发指南(第2版•修订版)》——第1章 过程型程序设计快速入门 1.1 创建并运行Python程序

第1章 过程型程序设计快速入门 本章提供了足以开始编写Python程序的信息.如果此时尚未安装Python,强烈建议读者先行安装Python,以便随时进行编程实践,获取实际经验,巩固所学的内容. 本章第1节展示了如何创建并执行Python程序.你可以使用自己最喜欢的普通文本编辑器来编写Python代码,但本节中讨论的IDLE程序设计环境提供的不仅是一个代码编辑器,还提供了很多附加的功能,包括一些有助于测试Python代码.调试Python程序的工具. 第2节介绍了Python的8个关键要素,通过

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

7.3 写入与分析XML文件 有些程序将其处理的所有数据都使用XML文件格式,还有些其他程序将XML用作一种便利的导入/导出格式.即便程序的主要格式是文本格式或二进制格式,导入与导出XML的能力也是有用的,并且始终是值得考虑的一项功能. Python提供了3种写入XML文件的方法:手动写入XML:创建元素树并使用其write()方法:创建DOM并使用其write()方法.XML文件的读入与分析则有4种方法:人工读入并分析XML(不建议采用这种方法,这里也没有进行讲述--正确处理某些更晦涩和更高级

《Python 3程序开发指南(第2版•修订版)》——第7章 文件处理 7.1 二进制数据的读写

第7章 文件处理 大多数程序都需要向文件中存储或从文件中加载信息,比如数据或状态信息.Python提供了多种实现方式,在第3章中,我们简要地介绍了如何处理文本文件,在上一章中介绍了pickles.在本章中,我们将更深入全面地介绍文件处理的相关知识与方法. 本章展示的所有技术都是平台无关的,这意味着,在某种操作系统/处理器体系结构组合平台上使用某实例程序保存的文件,也可以在另一种不同的操作系统/处理器体系结构组合平台上使用同样的程序加载.如果你在自己的程序中使用本章实例程序中使用的相关技术,就可以

《Python 3程序开发指南(第2版•修订版)》——2.7 练习

2.7 练习 1.修改程序print_unicode.py,以便用户可以在命令行上输入几个单独的单词,并且只有在Unicode字符名包含用户指定的所有单词时才打印相应列.这意味着,我们可以输入类似于如下的命令: print_unicode_ans.py greek symbol 实现上述要求的一种方法是使用words列表替换word变量(其中存放0.None或字符串).改变代码后,要记得更新使用帮助信息.这一改变需要添加不到10行代码,另外需要对不到10行的代码进行适当修改.文件print_un

《Python 3程序开发指南(第2版•修订版)》——2.3 浮点类型

2.3 浮点类型 Python提供了3种浮点值:内置的float与complex类型,以及来自标准库的decimal.Decimal类型,这3种数据类型都是固定的.float类型存放双精度的浮点数,具体取值范围则依赖于构建Python的C(或C#或Java)编译器,由于精度受限,对其进行相等性比较并不可靠.float类型的数值要使用小数点或使用指数表示,比如,0.0.4..5.7.-2.5.-2e9.8.9e-4等. 计算机使用基数2表示浮点数--这意味着,有些十进制数可以准确表示(比如0.5)

《Python 3程序开发指南(第2版•修订版)》——1.2 Python的关键要素

1.2 Python的关键要素 在本节中,我们将学习Python的8个关键要素,下一节中,我们将展示如何借助这些要素编写实际的小程序.关于本节中讲述的各关键要素,都有更多的内容需要阐述,因此,阅读本节的内容时,有时候你会觉得Python似乎遗失了一些内容,使得很多工作只能以一种冗繁的模式完成,如果使用前向索引或索引表格中的内容,那么你几乎总是可以发现Python具备你需要的特性,并且可以更紧凑的表达方式来完成当前展示的工作方式--还有很多其他内容. 1.2.1 要素#1:数据类型 任何程序语言都

《Python 3程序开发指南(第2版•修订版)》——2.4 字符串

2.4 字符串 字符串是使用固定不变的str数据类型表示的,其中存放Unicode字符序列.str数据类型可以作为函数进行调用,用于创建字符串对象--参数为空时返回一个空字符串,参数为非字符串类型时返回该参数的字符串形式,参数为字符串时返回该字符串的拷贝.str()函数也可以用作一个转换函数,此时要求第一个参数为字符串或可以转换为字符串的其他数据类型,其后跟随至多两个可选的字符串参数,其中一个用于指定要使用的编码格式,另一个用于指定如何处理编码错误. 前面我们注意到,字符串是使用引号创建的,可以