Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)

Posted 董哥的黑板报

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)相关的知识,希望对你有一定的参考价值。

一、字符集和字符编码的区别和联系
  • 两者的概念与区别:
    • 字符集:多个字符的集合。例如 GB2312 是中国国家标准的简体中文字符集,GB2312 收录简化汉字(6763 个)及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母,共 7445 个图形字符
    • 字符编码:把字符集中的字符编码为(映射)指定集合中的某一对象(例如:比特模式、自然数序列、电脉冲),以便文本在计算机中存储和通过通信网络的传递
  • 两者的关系:
    • 字符集是书写系统字母与符号的集合
    • 而字符编码则是将字符映射为一特定的字节或字节序列,是一种规则
  • 通常特定的字符集采用特定的编码方式:
    • 即一种字符集对应一种字符编码,例如ASCII、ios-8859-1、GB2312、GBK等,它们都采用一种字符编码按时限方式,因此它们既可以叫做是字符集也可以叫做是字符编码
    • 但 Unicode 不是,它采用现代的模型,Unicode字符集有多种字符编码方式,例如Unicode字符集有UTF-8字符编码方式、UTF-16字符编码方式
二、字符集编码的发展
  • 发展历史为:单字节 ===> 双字节 ===> 多字节

①单字节

  • ASCII(American Standard Code for Information Interchange),128 个字符,用 7 位二进制表示(00000000-01111111即 0x00-0x7F)
  • EASCII(Extended ASCII),256 个字符,用8位二进制表示(00000000-11111111 即 0x00-0xFF)
  • 当计算机传到了欧洲,国际标准化组织在 ASCII 的基础上进行了扩展,形成了 ISO-8859标准,跟 EASCII 类似,兼容 ASCII,在高 128 个码位上有所区别。但是由于欧洲的语言环境十分复杂,所以根据各地区的语言又形成了很多子标准,ISO-8859-1、ISO-8859-2、ISO-8859-3、……、ISO-8859-16

②双字节

  • 当计算机传到了亚洲,256 个码位就不够用了。于是乎继续扩大二维表,单字节改双字节,16 位二进制数,65536 个码位。在不同国家和地区又出现了很多编码,大陆的 GB2312、港台的 BIG5、日本的 Shift JIS 等等
  • 注意 65536 个码位这种说法只是理想情况,由于双字节编码可以是变长的,也就是说同一个编码里面有些字符是单字节表示,有些字符是双字节表示。这样做的好处是,一方面可以兼容 ASCII,另一方面可以节省存储容量,代价就是会损失一部分码位
  • GBK(Chinese Internal Code Specification 汉字内码扩展规范):
    • 是GB2312 的扩展(gbk 编码能够用来同时表示繁体字和简体字),按理说都属于双字节编码,码位是一样的,根本谈不上扩展,但实际上是预留空间在起作用
    • 比如下图为 GBK 的编码空间,GBK/1、GBK/2 是 GB2312 的区域,GBK/3、GBK/4、GBK/5 是 GBK 的区域,红色是用户自定义区域,白色可能就是由于变长编码损失的区域了
    • 支持国际标准 ISO/IEC10646-1 和国家标准 GB13000-1 中的全部中日韩汉字。 GBK 字符集中所有中文字符和全角符号占 2 个字节,字母和半角符号占一个字节。 没有特殊的编码方式,习惯称呼 GBK 编码。一般在国内,汉字较多时使用

  • UNICODE字符集国际标准字符集:
    • 当互联网席卷了全球,地域限制被打破了,不同国家和地区的计算机在交换数据的过程中,就会出现乱码的问题,即对同一组二进制数据,不同的编码会解析出不同的字符
    • 它将世界各种语言的每个字符定义一个唯一的编码, 以满足跨语言、跨平台的文本信息转换
    • 有多个编码方式,分别是UTF-8,UTF-16,UTF-32编码
    • 范例:“汉字”对应的 UNICODE 数字是 0x6c49 和 0x5b57,而编码的程序数据是:
      • UTF8 编码:       E6B189       E5AD97
      • UTF16BE 编码:6C49            5B57
      • UTF32BE 编码:00006C490  0005B57

③多字节

  • Unicode字符集可以使用的编码有三种:
    • UFT-8:一种变长的编码方案,使用 1~6 个字节来存储
    • UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用4个字节来存储
    • UTF-16:介于 UTF-8 和 UTF-32 之间,使用2个或者4个字节来存储,长度既固定又可变
  • UTF是Unicode Transformation Format的缩写,意思是“Unicode 转换格式”,后面的数 表明至少使用多少个比特位(Bit)来存储字符
三、Unicode字符集
  • UTF是Unicode Transformation Format的缩写,意思是“Unicode 转换格式”,后面的数表明至少使用多少个比特位(Bit)来存储字符
  • Unicode字符集可以使用的编码有三种:
    • UFT-8:一种变长的编码方案,使用 1~6 个字节来存储
    • UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用4个字节来存储
    • UTF-16:介于 UTF-8 和 UTF-32 之间,使用2个或者4个字节来存储,长度既固定又可变
  • Unicode是字符集,utf-8、utf-16、utf-32才是真正的编码方式

UTF-8

  • UTF-8:是一种变长字符编码,被定义为将代码点编码为 1 至 4 个字节,具体取决于代码点数值中有效位的数量
  • 注意:UTF-8 不是编码规范,而是编码方式
  • 下表为 Unicode 值对应的 utf8 需要的字节数量:
    • 前面红色的数字属于编码前缀,Unicode就是用这些前缀来判断你的字符类型属于什么编码方式(utf-8、utf-16、utf-32),例如对一堆二进制解析的时候就需要用前缀来判断

演示案例①

  • 例如,汉字“董”的Unicode编码(16进制)为8463,对应上图的unicode编码(16进制)属于第3种类型(位于000800-00FFFF之间)

  • 8463转换为二进制就是“1000 0100 0110 0011”,通过上面的编码表可以看出“董”对应的UTF-8编码前缀为“1110xxxx 10xxxxxx 10xxxxxx”,因此将编码前缀加到二进制前面,“董”的utf-8编码为“11101000 10010001 10100011”,因此“董”字在utf8编码下占用3字节

  • 然后再通过下图我们知道UTF8编码的十六进制为“E891A3”,转换为二进制就是“11101000 10010001 10100011”

演示案例②

  • 例如,字符“a”的Unicode编码(16进制)为61,对应上图的unicode编码(16进制)属于第1种类型(位于000000-00007F之间)

  • 61转换为二进制就是“0110 0001”,通过上面的编码表可以看出“a”对应的UTF-8编码前缀为“0xxxxxxx”,因此将编码前缀加到二进制前面,“a”的utf-8编码为“0 01100001,前面的0可以省略,因此就是“01100001”。因此“a”字在utf8编码下占用1字节

  • 然后再通过下图我们知道UTF8编码的十六进制为“64”,转换为二进制就是“01100001

UTF-16

  • UFT-16 比较奇葩,它使用 2 个或者 4 个字节来存储
  • 对于 Unicode 编号范围在 0~FFFF 之间的字符,UTF-16 使用两个字节存储,并且 直接存储 Unicode 编号,不用进行编码转换,这跟 UTF-32 非常类似。
  • 对于 Unicode 编号范围在 10000~10FFFF 之间的字符,UTF-16 使用四个字节存储, 具体来说就是:将字符编号的所有比特位分成两部分,较高的一些比特位用一个值 介于 D800~DBFF 之间的双字节存储,较低的一些比特位(剩下的比特位)用一个值介于 DC00~DFFF 之间的双字节存储

UTF-32

  • UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 编号即可,不需要任何编码转换
  • 浪费了空间,提高了效率
四、UTF BOM问题
  • BOM(Byte Order Mark)字节序(字节顺序的标识),其实就是用大端(BE)还是小端(LE)
  • 比如 UTF-16BE 和 UTF-16LE:
    • UTF-16BE,其后缀是 BE 即 big-endian,大端的意思。大端就是将高位的字节放在低地址表示
    • UTF-16LE,其后缀是 LE 即 little-endian,小端的意思。小端就是将高位的字节放 在高地址表示
    • UTF-16,没有指定后缀,即不知道其是大小端,所以其开始的两个字节表示该字节数组是大端还是小端。即FE FF表示大端,FF FE表示小端
  • UTF 在文件中的存储。UTF 格式在文件中总有固定文件头:

  • 当打开一个文件时,判断其属于什么编码类型,只要判断其开头处的标志就可以了:
    • EF BB BF 表示 UTF-8
    • FE FF 表示 UTF-16BE
    • FF FE 表示 UTF-16LE
    • 00 00 FE FF 表示 UTF32-BE
    • FF FE 00 00 表示 UTF32-LE
  • 如果一个Unicode编码类型的文件缺少了这些标志位,那么文件打开之后就会出现乱码(见下面演示案例②)
  • 注意:只有 UTF-8 兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,因为它们没有单字节编码
  • UTF-8缺省不带BOM:
    • UTF-8 中有一字节的情况,这种情况,就没有两端的说法了。至于另外的二,三,四字节情况,以三字节为例,如果你一定要弄出端法,也不是说不可以,比如,小端法就是“小-中-大”,大端法就是“大-中-小”
    • 但现实情况是 UTF-8 仅仅采用了一种端法,就是大端法

演示案例①(验证UTF-16)

  • 例如,“汉”字在文件中的存储就如下所示

  • 例如分别采用UTF-16、UTF-16BE、UTF-16LE存储“汉”这个中文字符

  • 然后我们使用二进制编辑器查看这三个文件对应的二进制内容,可以看出:
    • UTF-16:因为没有指定后缀,此处通过查看前两个字节,可以看出其是小端(LE)存储的,并且“汉”被存储为“49 6C”
    • UTF-16LE:小端存储,“汉”被存储为“49 6C”
    • UTF-16BE:大端存储,“汉”被存储为“6C 49”

演示案例②(乱码演示)

  • 我们在上面说过,如果一个Unicode编码类型的文件缺少了这些标志位,那么文件打开之后就会出现乱码
  • 接着上面的演示案例①,我们以UTF-16BE为例,其正常情况如下所示,文件以FE FF标志位开头

  • 现在我们使用二进制编辑器,将前面的FE FF标志位删去(随意删除),例如我们将FE删除

  • 现在再打开查看,发现已经出现了乱码

演示案例③(UTF-8缺省不带BOM)

  • 上面说过了UTF-8缺省不带BOM
  • 现在我们有一个UTF-8格式的文件,其存储的内容为“汉”

  • 通过二进制查看,可以看到其以“EF BB BF”开头

五、相关命令

file命令

  • file文件可以查看文件的类型
  • 演示案例和详细内容可以参阅:javascript:void(0)

iconv命令

六、字符集应用案例

mysql

  • 部分汉字在mysql使用utf8字符时写入出现异常,或者读取出现异常。比如????,其和熙存在区别。????在utf8模式下需要4个字节表示

  • MySQL中的UTF-8:
    • MySQL的“utf8 ”实际上不是真正的 UTF-8, “utf8”只支持每个字符最多3个字节。真正的utf8至少要支持4个字节
    • MySQL一直没有修复这个bug,他们在2010年发布了一个叫作"utf8mb4"的字符集,MySQL的"utf8mb4"是真正的"UTF-8"

演示案例

  • 下面我们调用MySQL的C API操作MySQL,在建表的时候其表的编码格式指定为utf8而不是utf8mb4,因此其只支持每个字符最多3个字节,然后向表中插入数据。并且调用mysql_set_character_set()函数将当前用户的默认字符集设置为utf8
  • 在插入数据的时候(见下面的INSERT_SAMPLE_TABLE宏),其插入的第二个熙字占用4字节,因此下面程序的结果应该会出错
#include <stdio.h>
#include <stdlib.h>
#include <mysql.h>

#define DROP_SAMPLE_TABLE "DROP TABLE IF EXISTS utf8_tbl"
#define CREATE_SAMPLE_TABLE "CREATE TABLE utf8_tbl ( \\
                                utf8_id INT UNSIGNED AUTO_INCREMENT, \\
                                utf8_title VARCHAR(100) NOT NULL,    \\
                                PRIMARY KEY (utf8_id)            \\
                            )ENGINE=InnoDB DEFAULT CHARSET=utf8"
#define INSERT_SAMPLE_TABLE "INSERT INTO utf8_tbl(utf8_title) VALUES(\'????熙\')"
#define SELECT_SAMPLE_TABLE "SELECT * FROM utf8_tbl"
#define DBNAME "unicode"
#define CHARSET "utf8"

int main()
{
    MYSQL *mysql; //MySQL连接句柄
    

    //初始化连接句柄
    mysql = mysql_init(NULL);
    if(mysql == NULL)
    {
        fprintf(stderr, "mysql_init failed, code: %d, reason: %s\\n", mysql_errno(mysql), mysql_error(mysql));
        return -1;
    }


    //连接MySQL服务端
    if(mysql_real_connect(mysql, "localhost", "root", "123456", DBNAME, 0, NULL, 0) == NULL)
    {
        fprintf(stderr, "mysql_connect failed, code: %d, reason: %s\\n", mysql_errno(mysql), mysql_error(mysql));
        mysql_close(mysql);
        return -1;
    }
    printf("Connection success\\n\\n");


    //设置当前连接的默认字符集(此处设置为utf8)
    if(mysql_set_character_set(mysql, CHARSET) != 0)
    {
        fprintf(stderr, "mysql_set_character_set failed, code: %d, reason: %s\\n", mysql_errno(mysql), mysql_error(mysql));
        mysql_close(mysql);
        return -1;
    }

    
    //删除表
    if(mysql_query(mysql, DROP_SAMPLE_TABLE) != 0)
    {
        fprintf(stderr, "drop table failed, code: %d, reason: %s\\n", mysql_errno(mysql), mysql_error(mysql));
        mysql_close(mysql);
        return -1;
    }
    printf("drop table success\\n\\n");


    //创建表
    if(mysql_query(mysql, CREATE_SAMPLE_TABLE) != 0)
    {
        fprintf(stderr, "create table failed, code: %d, reason: %s\\n", mysql_errno(mysql), mysql_error(mysql));
        mysql_close(mysql);
        return -1;
    }
    printf("create table success\\n\\n");


    //向表中插入数据
    if(mysql_query(mysql, INSERT_SAMPLE_TABLE) != 0)
    {
        fprintf(stderr, "insert failed, code: %d, reason: %s\\n", mysql_errno(mysql), mysql_error(mysql));
        mysql_close(mysql);
        return -1;
    }
    printf("insert success, insert %lu rows\\n\\n", (unsigned long)mysql_affected_rows(mysql));


    //查询数据(此处调用mysql_store_result一次性读取所有数据,然后再调用mysql_fetch_row逐行获取数据)
    if(mysql_query(mysql, SELECT_SAMPLE_TABLE) != 0)
    {
        fprintf(stderr, "select failed, code: %d, reason: %s\\n", mysql_errno(mysql), mysql_error(mysql));
        mysql_close(mysql);
        return -1;
    }
    else
    {
        //查询数据,将所有的结果保存到结果集中
        MYSQL_RES *res;
        res = mysql_store_result(mysql);
        if(res == NULL)
        {
            fprintf(stderr, "mysql_store_result failed, code: %d, reason: %s\\n", mysql_errno(mysql), mysql_error(mysql));
            mysql_close(mysql);
            return -1;
        }

        printf("select %lu rows\\n", (unsigned long)mysql_num_rows(res));
        //逐行获取数据
        MYSQL_ROW sqlrow;
        int columnNum = mysql_num_fields(res);
        while((sqlrow = mysql_fetch_row(res)) != NULL)
        {
            //逐列获取数据
            for(int i = 0; i < columnNum; ++i)
            {
                printf("%s\\t", sqlrow[i]);
            }
            printf("\\n");
        }

        //查询完成之后释放结果集
        mysql_free_result(res);
    }

    mysql_close(mysql);

    return 0;
}
  • 下面是效果,在插入数据的时候出错了,因为在插入数据的时候第二个熙字其占用4个字节,超过了MySQL中utf8编码的最大值,因此会出错

  • 现在我们修改程序,将表的格式设置为utf8mb4,当前用户的编码设置也设置为utf8mb4,然后再重新编译运行程序,那么就执行成功了

  • 修改之后再次操作,可以看到成功了

nginx

  • 如果在Nginx中使用中文可能会出现错误(例如页面中有中文)
  • 可以在Nginx配置文件中加入下面的选项来设置UTF-8配置
  • 例如下面是一个Nginx配置文件
server {
    listen  8888;

    # 设置为utf-8格式
    charset utf-8;

    server_name 192.168.221.141;
    location / {
        root /mnt/hgfs/ubuntu/vip/20191017-unicode/src/nginx;
        index index.php index.html index.htm;
    }
}

Redis

  • 默认情况下,在Redis中使用中文会出现乱码

  • 可以配置--raw选项来解决乱码问题,例如下面在连接时指定--raw
redis-cli --raw

以上是关于Linux(程序设计):26---字符集与字符编码概述(附Unicode字符集实现原理)的主要内容,如果未能解决你的问题,请参考以下文章

字符编码

Linux shell 命令 转换文件的字符编码

5.1 字符串

Linux(程序设计):27---iconv库(转换字符编码)

学号 2019-2020-2314《数据结构与面向对象程序设计》哈夫曼编码实践

javascript中的JSON字符编码与java不同