# 字节编码

# 简介

# 二进制编码

  • 人类使用的各种字符,在计算机底层都以二进制格式存储。因此,计算机经常需要将一个 char 字符,转换成 bits 二进制格式,该过程称为二进制编码(Binary Encoding)。
    • 人类发明了很多种编码规则,又称为编码格式。例如字母 'A' ,按照 ASCII 编码格式,会转换成二进制值 0100 0001
    • 编码的逆过程,称为解码(decode)。例如二进制值 0100 0001 ,按照 ASCII 编码格式,会转换成字母 'A' 。

# 字节编码

  • 二进制编码,是以 bit 为单位存储数据,但 bits 不方便人类阅读、记忆。为了解决这一问题,通常将以 bit 为单位的二进制值,转换成以 byte 为单位的十六进制值。

    • 例如二进制值 0100 0001 ,转换成十六进制值是 4 1 ,体积为 1 字节。
    • 此时依然属于二进制编码,只是以字节为单位展示,称为字节编码。
  • 区分这些概念:

    • 字节:在计算机中,bits 二进制值通常会被分组管理。每连续 8 个比特(bit)分为一组,称为 1 个字节(byte)。
    • 字符:在计算机中,指一个自然语言的文字符号。例如英文字母 'A' ,在 C 语言中用 char 数据类型的变量存储,占用 1 字节的存储空间。
    • 字符串:在计算机中,指连续多个字符。例如 "Hello World!" 。
  • 计算机程序将字符串写入文件中存储时,通常要转换成二进制编码。例如:

    1. 程序将一个字符串 "Hello World!" ,按照 ASCII 格式编码,转换成二进制数据,写入文件。
    2. 程序从文件读取二进制数据,按 ASCII 格式解码,转换成一个字符串 "Hello World!" 。
      • 如果解码格式与编码格式不一致,得到的字符串就可能与原字符串不一致,比如出现乱码。
  • 人类发明了很多种编码格式,可以大概分类为:

    • 单字节编码:每个字符的编码值,体积为 1 字节。
    • 多字节编码:每个字符的编码值,体积超过 1 字节,又称为宽字符。

# 字节序

  • 如果一个数据类型的长度超过 1 个字节,则需要考虑哪个字节先存储(Byte Order)。有两种方案:
    • 大端序(Big-Endian):高位字节先存储。例如 0x11223344 存储为 0x11 0x22 0x33 0x44 ,最高位的字节 0x11 存储在地址的最低位。
    • 小端序(Little-Endian) :低位字节先存储。例如 0x11223344 存储为 0x44 0x33 0x22 0x11 ,最高位的字节 0x11 存储在地址的最高位。
  • 如何判断一段文本的字节序?
    • 可以为每种字节序定义一种编码格式。例如将 UTF-16 编码格式细分为 UTF-16be 和 UTF-16le 。
    • 可以在一个文本文件的开头加上几个字节作为标记,用于声明字节序,称为 BOM(Byte Order Mark)。

# 单字节编码

# ASCII

  • :美国信息交换标准代码(American Standard Code for Information Interchange),是一种单字节编码格式。
  • 编码范围为 0x00 ~ 0x7F ,总共定义了 128 个字符。收录了拉丁字母、一些控制字符。
  • ASCII 又简单又常用,很多人甚至会背诵一些字符的 ASCII 编码值。

# ISO-8859-1

  • :又称为 Latin-1 ,是一种单字节编码格式,编码范围为 0x00 ~ 0xFF
  • 是 ASCII 码的超集,收录了拉丁字母、希腊语、阿拉伯语等字符。
    >>> 'hello'.encode('utf-8')
    b'hello'
    >>> _.decode('ISO-8859-1')
    'hello'
    
  • 定义了单字节的所有编码值。因此,任意一段 bytes 数据,不管它采用什么编码格式,即使是一段随机数据,都可以按照 ISO-8859-1 格式解码成 str 对象,虽然得到的 str 字符串不一定有意义。
    >>> bytes = '你'.encode('gbk')
    >>> bytes.decode('gbk')   # 使用正确的格式来解码
    '你'
    >>> bytes.decode('ISO-8859-1')  # 使用错误的格式来解码,但不会抛出异常
    'Äã'
    >>> bytes.decode('utf-8')  # 使用错误的格式来解码,并且会抛出异常
    UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc4 in position 0: invalid continuation byte
    

# Windows 1252

  • :又称为 cp1252 、ANSI 字符集,是 ISO-8859-1 的超集。

# 多字节编码

# Unicode

  • :一种字符集,又称为万国码、国际码、统一码,收录了世界上很多种语言的字符,给每个字符分配了唯一编码。
  • 编码范围为 U+000000 ~ U+10FFFF ,总共大约有 1.1 百万个码位(Code Point),可用于映射字符。
    • 开头两个字节从 U+00 到 U+10 划分为 17 个平面(plane),每个平面包含 2^16=65536 个码位。
    • 第一个平面称为第零平面、基本多语言平面(Basic Multilingual Plane, BMP),其它平面称为辅助平面。
    • 第零平面包含了最常用的字符,其中 D800 ~ DFFF 范围内的码位保留,不映射 Unicode 字符,因此通常被 UTF 编码使用。
  • 每个字符编码为 6 个十六进制数,加上 U+\u 作为前缀。
    • 只考虑第零平面时,可以只使用 4 个十六进制数,比如字母 A 表示成 U+0041 。

# UTF

  • :Unicode 转换格式(Unicode Transformation Format)
  • Unicode 字符的编码较长,大部分码位都用不到,浪费存储空间。因此实际使用时,通常通过 UTF 转换成更短的编码格式。

# UTF-8

  • :一种编码格式,兼容 ASCII 码。
  • 字符的编码长度可变,为 1 ~ 4 个字节。比如存储 ASCII 码字符时只用 1 个字节,存储中文字符时使用 3 个字节。
  • UTF-8 的编码长度可变,因此不存在多种字节序。
    • 按照 UTF-8 规范,处理 UTF-8 编码的文本时,不需要考虑字节序。
    • 不过,Microsoft 公司的 Office 等软件习惯了在处理 UTF-8 编码的文本时,加上 0xEF 0xBB 0xBF 三个字节作为 BOM 标记,因此与标准的 UTF-8 格式不同,这会导致一些问题:
      • 例如,用 VS Code 软件编写一个 UTF-8 编码、包含中文字符的 csv 表格文件之后,用 Excel 软件打开该文件,显示的字符会乱码。此时可用 Notepad++ 软件将该文件转换成 UTF-8-BOM 编码格式。或者用 iconv 1.csv -f utf-8 -t gbk > 2.csv 命令将该文件转换成 gbk 编码格式,则不需要声明字节序。
      • 反之,用 Excel 导出一个包含中文字符的 csv 表格文件之后,用 VS Code 打开该文件,显示的字符会乱码。此时可用 Notepad++ 软件将该文件转换成 UTF-8 编码格式。

# UTF-16

  • :一种编码格式,不兼容 ASCII 码。
  • 大部分字符的编码长度是 2 字节,少部分不常用的字符则是 4 字节。
  • 根据字节序的不同,细分为两种编码格式:
    • UTF-16be :大端序,BOM 标记为 0xFE 0xFF
    • UTF-16le :小端序,BOM 标记为 0xFF 0xFE
  • 例:
    >>> '12'.encode('utf-16')   # 编码格式名称中没有声明字节序,Python 默认会按小端序处理
    b'\xff\xfe1\x002\x00'       # 编码之后,会在开头加上 BOM 标记,末尾加上空字符 \x00 表示结束
    >>> 'A'.encode('utf-16')
    b'\xff\xfeA\x00'
    >>> ''.encode('utf-16be')   # 如果编码格式名称中声明了字节序,则不必再加上 BOM 标记
    b''
    >>> 'A'.encode('utf-16le')
    b'A\x00'
    

# UTF-32

  • :一种编码格式,不兼容 ASCII 码。
  • 每个字符的编码长度都是 4 字节。
  • 根据字节序的不同,细分为两种编码格式:
    • UTF-32be :大端序,BOM 标记为 0x00 0x00 0xFE 0xFF
    • UTF-32le :小端序,BOM 标记为 0xFF 0xFE 0x00 0x00

# UCS

  • UCS

    • :通用编码字符集(Universal Coded Character Set),由 ISO 10646 标准定义,相当于 Unicode 字符集的子集。
  • UCS-2

    • :一种编码格式,是 UTF-16 的子集。
    • 只对 Unicode 第零平面的字符进行编码。
    • 每个字符的编码长度都是 2 字节。
    • 根据字节序的不同,细分为两种编码格式:
      • UCS-2be
      • UCS-2le
  • UCS-4

    • :一种编码格式,是 UTF-32 的超集。
    • 每个字符的编码长度都是 4 字节。

# 中文编码

# GB2312

  • :一种简体中文的编码格式,于 1980 年发布,已经过时。
  • 在 Windows 中别名为 CP936 。
  • 收录了 6k 多个汉字、拉丁字母等。

# BIG5

  • :一种繁体中文的编码格式,又称为大五码,于 1983 年发布。
  • 收录了 1w 多汉字、拉丁字母等。

# GBK

  • :一种中文编码格式,于 1995 年发布。
  • 向下兼容 GB2312 ,向上支持 ISO 10646 标准,也收录了 BIG5 中的繁体汉字。总共收录了 2w 多个汉字。
  • 英文字符的编码长度为 1 字节,中文字符的编码长度为 2 字节。

# GB18030

  • :一种中文编码格式,于 2000 年发布。
  • 是 GBK 的超集。总共收录了 7w 多个汉字。
  • 字符的编码长度可变,包括:
    • 1 字节,向下兼容 ASCII 。
    • 2 字节,向下兼容 GBK 。
    • 4 字节

# import codecs

  • :Python 的标准库,用于管理 Python 的编解码器。
  • 它将编解码器称为 codec ,包括 encoder 和 decoder 。
  • 它为所有编解码器定义了抽象的基类:
    class Codec:
        def encode(self, input, errors='strict'):
            pass
    
        def decode(self, input, errors='strict'):
            pass
    
  • Python 解释器内置了很多种编码格式的编解码器,它们都在 codecs 模块中注册了。
    • 编码格式的名称不区分大小写。对于连字符 - ,可以替换成下划线 _ ,也可以省略。
    • 例:查询一种编码格式对应的编解码器
      >>> import codecs
      >>> codecs.lookup('utf-8')
      <codecs.CodecInfo object for encoding utf-8 at 0x151dc6a99a0>
      >>> codecs.lookup('utf_8')
      <codecs.CodecInfo object for encoding utf-8 at 0x151dcf59400>
      >>> codecs.lookup('utf8')
      <codecs.CodecInfo object for encoding utf-8 at 0x151dcf59280>
      

# import chardet

  • :Python 的第三方库,用于检测 bytes 对象采用的字节编码格式。
  • 安装:pip install chardet
  • 其原理是通过比对各种字符集的特征字符来猜测编码格式。
    • 检测的数据量太少就容易猜错。
    • 不能处理 str 对象,因为 str 已经是按 Unicode 格式编码了。
  • 例:检测一个字符串
    >>> import chardet
    >>> chardet.detect(b'Hello, world!')
    {'encoding': 'ascii', 'confidence': 1.0, 'language': ''}    # confidence 表示可信度,这里为 100%
    >>> chardet.detect('你'.encode('GBK')))
    {'encoding': None, 'confidence': 0.0, 'language': None}
    >>> chardet.detect('你好'.encode('GBK'))
    {'encoding': 'TIS-620', 'confidence': 0.3598212120361634, 'language': 'Thai'}
    >>> chardet.detect('你好,欢迎来到这里!'.encode('GBK'))      # 增加检测的字符数量时,检测结果可能改变
    {'encoding': 'GB2312', 'confidence': 0.99, 'language': 'Chinese'}
    
    • 即使 chardet 将一个字符串识别为 GB2312 格式,建议也当作 GBK 格式解码。以免 chardet 将 GBK 格式的字符串,误识别成了 GB2312 格式。
  • 例:检测一个文件
    >>> f = open(path, 'rb')
    >>> chardet.detect(f.read())['encoding']
    'GB2312'