了解编码问题

了解编码问题

虽然有一般性的UnicodeError异常,但是报告错误时几乎都会指明具体的异常:UnicodeEncoderError
(把字符串转换成二进制序列时)或UnicodeDecoderError(把二进制序列转换成字符串时)。如果
源码的编码与预期不符,加载Python模块时还可能抛出SyntaxError

处理UnicodeEncoderError

多数非UTF编码器只能处理Unicode字符的一小部分子集。把文本转换成字节序列时,如果目标编码中
没有定义某个字符,那就会抛出UnicodeEncoderError异常,除非把errors参数传给编码方法或函数,
对错误进行特殊处理。处理错误的方式如下:

city = 'São Paulo'
city.encode('utf_8')
b'S\xc3\xa3o Paulo'
city.encode('utf_16')
b'\xff\xfeS\x00\xe3\x00o\x00 \x00P\x00a\x00u\x00l\x00o\x00'
city.encode('iso8859_1')
city.encode('cp437')
---------------------------------------------------------------------------

UnicodeEncodeError                        Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_390296/2919144370.py in <module>
----> 1 city.encode('cp437')
      2 


C:\ProgramData\Miniconda3\envs\tf2\lib\encodings\cp437.py in encode(self, input, errors)
     10 
     11     def encode(self,input,errors='strict'):
---> 12         return codecs.charmap_encode(input,errors,encoding_map)
     13 
     14     def decode(self,input,errors='strict'):


UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined>
city.encode('cp437', errors='ignore')
b'So Paulo'
city.encode('cp437', errors='replace')
b'S?o Paulo'
city.encode('cp437', errors='xmlcharrefreplace')
b'S&#227;o Paulo'
  1. 'utf_?'编码能处理任何字符串
  2. 'iso8859_1'编码也能处理字符串'São Paulo'
  3. 'cp437'无法编码'ã'。默认的错误处理方式'strict'抛出UnicodeEncodeError
  4. errors='ignore'处理方式悄无声息地跳过无法编码的字符;这样做通常很是不妥
  5. 编码时指定errors='replace',把无法编码的字符替换成'?';数据损坏了,但是直到出了问题
  6. 'xmlcharrefreplace'把无法编码的字符替换成XML实体

处理UnicodeDecodeError

不是每个字节都包含有效地ASCII字符,也不是每一个字符序列都是有效的UTF-8或UTF-16。因此,把二进制序列转换成文本时,如果假设
是这两个编码中的一个没遇到无法转换的字节序列时会抛出UnicodeDecodeError

另一方面很多陈旧的8位编码——如'cp1252'、'iso8859_1'和'koi8_r'——能解码任何字节序列流而不抛出错误。
因此,如果程序使用错误的8位编码,编码过程悄无声息,而得到的是无用输出。

# 把字节序列编码成字符串:成功和错误处理
octets = b'Montr\xe9al'
octets.decode('cp1252')
'Montréal'
octets.decode('iso8859_7')
'Montrιal'
octets.decode('koi8_r')
'MontrИal'
octets.decode('uft_8')
---------------------------------------------------------------------------

LookupError                               Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_390296/139564355.py in <module>
----> 1 octets.decode('uft_8')


LookupError: unknown encoding: uft_8
octets.decode('utf_8', errors='replace')
'Montr�al'
  1. 这些字节序列是是哦那个latin1编码的'Montréal';'\xe9'字节对应'é'
  2. 可以使用'cp1252'(Windows 1252)解码,因为它是latin1的有效超集
  3. ISO-8859-7可用于编码希腊文,因此无法正确解释'\xe9'字节,而且没有抛出错误
  4. KOI8-R用于编码俄文;这里'\xe9'表示西里尔字母'И'
  5. 'utf_8'编解码器检测到octets不是有效地UTF-8字符串,抛出UnicodeDecodeError
  6. 使用'replace'错误处理方式,'\xe9'替换成了'�'(码位是U+FFFD),这是官方指定的REPLACE CHARACTER(替换字符),表示未知字符

使用预期之外的编码加载模块时抛出的SyntaxError

Python3默认使用UTF-8编码源码,Python2(从2.5开始)则默认使用ASCII。如果加载的.py模块中包含UTF-8之外的数据,而且没有声名编码,会得到SyntaxError

GNU/Linux和OS X系统大都使用UTF-8,因此打开在Windows系统中使用cp1252编码的.py文件时可能发生这种情况。这个错误在Windosw版Python中也可能会发生,因为Python3
为所有平台设置的默认编码都是UTF-8

为了修正这个问题,可以在文件顶部添加一个神奇的coding注释

# coding:cp1252
print('Montréal')
Montréal

如何找出字节序列的编码

有些通信协议和文件格式,如HTTP和XML,包括明确指明内容编码的首部。可以肯定的是,某些字节流不是ASCII,因为其中包含大于
127的字节值,而且指定UTF-8和UTF-16的方式也限制了可用的字节序列。不过即使如此,也不能根据特定的位模式来100%确定二进制文件的编码时ASCII或UTF-8

然而,就像人类语言也有规则和限制一样,只要假定字节流是人类可读的纯文本,就可能通过试探和分析找出编码。例如b'\x00'字节经常出现,
那么可能是16位或32位编码,而不是8位编码方案,因为纯文本中不能包含空字符;如果字节序列b'\x20\x00'经常出现,那么可能是UTF-16LE编码中的空格字符(U+0020),而不是鲜为人知
的U+2000 EN QUAD字符

统一字符编码侦测包Chardet就是这样工作的,它能识别所支持的30种编码。Chardet是一个Python库,可以在程序中使用,不过它也提供了命令行工具chardetect

$ chardetect 04-text-btye.asciidoc

二进制序列编码文档通常不会指明自己的编码,但是UTF格式可以在文本内容的开头添加一个字节序标记

有用的鬼符

u16 = 'Eontréal'.encode('utf_16')
u16
b'\xff\xfeE\x00o\x00n\x00t\x00r\x00\xe9\x00a\x00l\x00'

b'\xff\xfe'这是BOM,即字节序标记(byte-order mark),指明编码时使用Intel CPU的小字节序列

在小字节序设备中,各个码位的最低有效字节在前面:字母'E'的码位是U+0045(十进制数69),在字节偏移的第2位和第3位编码为69和0

list(u16)

在大字节序CPU中,编码顺序是相反的;'E'编码为0和69

为了避免混淆,UTF-16要在编码的文本前面加上特殊的不可见符号ZERO WIDTH NO-BREAK SPACE(U+FFFF)。在小字节序系统中,这个字符编码为b'\xff\xfe'(十进制数255,254)。
因为按照设计,U+FFFE字符不存在,在小字节编码中,字节b'\xff\xfe'必定是ZERO WIDTH NO-BREAK SPACE,所以编码器知道该用哪个字节序。

UTF-16有两个变种:UTF-16LE,显式指明使用小字节序;UTF-16BE,显式指明使用大字节序。如果使用这两个变种,则不会生成BOM

u16le = 'Eontr éal'.encode('utf_16le')
list(u16le)
[69, 0, 111, 0, 110, 0, 116, 0, 114, 0, 32, 0, 233, 0, 97, 0, 108, 0]
u16be = 'Eontr éal'.encode('utf_16be')
list(u16be)
[0, 69, 0, 111, 0, 110, 0, 116, 0, 114, 0, 32, 0, 233, 0, 97, 0, 108]

如果有BOM,UTF-16编解码器会将其过滤掉,然后提供没有前导ZERO WIDTH NO-BREAK SPACE字符的真正文本。
跟据标准,如果文件使用UTF-16编码,而且没有BOM,那么应该假定它使用的是UTF-16BE(大字节序)编码。然后Intel x86架构用的是小字节序,
因此有很多文件用的是不带BOM的小字节序UTF-16编码

UTF-8的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要BOM。尽管如此,某些Windows应用依然会在UTF-8编码的文件中添加BOM;而且,Excel会根据有没有BOM确定文件是不是UTF-8编码,否则,它假设内容使用Windows代码页(codepage)编码。
UTF-8编码的U+FEFF字符是一个三字节序列:b'\xfe\xbb\xbf'。因此如果文件以这三个字节开头,有可能是带BOM的UTF-8文件。然后,Python
不会因为文件以'b\xfe\xbb\xbf'开头就自动假定它是UTF-8编码

posted @ 2021-10-27 10:35  里列昂遗失的记事本  阅读(121)  评论(0编辑  收藏  举报