2.1 文本处理
在日常的运维工作中一般都离不开与文本,如日志分析、编码转换、ETL加工等。本节从编码原理、文件操作、读写配置文件、解析XML等实用编程知识出发,希望能抛砖引玉,为读者在处理文本问题时提供可实践的方法。
2.1.1 Python编码解码
我们编写程序处理文本的时候,不可避免地遇到各种各样的编码问题,如果对编码解码过程一知半解,遇到这类问题就会很棘手。本小节从编码解码的原理出发,结合Python 3代码实例一步步揭开文本编码的面纱,编码解码的原理是相通的,学会编码解码,对学习其他编程语言也非常有帮助。
首先我们需要明白,计算机只处理二进制数据,如果要处理文本,就需要将文本转换为二进制数据,再由计算机进行处理。
将文本转换为二进制数据就是编码,将二进制数据转换为文本就是解码。编码和解码要按照一定的规则进行,这个规则就是字符集。
以常见的ASCII编码为例,字符'a'在ASCII码表中对应的数据是97,二进制是1100001。下面在Python中验证一下:
由于ASCII编码只占用一个字节,也就是二进制8位,共有28 256种可能,完全可以覆盖英文大小写字母及特殊符号。而我们中文汉字远超过256个,使用ASCII编码的一个字节来处理中文显然是不够用的,于是我国就制订了支持中文的GB2312编码,使用两个字节,可以支持216共65536种汉字,可以覆盖常用的中文汉字60370个(当代《汉语大字典》(2010年版)收字60370个)。
例如:汉字的“汉”。
在这里介绍几种常见的中文编码。
GB2312或GB2312-80是中国国家标准简体中文字符集,共收录6763个汉字,同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。
GBK即汉字内码扩展规范,共收入21886个汉字和图形符号。
GB 8030与GB2312-1980和GBK兼容,共收录汉字70244个,是一二四字节变长编码。
由上可以看出支持的汉字范围:GB18030 > GBK > GB2312。
对于一些生僻字,可能需要GBK或GB18030进行编码,如“祎”。
这仅仅是适用中文文本的一个编码,全世界有上百种语言,每种语言都设计自己独特的编码,这样计算机在跨语言进行信息传输时还是无法沟通(出现乱码)的,于是Unicode编码应运而生,Unicode使用2~4个字节编码,已经收录136690个字符,并且还在一直不断扩张中。把所有语言统一到一套编码中,这套编码就是Unicode编码。使用Unicode编码,无论处理什么文本都不会出现乱码问题。Unicode编码使用两个字节(16位bit)表示一个字符,比较偏僻的字符需要使用4个字节。
Unicode起到以下作用。
直接支持全球所有语言,每个国家都可以不用再使用自己之前的旧编码了,用Unicode就可以。
Unicode包含了与全球所有国家编码的映射关系。
几乎所有的系统、编程语言都默认支持Unicode。但是新的问题又来了,如果一段纯英文文本,用Unicode编码存储就会比用ASCII编码多占用一倍空间!存储和网络传输时一般数据都会非常多。为了解决上述问题,UTF编码应运而生,UTF编码将一个Unicode字符编码成1~6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4~6个字节。注意,从Unicode到UTF并不是直接对应的,而是通过一些算法和规则来转换的。UTF编码有以下三种。
UTF-8:使用1、2、3、4个字节表示所有字符,优先使用1个字节,若无法满足,则增加一个字节,最多4个字节。英文占1个字节、欧洲语系占2个字节、东亚占3个字节,其他及特殊字符占4个字节。
UTF-16:使用2、4个字节表示所有字符,优先使用2个字节,否则使用4个字节表示。
UTF-32:使用4个字节表示所有字符。
例如:汉字的“汉”,在UTF-8字符集中3个字节。
>>> list("汉".encode("utf-8")) [230, 177, 137]
而英文无论采集哪种编码,都是一致的。如果使用纯英文编写代码,就基本不会遇到编码问题。如"a"在ASCII、GBK、UTF-8中的编码结果都是一致的。
>>> list("a".encode("ascii")) [97] >>> list("a".encode("gbk")) [97] >>> list("a".encode("utf-8")) [97]
下面结合Python代码实例来理解编码,如图2.1所示。
图2.1 代码实例
我们使用vim编辑器编写str_encode_decode.py,在第一行指定Python解释器以UTF-8编码解码源文件,并保存为UTF-8编码的文本文件,然后运行程序。这一编码解码过的程如图2.2所示。
图2.2 Python源代码的编码解码过程
上图中的Unicode字符串就是我们在编辑器中看到的字符串,如“我是中国人”这个字符串,在Python 3中所定义的字符串就是Unicode字符串。Unicode字符串可以编码为任意编码格式的字节码,解码时使用同一编码解码即可得到原来的Unicode字符串。
上述1.py的第5行将Unicode字符串内容以UTF-8的编码方式写入到a.txt,第9行从a.txt读取内容并以UTF-8的编码解码输出Unicode字符串,确保写入编码和读取编码一致就不会出现编码问题。
上述代码的运行结果如图2.3所示。
图2.3 运行结果
在这里顺带介绍一下Python语言的with … as …的用法。有一些任务,可能事先需要设置,事后做清理工作。对于这种场景,Python的with语句提供了一种非常方便的处理方式。一个很好的例子是文件处理,你需要获取一个文件句柄,并从文件中读取数据,然后关闭文件句柄。如果不用with语句,代码如下:
1 file = open("a.txt") 2 data = file.read() 3 file.close()
这里有两个问题:一是可能忘记关闭文件句柄;二是文件读取数据发生异常,没有进行任何处理。下面是处理异常的加强版本:
虽然这段代码运行良好,但是太冗长了,这时候就是with一展身手的时候了。除了有更优雅的语法,with还可以很好地处理上下文环境产生的异常。下面是with版本的代码:
with语句里是怎么执行的呢?Python对with的处理是非常聪明的,with所求值的对象必须有__enter__()方法和__exit__()方法,紧跟with后面的语句被求值后,调用对象的__enter__()方法,该方法的返回值将被赋值给as后面的变量。当with后面的代码块全部被执行之后,将调用前面返回对象的__exit__()方法来收尾。
读者可能会有疑问,如果编写Python程序时未指定Python解释器以何种编码解码呢?答案是使用系统的默认编码。默认编码可以通过sys.getdefaultencoding()来查看Python解释器会用的默认编码,以Windows系统为例:
>>> import sys >>> sys.getdefaultencoding() 'utf-8' >>>
说明在此电脑上Python解释器默认使用的是UTF-8编码,如果不指定Python解释器以何种编码解码,则默认以UTF-8方式解码源文件,因此在保存源代码文件时请确保以UTF-8编码保存。
2.1.2 文件操作
用Python或其他语言编写应用程序时,若想把数据永久保存下来,必须保存于硬盘中,这就涉及我们编写应用程序来操作硬件,而应用程序是无法直接操作硬件的,需要通知操作系统,由操作系统完成复杂的硬件操作。操作系统把复杂的硬件操作封装成简单的接口给用户/应用程序使用,其中文件就是操作系统提供给应用程序来操作硬盘虚拟接口,用户或应用程序通过操作文件,可以将自己的数据永久保存下来。有了文件的概念,我们无须再考虑操作硬盘的细节,只需要关注操作文件的流程即可。
(1)打开文件,得到一个文件句柄,并赋值给一个变量。
(2)通过句柄对文件进行操作。
(3)关闭文件。
1. 普通文件操作
Python文件操作也是非常简单,只需要一个open函数返回一个文件句柄,无须导入任何模块。
open函数原型如下:
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
其中:
(1)参数file是一个表示文件名称的字符串,如果文件不在程序当前的路径下,就需要在前面加上相对路径或绝对路径。
(2)参数mode是一个可选字参数,指示打开文件的方式,若不指定,则默认为以读文本的方式打开文件。字符串及含义可参见表2-1。
表2-1 字符串及含义
默认的打开方式是'rt' (mode='rt')。Python是区分二进制方式和文本方式的,当以二进制方式打开一个文件时(mode参数后面跟'b'),返回一个未经解码的字节对象;当以文本方式打开文件时(默认是以文本方式打开,也可以mode参数后面跟't'),返回一个按系统默认编码或参数encoding传入的编码来解码的字符串对象。
(3)buffering是一个可选的参数,buffering=0表示关闭缓冲区(仅在二进制方式打开时可用);buffering=1表示选择行缓冲区(仅在文本方式打开时可用);buffering大于1时,其值代表固定大小的块缓冲区的大小。当不指定该参数时,默认的缓冲策略是这样的:二进制文件使用固定大小的块缓冲区,文本文件使用行缓冲区。
【示例2-1】先来看一个例子。
将上述代码保存为read_write_file.py,运行结果如图2.4所示。
图2.4 运行结果
从上面的例子可以看出,以二进制读取文件时,读取的是文件字符串的编码(以encoding指定的编码格式进行的编码),将读取的字节对象解码,可得出原字符串。
请注意以下几点:
(1)记得使用完毕后及时关闭文件,释放资源。打开一个文件包含两部分资源:操作系统级打开的文件+应用程序的变量。在操作完毕一个文件时,必须把与该文件的这两部分资源一个不落地回收,回收操作系统级打开的文件,如f.close(),回收应用程序级的变量,如del f。其中del f一定要发生在f.close()之后,否则就会导致操作系统打开的文件还没有关闭,白白占用资源,而Python自动的垃圾回收机制决定了我们无须考虑del f,这就要求我们,在操作完毕文件后,一定要记住f.close()。刚开始的时候很容易忘记使用f.close()方法去关闭,推荐傻瓜式操作方式:使用with关键字来帮我们管理上下文,系统会自动为我们关闭文件和处理异常,如下面两行代码即可完成安全的写操作。
with open('a.txt','w') as f: f.write(“hello word”)
(2)open()函数是由操作系统打开文件,如果我们没有为open指定编码,那么打开文件的默认编码很明显是操作系统默认的编码:在Windows下是gbk,在Linux下是utf-8。若要保证不乱码,就必须让读取文件和写入文件使用的编码一致。
常见的文件操作方法可参见表2-2。
表2-2 常见的文件操作方法
读取文件内位置的定位方法:
(1)通过read方法传输参数,如read(3),当文件打开方式为文本模式时,代表读取3个字符,当文件打开方式为二进制模式时,代表读取3个字节。
(2)以字节为单位定位,如seek、tell等方法。其中seek有3种移动方式:0、1、2,其中1和2必须在二进制模式下进行,但无论哪种模式,都是以bytes为单位移动的。f.tell()返回文件对象当前所处的位置,它是从文件开头开始算起的字节数。如果要改变文件当前的位置,可以使用f.seek(offset, from_what)函数。from_what如果是0,则表示开头;如果是1,则表示当前位置;如果是2,则表示文件的结尾。例如:
seek(x,0)表示从起始位置即文件首行首字符开始移动x个字符;
seek(x,1)表示从当前位置向后移动x个字符;
seek(-x,2)表示从文件的结尾向前移动x个字符。
【示例2-2】在文件中定位。
>>> f = open("tmp.txt", "rb+") >>> f.write(b"abcdefghi") 9 >>> f.seek(5) # 移动到文件的第六个字节 5 >>> print(f.read(1)) b'f' >>> f.seek(-3, 2) # 移动到文件的倒数第三个字节 6 >>> print(f.read(1)) b'g'
【示例2-3】基于seek实现类似Linux命令tail -f的功能(文件名为lx_tailf.py)。
当tmp.txt追加新的内容时,新内容会被程序立即打印出来。
2. 大文件的读取
当文件较小时,我们可以一次性全部读入内存,对文件的内容做出任意修改,再保存至磁盘,这一过程会非常快。
【示例2-4】如下代码将文件a.txt中的字符串str1替换为str2。
当文件很大时,如GB级的文本文件,上面的代码运行将会非常缓慢,此时我们需要使用文件的可迭代方式将文件的内容逐行读入内存,再逐行写入新文件,最后用新文件覆盖源文件。
【示例2-5】对大文件进行读写。
本示例中的大文件为a.txt,当我们打开文件时,会得到一个可迭代对象read_f,对可迭代对象进行逐行读取,可防止内存溢出,也会加快处理速度。
处理大数据还有多种方法,如下:
(1)通过read(size)增加参数,指定读取的字节数。
(2)通过readline,每次只读一行。
file对象常用的函数参见表2-3。
表2-3 file对象常用的函数
3. 序列化和反序列化
什么是序列化和反序列化呢?我们可以这样简单地理解:
序列化:将数据结构或对象转换成二进制串的过程。
反序列化:将在序列化过程中所生成的二进制串转换成数据结构或对象的过程。
Python的pickle模块实现了基本的数据序列和反序列化。通过pickle模块的序列化操作,我们能够将程序中运行的对象信息保存到文件中并永久存储。通过pickle模块的反序列化操作,我们能够从文件中创建上一次程序保存的对象。
基本方法如下:
pickle.dump(obj, file, [,protocol])
该方法实现序列化,将对象obj保存至文件中。
x=pickle.load(file)
该方法实现反序列化,从文件中恢复对象,并将其重构为原来的Python对象。
注解:从file中读取一个字符串,并将其重构为原来的Python对象。
【示例2-6】序列化实例(example_serialize.py)。
上述代码将不同的Python对象依次写入文件,并打印对象的相关信息,运行结果如图2.5所示。
图2.5 运行结果
【示例2-7】反序列化演示(example_deserialization.py)。
上述代码从文件中依次恢复序列化对象,并打印对象的相关信息,运行结果如图2.6所示。
图2.6 运行结果
可以看出运行结果与序列化实例运行的结果完全一致。
2.1.3 读写配置文件
配置文件是供程序运行时读取配置信息的文件,用于将配置信息与程序分离,这样做的好处是显而易见的:例如在开源社区贡献自己源代码时,将一些敏感信息通过配置文件读取;提交源代码时不提交配置文件可以避免自己的用户名、密码等敏感信息泄露;我们可以通过配置文件保存程序运行时的中间结果;将环境信息(如操作系统类型)写入配置文件会增加程序的兼容性,使程序变得更加通用。
Python内置的配置文件解析器模块configparser提供ConfigParser类来解析基本的配置文件,我们可以使用它来编写Python程序,让用户最终通过配置文件轻松定制自己需要的Python应用程序。
常见的pip配置文件如下。
[global] index-url = https://pypi.doubanio.com/simple trusted-host = pypi.doubanio.com
【示例2-8】现在我们编写一个程序来读取配置文件的信息(read_conf.py)。
上述代码通过实例化ConfigParser类读取配置文件,遍历配置文件中的section信息及其键值信息,通过索引获取值信息。在命令窗口执行python read_conf.py得到如图2.7所示的运行结果。
图2.7 运行结果图
【示例2-9】将相关信息写入配置文件(write_conf.py)。
上述write_conf.py通过实例化ConfigParser类增加相关配置信息,最后写入配置文件。执行python write_conf.py,运行结果如图2.8所示。
图2.8 运行结果
从上面读写配置文件的例子可以看出,configparser模块的接口非常直接、明确。请注意以下几点:
section名称是区分大小写的。
section下的键值对中键是不区分大小写的,config["bitbucket.org"]["User"]在写入时会统一变成小写user保存在文件中。
section下的键值对中的值是不区分类型的,都是字符串,具体使用时需要转换成需要的数据类型,如int(config['topsecret.server.com'][ 'port']),值为整数50022。对于一些不方便转换的,解析器提供了一些常用的方法,如getboolean()、getint()、getfloat()等,如config["DEFAULT"].getboolean('Compression'))的类型为bool,值为True。用户可以注册自己的转换器或定制提供的转换方法。
section的名称是[DEFAULT]时,其他section的键值会继承[DEFAULT]的键值信息。如本例中config["bitbucket.org"]['ServerAliveInterval'])的值是45。
2.1.4 解析XML文件
XML的全称是eXtensible Markup Language,意为可扩展的标记语言,是一种用于标记电子文件使其具有结构性的标记语言。以XML结构存储数据的文件就是XML文件,它被设计用来传输和存储数据。例如有以下内容的xml文件:
<note> <to>George</to> <from>John</from> <heading>Reminder</heading> <body>Don't forget the meeting!</body> </note>
其内容表示一份便签,来自John,发送给George,标题是Reminder,正文是Don't forget the meeting!。XML本身并没有定义note、to、from等标签,是生成xml文件时自定义的,但我们仍能理解其含义。XML文档仍然没有做任何事情,它仅仅是包装在XML标签中的纯粹信息。我们编写程序来获取文档结构信息就是解析XML文件。
Python有三种方法解析XML:SAX、DOM、ElementTre。
1. SAX(simple API for XML )
SAX是一种基于事件驱动的API,使用时涉及两个部分,即解析器和事件处理器。解析器负责读取XML文件,并向事件处理器发送相应的事件(如元素开始事件、元素结束事件)。事件处理器对相应的事件做出响应,对数据做出处理。使用方法是先创建一个新的XMLReader对象,然后设置XMLReader的事件处理器ContentHandler,最后执行XMLReader的parse()方法。
创建一个新的XMLReader对象,parser_list是可选参数,是解析器列表xml.sax.make_parser( [parser_list] )。
自定义事件处理器,继承ContentHandler类,该类的方法可参见表2-4。
表2-4 ContentHandler类的方法
执行XMLReader的parse()方法:
xml.sax.parse( xmlfile, contenthandler[, errorhandler])
参数说明:
xmlstring:xml字符串。
contenthandler:必须是一个ContentHandler的对象。
errorhandler:如果指定该参数,errorhandler必须是一个SAX ErrorHandler对象。
【示例2-10】下面来看一个解析XML的例子。example.xml内容如下:
read_xml.py内容如下:
代码说明:read_xml.py自定义一个MenuHandler,继承自xml.sax.ContentHandler,使用ContentHandler的方法来处理相应的标签。在主程序入口先获取一个XMLReader对象,并设置其事件处理器为自定义的MenuHandler,最后调用parse方法来解析example.xml。运行结果如图2.9所示。
图2.9 运行结果
SAX用事件驱动模型,通过在解析XML的过程中触发一个个的事件并调用用户定义的回调函数来处理XML文件,一次处理一个标签,无须事先全部读取整个XML文档,处理效率较高。其适用场景如下:
对大型文件进行处理。
只需要文件的部分内容,或者只须从文件中得到特定信息。
想建立自己的对象模型时。
2. DOM(Document Object Model)
文件对象模型(Document Object Model,DOM)是W3C组织推荐的处理可扩展置标语言的标准编程接口。一个DOM的解析器在解析一个XML文档时,一次性读取整个文档,把文档中的所有元素保存在内存中一个树结构里,之后可以利用DOM提供的不同函数来读取或修改文档的内容和结构,也可以把修改过的内容写入xml文件。
【示例2-11】使用xml.dom.minidom解析xml文件。
dom_xml.py内容如下:
代码说明:代码使用minidom解析器打开XML文档,使用getElementsByTagName方法获取所有标签并遍历子标签,逻辑上比SAX要直观,运行结果如图2.10所示,与SAX运行结果一致。
图2.10 运行结果
3. ElementTre
ElementTre将XML数据在内存中解析成树,通过树来操作XML。
【示例2-12】ElementTre解析XML。
ElementTre_xml.py内容如下:
代码相当简洁,运行结果如图2.11所示。
图2.11 运行结果