《Python Cookbook(第2版)中文版》——第1章 文本 1.1 每次处理一个字符-阿里云开发者社区

开发者社区> 开发与运维> 正文

《Python Cookbook(第2版)中文版》——第1章 文本 1.1 每次处理一个字符

简介:

本节书摘来自异步社区《Python Cookbook(第2版)中文版》一书中的第1章,第1.1节,作者[美]Alex Martelli , Anna Martelli Ravenscrof , David Ascher ,高铁军 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

第1章 文本

引言

感谢:Fred L. Drake, Jr.,PythonLabs

对于脚本语言来说,文本处理任务构成了一个重要的组成部分,每个人都会同意文本处理非常有用。每个人都会有一些文本需要重新格式化或者转化为另一种形式。问题是,每个程序都与另一个程序有点不同,无论它们是多么相似,想提取出一些可复用的代码片段并用它来处理不同的文件格式仍然是非常困难的。

什么是文本

看起来问题有点简单得过分了,事实上,我们看到了文本,就知道了什么是文本,文本是一串字符,这正是它与二进制数据之间的不同。二进制数据是一串字节。

不幸的是,所有的数据进入程序中都只是一串字节。没有什么库函数能够帮助我们确定某一个特定的字节串是否代表文本,我们只能自己创造一些试探的方法来判断那些数据是否能够用文本的方式来处理(不一定正确)。第1.11节就展示了一种试探的方法。

Python的字符串是一串不可改变的字节或者字符。绝大多数我们创造的方法以及处理字符串的方式都是把它们当做一串字符来处理的,但是一些字节串也是可以处理的。Unicode字符串是一串不可改变的Unicode字符:由Unicode字符串转到普通字符串或者由普通字符串转化为Unicode字符串的过程是通过codecs(编码器-解码器)对象来完成的,在这些对象涉及的很多标准转化方法中,字符串可以被表示为一串字节(也被称为编码和字符集)。但是需要注意的是,Unicode字符串不像字节串那样可以身兼两职。第1.20节、第1.21节和第1.22节展示了Python中Unicode的一些基本方法。

现在假设我们的程序通过上下文环境确认了它要处理的是文本。程序一般会接收外部输入,这通常是最好的方法。我们能够识别该文件的原因是,它有个已知的名字和已经定义好了的格式(在UNIX世界这很普遍),或者它有个著名的文件扩展名来指示出它内容的格式(在Windows世界这很普遍)。现在有个问题:我们必须使用“格式”这种东西,要不然上面这段话毫无意义。人们不是认为文本很简单吗?

让我们面对这个问题:其实并没有所谓的“纯”文本这样的东西,即使有,我们也可能不会关心(也有例外,计算机语言学家在某些情况下可能会研究纯粹的文本)。我们的程序想处理的东西是包含在文本中的信息。我们关心的文本可能包含了配置命令、控制命令、流程定义命令、供人类阅读的文档、甚至制表信息。包含配置数据和一系列命令的文本通常可以通过严格的语法检查来验证,然后才能决定是否可以依赖其中的信息。向用户提示输入文本中的错误还远远不够,这也不是我们要讨论的主题。

供人类阅读的文档似乎比较简单,但仍然有很多细节需要注意。由于它们通常被写为自然语言的形式,它们的语法和句法很难检查。不同的文本可能会使用不同的字符集和编码,如果事先不知道相关的编码信息,想检查出一段文本使用了何种字符集和编码是极其困难的,甚至是不可能的。然而,对于自然语言文档的正确呈现,这却是非常必要的。自然语言文本也有结构,但是这种结构并不明显而且还需要你至少懂一点该种自然语言。字符组成了单词,单词再形成了句子,然后句子构成了段落,但仍然有更大的结构。单独的一个段落很难定位,除非你知道文档的排版约定:究竟是每行构成一个段,还是多行组成一个段?如果是后者,我们怎么判断哪些行构成了一段?段之间可能会被空白行、缩进或者其他的特殊符号隔开。第19.10节就给出了一个读取文本文件中由空白行隔开段落的例子。

制表信息就像自然语言文本一样也有很多值得讨论的地方,它实际上给输入的格式增加了第二个维度:文本不再是线性的,也不再是一串字符,而是一个字符的矩阵,每一个单独的文本块可以被缩进和组织起来。

基本的文本操作

就像其他的数据格式,我们也需要用一些不同的方式在不同的场合下处理文本。总地来说,有三种基本的操作:

  • 解析数据并将数据放入程序内部的结构中;
  • 将数据以某种方式转化为另一种相似的形式,数据本身发生了改变;
  • 生成全新的数据。

有很多方式可以完成解析,一些特别的解析器可以有效处理有诸多限制的数据格式,并可以解析大量其他的格式。比如RFC 2822型的邮件头格式(参看Python标准库的rfc822模块)和由ConfigParser模块处理的配置文件格式。netrc模块则提供了一种解析应用特有格式的解析器例子,这个模块基于shlex模块。shlex提供一个典型的分词器(tokenizer),可应用于一些基本的语言,特别适合用来创建可读的配置文件以及让用户在一个具有交互和提示能力的环境下输入命令。Python标准库中有很多类似的特别解析器,关于它们的使用示例和方法请参看第2章和第13章。Python中还有一些正式的解析工具,它们依赖于一些庞大的附加包,可以阅读第16章的引言部分以获得一个大致的了解。

当我们提到文本的时候,我们的第一个念头往往是,所谓文本处理,有时候就是文本格式转换。在本章中,我们将会看到一些可以应用于各种场合的转换方法。有时我们要处理的文本是存储于外部文件中的,有时我们则直接处理内存中的字符串。

通过Python的print语句,文件对象或者类文件对象的write方法,我们可以轻易地产生基于程序特定结构的文本数据、对于那些将输出文件作为一个传入参数的应用,我们通常是用该程序的一个方法或者函数来完成此功能。举个例子,函数可以像下面这样使用print语句:

    print >>thefile, sometext
    thefile.write(sometext)

这就将产生的输出写入到了文件。不过,这并不是通常我们所认为的文本处理,因为在这个过程中并没有文本输入。本书有很多使用print和write的例子,在进一步的阅读中你会看到更多的示例。

文本的来源

当文本处于内存的时候,如果数据量不是很大,处理起来相对比较容易。对文本的搜索可以轻易和快速地跨越多行,而且不用担心搜索会超出缓存的边界。只要我们能设法让文本处于内存,并以一种普通字符串的形式呈现,就可以充分利用string对象的各种内建的操作来简单而快速地处理文本。

基于文件的转化则需要一些特别的对待,因为需要考虑I/O性能和可观的开销,以及实际上需要放入内存的数据量。当处理位于磁盘上的数据时,由于数据的尺寸,我们常常避免将整个文件载入内存:把一个80MB的文件全部放入内存并不是一件随随便便的事情!当我们的程序只需要处理一部分数据时,尽量让它只处理较小的数据段往往能够得到可观的性能提升,因为这样我们可以让程序拥有更多的资源来运行。相对于涉及大量磁盘读写的大块数据处理,使用谨慎的缓存管理通常能够取得更好的效果。与文件相关的内容请参看第2章。

另一个有趣的文本来源是网络。通过socket可以从网络中取回文本。我们可以把socket看成是一个文件(使用socket对象的makefile方法),从socket中取回的数据可能是完整的一块,也可能是不完整的数据块,这时我们可以继续等待直到更多的数据到达。由于在最后的数据块到达之前文本数据可能是不完整的,所以用makefile创建的文件对象可能不太适合被传递给文本处理代码。在处理来自网络连接的文本时,在进一步的处理之前,我们通常需要完全读取连接中的所有数据。如果数据很庞大,我们可以将其存入一个文件,每当有新数据部分到达,就在文件末尾不断添加,直到最后接收完所有数据,然后再将这个文件用于文本处理。如果要求文本处理这一步要在接收完所有数据之前启动,则需要在具体的处理上采用更加精巧的方法。这方面的解析器例子可以参考标准库中的htmllib和HTMLParser模块。

字符串基础

Python提供的用于文本处理的最主要的工具就是字符串—不可改变的字符序列。实际上存在两种字符串:普通字符串,包含了8位(ASCII)字符;Unicode字符串,包含了Unicode字符。我们这里不对Unicode字符串做太多讨论:它们的处理方法和普通字符串很类似,只不过它们每个字符占用2(或者4)个字节,所以它们拥有成千上万(甚至上亿)个不同的字符,而普通字符串却仅有256个不同字符。当需要处理一些有不同字母表的文本时,尤其是亚洲的象形文字,Unicode字符串的价值就体现出来了。而普通字符串对于英文以及一些非亚洲的精简语言已经够用了。比如,所有的欧洲字母表都可以用普通字符串表示,我们采取的典型的方法是使用国际标准编码ISO-8859-1(或者ISO-8859-15,如果需要欧洲的货币符号)。

在Python中,可以用下列方式表现一个文本字符串:

'this is a literal string'
"this is another string"

字符串的值被单引号或者双引号圈起。这两种表示法在程序中完全一样,都允许你在字符串内部把另一种表示法的引用符号包括进来,而无须用反斜线符号进行转义:

'isn\'t that grand'
"isn't that grand"

为了让一个文本字符串扩展到多行,可以在一行的末尾使用反斜线符号,那意味着下面的一行仍是上面字符串的延续:

big = "This is a long string\
that spans two lines."

如果想让字符串的输出分为两行,可以在字符串中嵌入换行符:

big = "This is a long string\n\
that prints on two lines."

还有一种方式是用一对连续的三引用符将字符串圈起:

bigger = """
This is an even 
bigger string that 
spans three lines.
"""

使用这种三引用符,无须在文本中加入续行符和换行符,因为文本将按照原貌被储存在Python的字符串对象中。也可以在字符串前面加一个r或者R,表示该字符串是一个真正的“原”字符串,需要它的原貌:

big = r"This is a long string\
with a backslash and a newline in it"

使用“原”字符串,反斜线转义被完全忽略了。还可以在字符串前面加一个u或者U来使之成为一个Unicode字符串:

hello = u'Hello\u0020World'

字符串是无法改变的,那意味着无论你对它进行什么操作,你总是创建了一个新的字符串对象,而不是改变了原有的字符串。字符串是字符的序列,所以也可以通过索引的方法访问单个字符:

mystr = "my string"     
mystr[0]        # 'm'
mystr[-2]       # 'n'

也可以用切片的方法访问字符串的一个部分:

mystr[1:4]      # 'y s'
mystr[3:]       # 'string'
mystr[-3:]      # 'ing'

切片的方法还可以扩展,增加第三个参数,作为切片的步长:

mystr[:3:-1]    # 'gnirt'
mystr[1::2]     # 'ysrn'

可以通过循环遍历整个字符串:

for c in mystr:

上述方法将c依次绑定到了mystr中的每一个字符。还可以构建另一个序列:

list(mystr)     # 返回 ['m','y',' ','s','t','r','i','n','g']

通过简单的加法,还可以实现字符串的拼接:

mystr+'oid'     # 'my stringoid'

乘法则完成了对字符串多次重复:

'xo'*3          # 'xoxoxo'

总之,可以对字符串做任何你能够对其他序列所做的操作,前提是不能试图改变字符序列,因为字符串是不能改变的。

字符串对象有很多有用的方法。比如,可以用s.isdigit()来测试字符串的内容,如果s不是空的而且所有的字符都是数字,该方法将会返回true(否则返回false)。还可以用s.upper()来创建一个修改过的字符串,该字符串很像原字符串s,不过其中的每一个字符都是原对应字符的大写形式。可以用haystack.count('needle')在一个字符串中搜索另一个字符串,该方法返回了子串‘needle’在字符串haystack中出现的次数。如果有一个庞大的包含多行文本的字符串,可以用splitlines来将其分隔为多个单行字符串并置入一个列表中:

list_of_lines = one_large_string.splitlines( )

然后还可以用join来重新生成一个庞大的单个字符串:

one_large_string = '\n'.join(list_of_lines)

本章展示了字符串对象的许多方法。可以在Python的Library Reference和Python in a Nutshell中读到更多的相关内容。

Python中的字符串还可以通过re模块用正则表达式来操作。正则表达式是一种强大无比(但也很复杂)的工具,你可能已经通过其他语言(比如Perl)、使用vi编辑器、或者使用命令行方式的工具(比如grep)对它有所了解了。在本章的下半章中你会看到一些使用正则表达式的例子。更多相关文档,请参考Library Reference和Python in a Nutshell。如果想掌握这方面的知识,J.E.F. Friedl的Mastering Regular Expressions(O’Reilly)也是值得推荐的读本。Python的正则表达式和Perl的正则表达式基本相同,Friedl的书对这方面的内容介绍得非常全面。

Python的标准模块string提供的很多功能和字符串方法提供的功能基本相同,后者被打包成一系列函数,而非方法。string模块还提供一些附加的功能,比如string.maketrans函数,本章中有几节展示了其用法;还有一些有用的字符串常量(比如,string.digits,值为’0123456789’),再比如Python 2.4中引入的新类Template,提供了一种简单而灵活的方式来格式化带有内嵌变量的字符串,你将在本章中看到其应用。字符串格式化操作符,%,提供了一种简洁的方法将字符串拼接并根据某些对象(比如浮点数)来精确格式化字符串。在本章中你同样会看到大量相关例子并学会如何根据自己的目的来使用%。Python还有一些标准模块或者扩展模块,可处理一些特定种类的字符串。本章并未覆盖这样的特殊领域,不过本书第12章将全面而深入地讨论XML处理方面的内容。

1.1 每次处理一个字符

感谢:Luther Blissett

任务

用每次处理一个字符的方式处理字符串。

解决方案

可以创建一个列表,列表的子项是字符串的字符(意思是每个子项是一个字符串,长度为一。Python实际上并没有一个特别的类型来对应“字符”并以此和字符串区分开来)。我们可以调用内建的list,用字符串作为参数,如下:

thelist = list(thestring)

也可以不创建一个列表,而直接用for语句完成对该字符串的循环遍历:

for c in thestring:
       do_something_with(c)

或者用列表推导中的for来遍历:

results = [do_something_with(c) for c in thestring]

再或者,和列表推导效果完全一样,可以用内建的map函数,每次取得一个字符就调用一次处理函数:

results = map(do_something, thestring)

讨论

在Python中,字符就是长度为1的字符串。可以循环遍历一个字符串,依次访问它的每个字符。也可以用map来实现差不多的功能,只要你愿意每取到一个字符就调用一次处理函数。还可以用内建的list类型来获得该字符串的所有长度为1的子串列表(该字符串的字符)。如果想获得的是该字符串的所有字符的集合,还可以调用sets.Set,并将该字符串作为参数(在Python 2.4中,可以用同样的方式直接调用内建的set):

import sets
magic_chars = sets.Set('abracadabra')
poppins_chars = sets.Set('supercalifragilisticexpialidocious')
print ''.join(magic_chars & poppins_chars)   # 集合的交集
acrd

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章