在Python读取文件中,最让人头疼的就是对文本文件的读取,因为对文本文件读取涉及到编码。如果一个采用GBK编码的文件,使用UTF-8进行解码,那么得到的字符就会产生乱码,因此作为程序员,我们在开发中理解正在处理的是那种字符串数据非常重要。
1、Python中的字符串
实际上Python语言中有两种不同的字符串,一种是存储文本(是str类的实例,下文称为文本字符串),一种是存储原始字节(bytes类型,下文称为字节字符串)。看下面的例子:
text_str = "我爱中国"
byte_str = b"I Love China"
print(type(text_str))
print(type(byte_str))
运行结果是:
字节字符串只需要在""前加入b就可以了。不过这里需要注意,如果采用b"xxxx"的方式进行字节字符串的创建,那引号中的内容必须是在ASCII表中的字符(26个字母加上一些符号),如果使用b“我爱中国”,将会抛出“SyntaxError: bytes can only contain ASCII literal characters."异常。这因为在Python中,文本字符串内部是使用Unicode储存的,而字节字符串储存原始字节并显示ASCII(当字节字符串被发送到print()时,会显示每个字节)。如果采用b""的方式进行创建字节字符串,那解释器是无法知道非ASCII字符应该采用那种方式进行转换成二进制字节的(也就是不知道该采用那种编码方式)。
上面提到了编码方式以及ASCII表,这些概念读者们可以从互联网上得到非常详细的解答,这里我们只做简单比喻。ASCII码采用1个字节(8bit)的方式将字符与二进制码一一对应,因此可以表示255种不不同的字符(比如65对应字符“A”)。这些二进制码与字符的对应关系被编成了一个表,称为ASCII表。计算机发展之后各国家的字符太多以至于255种对应关系已经表示不了了,所以扩展出了一个叫做Unicode字符集,可以理解为扩大版的ASCII表,它采用更多的字节去表示一个字符,因此Unicode字符集可以表示更多的字符。我们下面需要了解的概念是编码方式,编码方式可以理解为采用怎样的算法将01010101这串二进制码变成可以进行对应的字符。常见的Unicode字符集的编码方式有UTF-8,UTF-16,UTF-32。因此明确字符集+编码方式,就能够准确的讲一个二进制码转化为字符了(详情参看视频中的解读)。
回归正题,Python中的对于文本字符串与字节字符串之间是存在互换关系的。文本字符串中包含了一个encode()方法,将str对象转化为bytes对象,encode()方法中有一个必须参数,表示采用什么方式对文本字符进行编码。字节字符串中也包含了一个decode()方法,将bytes对象转化为str对象,decode()方法也包含有一个必须参数,表示采用什么方式对二进制字符串解码。比如看下面这个例子:
text_str = "我爱中国"
byte_str = text_str.encode("utf-8")
print(type(text_str))
print(type(byte_str))
print(byte_str)
我们将"我爱中国"这几个字通过"utf-8"的方式编码成了字节字符串,并打印字节字符串的内容,执行结果如下:
我们需要注意的是,"我爱中国"只有4个字符,为什么转换后的字节字符串有这么多个字符啊。字节字符串中的每个\xe6表示的是16进制的数字(也就是8个bit,即1个字节),所以byte_str打印出了12个字节,为什么呢?那是因为中文字符采用utf-8编码后,每个中文字符需要用3个字节去表示,也就是/xe6/x88/x91表示字符"我",不信可以看下面的代码。
byte_str = b'\xe6\x88\x91'
text_str = byte_str.decode("utf-8")
print(byte_str, "转换后结果是:", text_str)
结果是:
有读者可能会问,刚才不是说采用b“xxxx”的方式进行创建字节字符串只能使用ASCII的方式么,为什么这里可以用"/xe6/x88/x91"呢?实际上b"xxx"的正确创建规则就是这样的\x16进制数的方式,只不过由于ASCII表中刚好每个字符就是对应一个字节(一个16进制数),所以解释器自动将一个ASCII表中的字符转化为\x的形式了。
2、读取文件
我们需要有这样一个共识文件总是储存的是字节。而我们想要从一个文件中读取文本数据,那就涉及到了将字节字符串(bytes)变为文本字符串(str)的过程,也就是解码过程。
Python中我们常常使用with open(‘xxx’,‘r’) as f: 的这种方式读取文件。这里采用“r”,表示读取的是文本字符串。现在问题来了刚不是说文件储存的是字节吗?这里是怎么读取成文本字符串的呢?因为Python解释器中,自动为我们将字节字符串解码(decode)成文本字符串了。Python会读取当前操作系统的默认编码方式(在Mac OS和Linux系统下,默认的编码方式UTF-8,而在Windows上则使用的是CP-963或者其他的编码方式),并使用这种默认的编码方式对字节字符串进行解码。这也就是为什么,我们创建文件后,直接使用with读取不会出问题,因为我们创建文件和解码文件都是采用的当前系统下默认的编码方式。
但并不是所有的文件都会在同一个系统中完成编码/解码的,因此如果在Linux上保存的文件(默认使用UTF-8编码保存的),在Windows环境下采用默认编码方式读取,就会产生问题了。因此显式的表明当前读取的文件采用的是什么样的编码方式是非常重要,我们可以在open()中添加encoding=“xxx”参数,显式的表明该文件是采用什么样的编码得到的,Python解释器会根据这个对字节字符串进行解码。看例子,我们在window环境下创建了一个UTF-8的方式保存的文本,并在Python中读取:
with open("字符串与Unicode\\utf_8文本.txt", "r") as f:
text_str = f.read()
print(type(text_str))
print(text_str)
执行的结果是:
由于我们并没有显式的给定该文本是由utf-8编码方式保存的,因此Python默认使用WIndows的编码方式进行自动解码(即采用gbk编码方式),所以报错了,下面我们在open中指定该文件的是由utf-8保存得到的,这样Python解释器就不会使用自动的方式进行解码了。
with open("字符串与Unicode\\utf_8文本.txt", "r", encoding="utf-8") as f:
text_str = f.read()
print(type(text_str))
print(text_str)
执行结果是:
得到了我们想要的答案。本例中的utf_8文本.txt文件如下:
如果我们只想读取出字节字符串,那么我们可以在open()时采用"rb"模式,这样读取的就是字节字符串了。(需要注意的是采用rb,wb这种读取字节字符串的方式,是不能指定encoding参数的,因为encoding参数本就是为了字节转字符所设定的,现在不需要转了,所以不用指定了),我们看下面代码,依旧是读取上述txt文件,不过读取的是字节字符串。
with open("字符串与Unicode\\utf_8文本.txt", "rb") as f:
byte_str = f.read()
print(type(byte_str))
print(byte_str)
print(byte_str.decode("utf-8"))
代码的第6行,我们还将读取到的字节字符串采用“utf-8”的方式,手动解码并打印结果,最后执行结果如下:
再提一句,如果我们打开的文件不是文本类型的,比如音频文件,我们读取就不能使用"r"模式了,因为Python解释器无法将音频的字节字符流转换为文本的,因此我们读取只能采用“rb”模式(其实我们对于非文本类型的文件都不会只采用open方式的,一般都有支持的库)。