总结
Python2.x助长了许多不良习惯WRT文本处理。特别是,它名为str的类型实际上并不代表Unicode标准中的文本(该类型是Unicode),默认的“字符串文字”实际上产生了一系列原始字节-如果可以避免采用“代码页”样式的编码,可以使用一些方便的函数将其视为字符串。
在3.x中,“字符串文字”现在产生实际的字符串,内置功能不再在这两种类型之间进行任何隐式转换。因此,同一代码现在有一个TypeError,因为文本和变量的类型不兼容。要解决此问题,必须替换或转换其中一个值,以便类型匹配。
Python文档有一个非常详细的指南,可以正确使用Unicode。
在问题中的示例中,输入文件被处理为包含文本。因此,文件应该首先以文本模式打开。即使在2.x中,文件也会以二进制模式打开的唯一好原因是避免通用换行转换;在3.x中,这是通过在文本模式下打开文件时指定换行关键字参数来完成的。
要将文件正确读取为文本,需要知道文本编码,该编码在代码中由(字符串)名称指定。编码iso-8859-1是一种安全的回退;它按顺序分别解释每个字节,表示前256个Unicode码点之一(因此它不会因无效数据而引发异常)。utf-8在编写时更为常见,但它不接受任意数据。(然而,在许多情况下,对于英语文本,区别并不重要;这两种编码以及更多编码都是ASCII的超集。)
因此:
with open(fname, 'r', newline='\n', encoding='iso-8859-1') as f:
lines = [x.strip() for x in f.readlines()]
# proceed as before
# If the results are wrong, take additional steps to ascertain the correct encoding
从2.x迁移到3.x时如何创建错误
在2.x中,“somepattern”创建了一个str,即程序员可能会假装为文本的字节序列。str类型与字节类型相同,但不同于正确表示文本的unicode类型。提供了许多方法来将该数据视为文本,但它不是文本的适当表示。假设每个值作为文本字符(编码)的含义。(为了将原始数据视为“文本”,有时会在str和unicode类型之间进行隐式转换。然而,这会导致自身的混淆错误,例如从编码尝试中获取UnicodeDecodeError,反之亦然)。
在3.x中,“somepattern”创建了一个str;但现在str表示Unicode使用正确的文本表示字符串类型。(unicode不再用作类型名称,只有字节是指字节类型的序列。)对字节进行了一些更改,以将其与假定的编码解释的文本分离(特别是,索引到字节对象现在会产生int,而不是1元素字节),但是许多奇怪的遗留方法仍然存在(包括那些甚至很少使用实际字符串的方法,如zfill)。
为什么这会导致问题
数据tmp是一个字节实例。它来自二进制源:在本例中,一个以“b”文件模式打开的文件。在其他情况下,它可能来自原始网络套接字、使用urllib或类似工具发出的web请求或其他API调用。
这意味着它不能与字符串组合执行任何有意义的操作。字符串的元素是Unicode代码点(即,以表示所有世界语言和许多其他符号的通用形式表示文本字符的抽象)。字节的元素是字节。(特别是在3.x中,它们被解释为范围从0到255(含0到255)的无符号整数。)
当代码被迁移时,字面上的“某种模式”从描述字节变成了描述文本。因此,代码从合法的比较(字节序列到字节序列)变成了非法的比较(字符串到字节顺序)。
解决问题
为了对字符串和字节序列进行操作-无论是使用==检查相等性,使用<检查词典比较,使用In搜索子字符串,使用+连接,还是其他任何操作-字符串必须转换为字节序列,反之亦然。一般来说,只有一个答案是正确的、明智的,这取决于上下文。
固定源
有时,一开始就可以看出其中一个值是“错误的”。例如,如果读取文件是为了生成文本,那么它应该以文本模式打开。在3.x中,文件编码可以简单地作为一个编码关键字参数传递给打开,并且可以无缝地处理到Unicode的转换,而无需将二进制文件输入到显式转换步骤(因此,通用换行处理仍然无缝地进行)。
在原始示例的情况下,可能如下所示:
with open(fname, 'r') as f:
lines = [x.strip() for x in f.readlines()]
此示例假定文件的默认编码依赖于平台。这通常适用于在同一台计算机上以直接方式创建的文件。然而,在一般情况下,数据的编码必须是已知的,才能正确使用。
如果已知编码为UTF-8,则通常指定为:
with open(fname, 'r', encoding='utf-8') as f:
lines = [x.strip() for x in f.readlines()]
类似地,本应为字节文字的字符串文字只是缺少前缀:要使字节序列表示整数值[101,120,97,109,112,108,101](即字母示例的ASCII值),请写字节文字b'example',而不是字符串文字'example')。同样地,反过来。
在原始示例的情况下,它看起来像:
if b'some-pattern' in tmp:
这里面有一个内置的保护措施:字节字面语法只允许ASCII字符,所以像b'ëxãmþlê'这样的东西将被捕获为SyntaxError,而不管源文件的编码是什么(因为不清楚字节值是指什么;在旧的隐含编码方案中,ASCII范围已经建立得很好,但其他一切都悬而未决。)当然,具有表示值128..255的元素的字节文本仍然可以通过对这些值使用\x转义来编写:例如,b'\xebx\xe3m\xel\xea'将生成与Latin-1(ISO 8859-1)编码中的文本ëxãmþlê相对应的字节序列。
适当时转换
只有在确定了编码后,才能在字节序列和文本之间进行转换。一直如此;我们过去只是在本地假设一种编码,然后基本上忽略了我们已经这样做了。(像东亚这样的地方的程序员历史上更清楚这个问题,因为他们通常需要处理具有256个以上不同符号的脚本,因此他们的文本需要多字节编码。)
在3.x中,由于没有压力将字节序列隐式处理为具有假定编码的文本,因此在幕后没有隐式转换步骤。这意味着理解API很简单:字节是原始数据;因此,它们用于对文本进行编码,这是一种抽象。因此,str(表示文本)提供了.encode()方法,以便将文本编码为原始数据。类似地,.decode()方法由字节(表示字节序列)提供,以便将原始数据解码为文本。
将这些应用到示例代码中,再次假设UTF-8编码是适当的,可以得到:
if 'some-pattern'.encode('utf-8') in tmp:
and
if 'some-pattern' in tmp.decode('utf-8'):