用Python理清编码问题:BOM、换行符和编码原则

BOM

自标记文件编码,UTF编码系列采用的是BOM技术,就是使用一个特殊字符(zero width no-break space),一个Unicode没用到的码位,其值为0XFEFF,放置在文件开头,当编辑器读的时候,看到这个BOM就知道文件采用的什么编码。

es = 'A'
codes = ['utf-32','utf-16']
print([es.encode(code) for code in codes])

其输出[b'\xff\xfe\x00\x00A\x00\x00\x00', b'\xff\xfeA\x00'],字节序列开头的\xff\xfe就是BOM,有时候他可能是\xfe\xff的形式,这说明文本是大字节序。0xFEFF的UTF-8编码是0x\ef0x\bb0x\bf。

当我们使用UTF-16LE或者BE编码时,其输出不带BOM头。这是因为程序认为我们既然明确指定了带大小端的编码,说明我们知道并掌握自己怎么使用字节序列。

Windows记事本中的BOM

BOM技术在Windows的记事本中比较常见,新建一个文本文件,写入相同内容,然后另存为记事本支持的四种格式:ANSI、Unicode、Unicode big endian、utf-8,查看其二进制形式,可见其BOM字段和相应编码的字节序列。Python打开文件时的'b'选项,使用二进制模式读取。

# 文件内容都是:abAB巩★☆,但另存为时选择的编码格式不同。

fs = ['ansi.txt','unicode.txt','unicode big endian.txt','utf-8.txt']

for f in fs:
    with open(f,'rb') as f_:
        print(f_.readline())

输出是:

b'abAB\xb9\xae\xa1\xef\xa1\xee'
b'\xff\xfea\x00b\x00A\x00B\x00\xe9\x5d\x05\x26\x06\x26'
b'\xfe\xff\x00a\x00b\x00A\x00B\x5d\xe9\x26\x05\x26\x06'
b'\xef\xbb\xbfabAB\xe5\xb7\xa9\xe2\x98\x85\xe2\x98\x86'

从字符巩可以看出

  • ANSI实际采用的是GB系列编码。

  • Unicode.txt和unicode big endian.txt存储编码是UTF-16。更具体的,根据BOM信息,可知unicode.txt实际格式是UTF-16LE,而unicode big endian.txt实际格式是UTF-16BE。

  • UTF-8是带有BOM头的UTF-8存储格式。

其它编辑器的BOM

记事本使用BOM作文本编码信息的自标记,但是这并不是对编辑器的强制要求,也就是可带可不带,比如Linux下VIM编辑器,就不带BOM信息,这也是跨操作系统、编辑器编辑时,经常遇到问题的地方。

vim设置中,查询BOM:set bomb?,取消BOM::set nobomb,设置BOM:set bomb

上文中的ANSI为啥是GB系列编码呢?

Windows的ANSI和代码页

至于ANSI格式为什么用GB系编码形式,需要多说一句。ANSI是美国国家标准协会,各国(非拉丁语系国家)指定自身文字编码,得到ANSI的认可之后使用。所以各个国家的ANSI实际指代编码各不相同,比如中国是GB2312,日本JIT,台湾地区Big5。ANSI实际是指向具体编码的“指针”。

相较于,Windows记事本用ANSI来指代本地区的编码;Windows系统用代码页指代本系统编码,但这两个概念可视为一个概念。代码页或ANSI从命令行窗口输入chcp可见。比如简体中文的操作系统会返回活动代码页;936,这就是CP936,也就是GBK编码规范。代码页也是指向具体编码的“指针”。

在记事本选择ANSI格式,实际是选择当前系统的活动代码页指代的具体编码,本例中选记事本ANSI,就是选代码页936,代码页CP936就指向GBK编码。

在Unix系统和Windows系统间切换时,经常出现文本出现^M和所有行挤成一行的问题。

换行符问题

Window下,UE编辑器或者vim的十六进制打开文件时,可看到文件的最后有0x0d0a这两个字节。这分别是Windows中的换行(0x0a或\n)和回车(0x0d或\r)的ASCII码。

VIM编辑器使用命令:% ! xxd转换位十六进制显示;使用命令:% ! xxd -r转换为文本显示。

这要从机械打字机说起,机械打字机打印完一整行之后,需要手动将“车”拉回到左侧,在回车的过程中,打字机自动完成了换行的操作,就可以继续打印第二行。

  • Windows系统借鉴这两个字符,使用回车和换行表示一行完成,另起一行。

  • Unix系统仅仅使用了换行符。

    • Unix系统打开Windows系统的文件时,由于多了回车符号,常看到行末尾出现奇怪的^M符号,这是Unix解析\r产生的。

    • Windows系统里打开Unix系统文件时,由于少了回车符,所有行挤在一起,变成一行。

  • Mac系统使用回车\r表示换行。

Python编程中编码问题

以上,我们理清了字符编码的发展脉络,知道了BOM以及不同操作系统、编辑器对BOM的应用。在Python编程中,我们如何处理编码问题呢?有以下几个注意事项:

建议使用UTF-8

为了国际交流的便捷,Python使用UTF-8作为默认编码格式,在日常网页编程,数据库默认编码,以及文本存储时都建议使用UTF-8。

decode和encode用法

什么时候encode,什么时候decode呢?需要记住一个原则:Python3字符串是Unicode码位,也就是说所有Python字面量实际是Unicode码位整数。所有转换都要以Unicode码位为中间体:

  • Unicode码位转字节序列时用encode

  • 字节序列转为Unicode时用decode

str = 'abAB巩'    #Unicode码位
gbk_seq = str.encode('gbk') #Unicode码位转GBK字节序列
str2 = gbk_seq.decode('gbk') #GBK字节序列转Unicode码位
utf_seq= str2.encode('utf-8') #Unicode码位转UTF-8字节序列
print(str,str2,gbk_seq,utf_seq)

Python2字面量实际是带有编码格式的字节序列。

三明治原则

三明治原则是在读写文件时,一定显式指定文件编码,确保Python程序中只处理Unicode码位,而不涉及字节序列。只有在输入和输出时,文件才转换为相应编码下的字节流。这种两端有具体编码,内里只有Unicode的处理方式,像两片面包夹着食材,所以称为三明治原则。

with open('ansi.txt','r',encoding='gbk')as f,open('u8.txt','w',encoding='utf-8') as f2:
    s = f.readline()
    s = s[::-1]
    f2.write(s)

以上代码,s字符串相关的处理都是Unicode码位。只有输入和输出时,才指定了相应的编码GBK和UTF-8。

不要使用python2

不要使用python2!Python2中的Str类型,既是字符串,又是字节序列类型,特别复杂。非有特殊要求,建议不要使用。以下内容,足以一窥Python2的复杂:

Python2中,s = 'abAB巩’,是一个字节序列,它的具体值根据采用的编码(GBK,UTF-8)而不同。u = u'abAB巩'表示的是Unicode码位。另外Python2中的编码类型声明# -*- coding:utf-8声明的是程序中常量的编码类型,需要和文本保存编码类型一致。

使用chardet推测字节序列的编码

若无法确定字节流的编码,模块chardet可用来推测字节流的编码。

import chardet
chardet.detect(b'\xef\xbb\xbfabAB\xe5\xb7\xa9\xe2\x98\x85\xe2\x98\x86')

返回是一个字典{'encoding':'utf-8',...},指出了最可能的编码及其可信度。

总结

  • 为了自标记文件编码,引入了BOM标签。我们分析了BOM在记事本中的表现,了解了BOM并不是强制要求。

  • 了解了BOM、换行符导致的Linux和Windows之间的许多问题。

  • 学习了Python中编程的三明治原则;读写文件时,显式指明文件编码。

本系列总结

  • 从ASCII的简单,但不支持其他国家文字。

  • 到扩展ASCII支持其他国文字,但产生乱码。

  • 再到MBCS多字节编码可以支持汉语这种很多字符的语言,但由于太多语言编码导致各自为战互不兼容。

  • Unicode统一了世界各语言字符。Unicode几种编码形式中;

    • UTF-32简单,但浪费严重。

    • UTF-16使用两个字节为单位存储,节省了空间。

    • UTF-8使用一个字节直接存储,是效率、空间的平衡。

  • 为了自标记文件编码,引入了BOM标签。我们分析了BOM在记事本中的表现,了解了BOM并不是强制要求。

  • 了解了BOM、换行符导致的Linux和Windows之间的许多问题。

  • 学习了Python中编程的三明治原则;读写文件时,显式指明文件编码。

人类交流因语言文字不同,造成很大困扰,无数IT从业人员也遇到各种乱码折磨,不认真攻克编码问题,他就会时不时跳出来。需要我们认清编码是发展和优化平衡的结果,包含了一系列优化、例外和兼容。

本文代码

https://gitee.com/gongqingkui/codes/0qku9tc6az3j2dvoy5s8w39

作者:巩庆奎,大奎,对计算机、电子信息工程感兴趣。gongqingkui at 126.com

赞 赏 作 者

更多阅读

2020 年最佳流行 Python 库 Top 10

2020 Python中文社区热门文章 Top 10

5分钟快速掌握 Python 定时任务框架

特别推荐


点击下方阅读原文加入社区会员

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值