MySQL字符集编码终极指南进阶篇

2023年 8月 18日 53.4k 0

请先阅读上篇:
MySQL字符编码指南--基础篇

1. 字符集四类设置

1.1 操作系统字符集

以下配置项是Linux系统的本地化(localization)设置,用于控制系统在不同方面如何呈现和处理数据。下面是每个配置项的解释:

[root@VM-94-230-centos ~]# locale

LANG=zh_CN.GBK: 设置系统的默认语言和字符集。在这里,zh_CN表示中国的简体中文,GBK是一种常用于简体中文的字符编码。
LC_CTYPE="zh_CN.GBK": 控制字符分类和字符串处理的规则,例如字母的大小写转换。
LC_NUMERIC="zh_CN.GBK": 控制数字的格式,例如数字的千位分隔符。
LC_TIME="zh_CN.GBK": 控制日期和时间的格式。
LC_COLLATE="zh_CN.GBK": 控制字符串排序的规则,例如在字典排序中如何比较字符。
LC_MONETARY="zh_CN.GBK": 控制货币格式,例如货币符号和货币值的格式。
LC_MESSAGES="zh_CN.GBK": 控制系统消息的本地化,例如错误消息和提示。
LC_PAPER="zh_CN.GBK": 控制默认纸张的尺寸。
LC_NAME="zh_CN.GBK": 控制名字格式,例如姓名的顺序和称呼。
LC_ADDRESS="zh_CN.GBK": 控制地址格式,例如街道、城市和邮政编码的顺序。
LC_TELEPHONE="zh_CN.GBK": 控制电话号码格式。
LC_MEASUREMENT="zh_CN.GBK": 控制度量单位,例如使用公制还是英制。
LC_IDENTIFICATION="zh_CN.GBK": 控制元数据,例如语言、地区和字符集。
LC_ALL=zh_CN.GBK: 这个变量用于覆盖所有其他LC_变量和LANG变量。在这里,它设置了所有本地化类别为中国简体中文和GBK字符集。

1.2 终端字符集

我们一般使用终端软件远程登陆服务器,在登陆的时候需要选择编码即字符集,实际上这个字符集就是设置远程服务器操作系统的环境变量LC_ALL。

1.3 MySQL字符集

MySQL的字符集设置提供了灵活的层次结构,这些层次分4层,允许你在服务器、数据库、表和列级别控制字符集和排序规则,这有助于确保数据的一致性和正确性,特别是在处理多语言和国际化环境时。

以下是各个层次的参数:

1. 服务器层次
服务器层次的字符集设置适用于MySQL服务器实例的全局设置。

- `character_set_server`: 服务器的默认字符集。
- `collation_server`: 服务器的默认排序规则。

2. 数据库层次
你可以为特定数据库设置字符集和排序规则,这将覆盖服务器层次的设置。

- `CREATE DATABASE db_name CHARACTER SET charset_name COLLATE collation_name;`: 创建数据库时指定字符集和排序规则。
- `ALTER DATABASE db_name CHARACTER SET charset_name COLLATE collation_name;`: 修改现有数据库的字符集和排序规则。

3. 表层次
你可以为特定表设置字符集和排序规则,这将覆盖数据库和服务器层次的设置。

- `CREATE TABLE tbl_name (...) CHARACTER SET charset_name COLLATE collation_name;`: 创建表时指定字符集和排序规则。
- `ALTER TABLE tbl_name CONVERT TO CHARACTER SET charset_name COLLATE collation_name;`: 修改现有表的字符集和排序规则。

4. 列层次
你可以为表中的特定列设置字符集和排序规则,这将覆盖表、数据库和服务器层次的设置。

- 在`CREATE TABLE`或`ALTER TABLE`语句中,为特定列指定`CHARACTER SET charset_name`和`COLLATE collation_name`。

1.4 客户端字符集

客户端连接到MySQL服务器时也可以设置字符集。

- `character_set_client`: 控制客户端字符集。
- `character_set_connection`: 控制连接层字符集。
- `character_set_results`: 控制查询结果字符集。

这3个字符集的具体功能见下图:

MySQL命令:set names latin1 相当于设置session级别的character_set_client, character_set_connection, 和character_set_results 为latin1字符集。

1.5 简单示例

我们用python来显示下"数据库"在不同字符集下的16进制编码:

text = "数据库"
gbk_encoded = text.encode('GBK')
gbk_hex = gbk_encoded.hex()
print("GBK:"+gbk_hex)

utf8_encoded = text.encode('UTF8')
utf8_hex = utf8_encoded.hex()
print("UTF8:"+utf8_hex)

 运行结果如下:

GBK:cafdbeddbfe2
UTF8:e695b0e68daee5ba93

 可以看到因为GBK的汉字是双字节,所以一共6个字节。UTF8汉字是3字节,所以一共是9字节。

我们进行一个简单的测试,从1个网页上复制汉字"数据库"到linux的文本文件中,这个网页的原始编码是什么呢?查看网页源文件,可以看出是UTF8编码:

然后用vim将汉字粘贴到文本文件utf8中,查看文本内容和16进制编码: 

[root@VM-94-230-centos /data/test/char]# cat utf8.txt
数据库
[root@VM-94-230-centos /data/test/char]# hexdump utf8.txt
0000000 95e6 e6b0 ae8d bae5 0a93
000000a

在MySQL里查询编码:

GBK:
select hex('数据库');
CAFDBEDDBFE2

UTF8:
select hex('数据库');
E695B0E68DAEE5BA93

2.  字符集转换

2.1 通过内码转换

GBK到UTF8的转换涉及解码原始字节序列到内码(Unicode),然后重新编码为目标字符集。这个过程依赖于源和目标字符集的精确定义,以及用于执行转换的工具和库:

1. 解码(Decoding): 首先,需要将GBK编码的字节序列解码为内码。在这个过程中,每个GBK编码的字节序列被映射到相应的Unicode字符。

说明:内码(Internal Code)是指计算机系统内部使用的字符编码。在处理文本数据时,计算机系统通常会将外部编码(例如用户输入或文件中的编码)转换为内部统一的编码格式。这样做的目的是简化字符处理和操作,因为内码通常是为了适应特定系统或应用程序的需求而设计的。

以下是一些关于内码的关键点:

1)统一处理: 通过使用内码,系统可以将来自不同源和不同编码的文本统一为一种格式,从而简化文本处理和操作。

2)与平台无关: 内码通常设计为与特定平台或硬件无关,这样可以确保在不同系统之间传输和处理文本时的一致性。

3)转换: 当文本从外部源(例如文件、网络或用户输入)进入系统时,它通常会被转换为内码。同样,当文本离开系统时,它通常会被转换回适当的外部编码。

4)例子:Unicode是一种常用的内码标准,它旨在包括世界上所有的书写系统。许多现代操作系统和编程语言都使用Unicode作为内码,因为它允许用统一的方式处理各种不同的字符集。

5)与字符集和编码的关系: 字符集是一组字符的集合,而编码是字符集的具体表示。内码是一种特殊类型的编码,用于系统内部的字符表示。

总的来说,内码是计算机系统内部使用的字符编码,用于统一和简化文本处理。通过将外部编码转换为内码,系统可以更容易地处理来自不同源和不同编码的文本。

2. 编码(Encoding): 接下来,将Unicode字符编码为UTF-8字节序列。UTF-8是一种可变长度的字符编码,它使用1到4个字节来表示每个Unicode字符。UTF-8的设计允许对ASCII字符的向后兼容,这意味着任何有效的ASCII字符串也是有效的UTF-8字符串。

转换工具: 许多编程语言和操作系统提供了用于字符集转换的库和工具。例如,在Python中,你可以使用`encode`和`decode`方法轻松地在不同的字符集之间转换。这些工具通常基于预定义的字符映射表,这些表定义了如何在不同的字符集之间转换字符。

示例代码:

以下是一个使用Python将GBK编码的字符串转换为UTF-8编码的示例:

original_text_gbk = b'xc4xe3xbaxc3' # GBK编码的"你好"
decoded_text = original_text_gbk.decode('GBK') # 解码为Unicode
encoded_text_utf8 = decoded_text.encode('UTF-8') # 编码为UTF-8

print(decoded_text)
print(encoded_text_utf8)

运行结果:

你好
b'xe4xbdxa0xe5xa5xbd'

这里unicode是字符的全集,然后GBK、UTF8字符集与unicode之间有一一对应的映射表,通过查询2张映射表就能成功的进行字符集转换。这一种转换一般是无损的,即不会丢失原始信息。

那么通过unicode可以实现任意2个字符集之间的转换吗?实际是不一定,比如:

gbk->unicode->latin1  不可以
utf8->unicode->latin1  不可以

上面2种转换不可以是因为latin1字符集只能表示256个字符,绝大部分GBK和UTF8的字符在latin1字符集里面根本没有对应编码。
下面再看一些例子:
python例子:

>>> '数据库'.decode('UTF-8')
u'u6570u636eu5e93'

>>> '数据库'.decode('GBK')
Traceback (most recent call last):
File "", line 1, in
UnicodeDecodeError: 'gbk' codec can't decode byte 0x93 in position 8: incomplete multibyte sequence

这里转换失败是因为字符串实际是UTF8编码,但要求python用GBK换为unicode编码,但GBK和unicode的映射表里面没有找到对应的编码。

>>> 'hello'.decode('UTF-8')
u'hello'

>>> '&'.decode('UTF-8')
u'&'

>>> '数据库'.decode('UTF-8').encode('latin1')
Traceback (most recent call last):
File "", line 1, in
UnicodeEncodeError: 'latin-1' codec can't encode characters in position 0-2: ordinal not in range(256)

这里转换失败是因为UTF8字符没有找到latin1字符集中对应的编码。

>>> u'u6570'.encode('latin1')
Traceback (most recent call last):
File "", line 1, in
UnicodeEncodeError: 'latin-1' codec can't encode character u'u6570' in position 0: ordinal not in range(256)

>>> '数据库'.decode('UTF-8').encode('GBK')
'xcaxfdxbexddxbfxe2'

>>> '数据库'.decode('UTF-8').encode('UTF-8')
'xe6x95xb0xe6x8dxaexe5xbax93'

>>> '数据库'.decode('UTF-8').encode('GBK').decode('latin1')
u'xcaxfdxbexddxbfxe2'

2.2 直接转换

如果我们不通过unicode,直接转换可以吗?

这里有2个问题:

1. 转换后对应的字符会发生变化,不再是原来的字符了

2. GBK汉字是双字节,UTF8汉字是3字节,转换过程中可以生产单个字节剩余

3. 因为GBK并不是0000-FFFF的全集,UTF8也不是000000-FFFFFF的合集,所以有些转换会找不到对应的字符

问题2,3导致转换是有损的,即会丢失原始信息,无法复原。

下面看一些例子,第一个看UTF8转GBK:

text = "数据库"
utf8_encoded = text.encode('UTF8')

print(utf8_encoded)

encoded_text_gbk = utf8_encoded.decode('GBK') # 编码为UTF-8

print(encoded_text_gbk)

 运行报错:

b'xe6x95xb0xe6x8dxaexe5xbax93'
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
Cell In[38], line 6
2 utf8_encoded = text.encode('UTF8')
4 print(utf8_encoded)
----> 6 encoded_text_gbk = utf8_encoded.decode('GBK') # 编码为UTF-8
8 print(encoded_text_gbk)

UnicodeDecodeError: 'gbk' codec can't decode byte 0x93 in position 8: incomplete multibyte sequence

这是因为"数据库"这3个汉字的GBK编码是9个字节,换为双字节的GBK的时候,尾部有一个单字节的字符无法转换。

那我如果取前8个字节呢?

text = "数据库"
utf8_encoded = text.encode('UTF8')
utf8_encoded = b'xe6x95xb0xe6x8dxaexe5xba'

print(utf8_encoded)

encoded_text_gbk = utf8_encoded.decode('GBK') # 编码为UTF-8

print(encoded_text_gbk)

 运行结果:

b'xe6x95xb0xe6x8dxaexe5xba'
鏁版嵁搴

 顺利转换为了4个不同的汉字!这是一种乱码的原因。

实际上,可以用参数errors='replace'忽略部分字节的转换错误:

text = "数据库"
utf8_encoded = text.encode('UTF8')

print(utf8_encoded)

encoded_text_gbk = utf8_encoded.decode('GBK',errors='replace') # 编码为UTF-8

print(encoded_text_gbk)
print(encoded_text_gbk.encode('UTF8'))

 运行结果:

b'xe6x95xb0xe6x8dxaexe5xbax93'
鏁版嵁搴�
b'xe9x8fx81xe7x89x88xe5xb5x81xe6x90xb4xefxbfxbd'

注意字节序列xefxbfxbd表示UTF-8编码的替换字符(Replacement Character),其Unicode代码点为U+FFFD。替换字符通常用于替换输入中无法表示的字符。例如,当你尝试将一个无效的字节序列解码为字符串时,解码器可能无法确定该序列应该表示什么字符。在这种情况下,解码器可以插入替换字符来表示原始序列中的错误或未知部分。在许多系统和应用程序中,替换字符通常显示为一个黑色的菱形,其中包含一个白色的问号(�)。此时原始信息已经有部分丢失了!

再看一个例子,GBK转UTF8,这种转换其实 99%不可以,只有少数可以,如"平遥"

text = "平遥"
gbk_encoded = text.encode('GBK')

print(gbk_encoded)

encoded_text_utf8 = gbk_encoded.decode('UTF8',errors='replace') # 编码为UTF-8

print(encoded_text_utf8)

 运行结果:

b'xc6xbdxd2xa3'
ƽң

11000110 10111101 11010010 10100011
可以转换因为"平遥"符合utf8 2字节符号编码规则110… 10…,如果把这个汉字用GBK编码保存到记事本,智能识别有可能出错,显示不是“平遥”。
可能引起误断的汉字见:
http://blog.csdn.net/yimengqiannian/article/details/7060565

gbk->latin1  可以,变为单字节字符,因为latin1字符集包括了00-FF的所有区间,所以转换过程中一定不会丢失信息,可以视为万能字符集。这也是为什么MySQL可以用latin1字符集存GBK或者UTF8汉字的原因。
utf8->latin1  可以,变为单字节字符

text = "平遥"
gbk_encoded = text.encode('GBK')

print(gbk_encoded)

encoded_text_lain1 = gbk_encoded.decode('Latin1',errors='replace') # 编码为UTF-8

print(encoded_text_lain1)
print(encoded_text_lain1.encode('Latin1'))

运行结果:

b'xc6xbdxd2xa3'
ƽң
b'xc6xbdxd2xa3'

3. 摸拟测试

gbk->gbk->gbk 按顺序分别表示插入字符串原始字符集,客户端字符集,表字符集,简单理解就是字符的原始字符集,我们告诉数据库字符是什么字符集,目标字符集。我们告诉数据库字符是什么字符集可以用"set names XXX"命令。

考虑gbk, utf8, latin1三种主流字符集,则一共有27种组合, 下面一一列举:

3.1 完全匹配

gbk->gbk->gbk
utf8->utf8->utf8

这一种是最理想的,一定不会有乱码。

3.2 插入时进行正确的unicode转换

gbk->gbk->utf8
select hex(convert(CONVERT(UNHEX( ‘CAFDBEDDBFE2′) USING gbk) using utf8));
E695B0E68DAEE5BA93

utf8->utf8->gbk
select hex(convert(CONVERT(UNHEX( ‘E695B0E68DAEE5BA93′) USING utf8) using gbk));
CAFDBEDDBFE2

这一种相当于原始字符集与我告诉数据库的字符集匹配,数据库会利用内码unicode进行转换,也不会有乱码。

3.3 latin1单字节流保存

gbk->latin1->latin1
select hex(convert(CONVERT(UNHEX( ‘CAFDBEDDBFE2′) USING latin1) using latin1));
CAFDBEDDBFE2

utf8->latin1->latin1
select hex(convert(CONVERT(UNHEX( ‘E695B0E68DAEE5BA93′) USING latin1) using latin1));
E695B0E68DAEE5BA93

这一种就是利用了latin1是万能字符集,覆盖了00-FF的所有区间,将UTF8和GBK视为单字节字节流,用Latin1存储不会有乱码。但在读取的时候还是要 set names 实际的编码。

3.4 转为unicode后再转为latin1 无法表示,转为3F (latin1 中的?号)

gbk->gbk->latin1
select hex(convert(CONVERT(UNHEX( ‘CAFDBEDDBFE2′) USING gbk) using latin1));
3F3F3F

utf8->utf8->latin1
select hex(convert(CONVERT(UNHEX( ‘E695B0E68DAEE5BA93′) USING utf8) using latin1));
3F3F3F

utf8->gbk->latin1
select hex(convert(CONVERT(UNHEX( ‘E695B0E68DAEE5BA93′) USING gbk) using latin1));
3F3F3F3F

上面的转换实际都失败了,因为latin1字符集只有256个字符,绝大多数的GBK和UTF8字符都无法用Latin1字符集表示。在Latin-1(ISO 8859-1)字符集中,十六进制值3F对应于问号字符(?)。这个字符经常用于替换无法识别或无法表示的字符。例如,当你尝试使用Latin-1编码一个不在Latin-1范围内的字符时,你可以选择使用问号?来替换那个字符,这就是为什么在许多编码转换错误中你会看到问号的原因。

3.5 不符合utf8规则,无法转换,保存为空

gbk->utf8->latin1
gbk->utf8->utf8
gbk->utf8->gbk
select hex(convert(CONVERT(UNHEX( ‘CAFDBEDDBFE2′) USING utf8) using latin1));
Warning | 1300 | Invalid utf8 character string: ‘CAFDBE’

字符实际是GBK编码,我们告诉数据库是UTF8编码,然而数据库尝试用UTF8编码大部分会失败。

3.6 utf8 转换为双字节汉字后,尾部奇数字节截断

utf8->gbk->gbk
select hex(convert(CONVERT(UNHEX( ‘E695B0E68DAEE5BA93′) USING gbk) using gbk));
E695B0E68DAEE5BA
| Warning | 1300 | Invalid gbk character string: ‘93′ |

utf8->gbk->utf8
select hex(convert(CONVERT(UNHEX( ‘E695B0E68DAEE5BA93′) USING gbk) using utf8));
E98F81E78988E5B581E690B4
| Warning | 1300 | Invalid gbk character string: ‘93′ |

3.7 latin1 7f后的字符无法全部转为gbk

gbk->latin1->gbk
select hex(convert(CONVERT(UNHEX( ‘CAFDBEDDBFE2′) USING latin1) using gbk));
3F3F3F3F3F3F

utf8->latin1->gbk 注意 B0转为A1E3 93转为A1B0
select hex(convert(CONVERT(UNHEX( ‘E695B0E68DAEE5BA93′) USING latin1) using gbk));
3F3FA1E33F3F3F3F3FA1B0

select hex(convert(CONVERT(UNHEX('5F6A7F80') USING latin1) using gbk));
5F6A7F3F

这是因为MySQL的Latin1字符集7F之前是ASC II码,在GBK和UTF8字符集中也是一样的编码,但7F之后的字符,可能无法通过unicdoe转为GBK编码!

3.8 latin1字符转为utf8,7f后的字符变为2字节或者3字节

gbk->latin1->utf8 12字节
select hex(convert(CONVERT(UNHEX( ‘CAFDBEDDBFE2′) USING latin1) using utf8));
C38AC3BDC2BEC39DC2BFC3A2

utf8->latin1->utf8  20字节, latin1 的 95,93转为utf8的3字节,所以是3*6+2=20字节
select hex(convert(CONVERT(UNHEX( ‘E695B0E68DAEE5BA93′) USING latin1) using utf8));
C3A6E280A2C2B0C3A6C28DC2AEC3A5C2BAE2809C

4. 脚本测试

4.1 测试脚本

#!/usr/bin/sh

sql=”
create table if not exists chartest.t_utf8
(chartable varchar(100),
charclient varchar(100),
chardata varchar(100),
data varchar(100),
msg varchar(500),
primary key (chartable,charclient,chardata)
)default

create table if not exists chartest.t_gbk
(chartable varchar(100),
charclient varchar(100),
chardata varchar(100),
data varchar(100),
msg varchar(500),
primary key (chartable,charclient,chardata)
)default charset=gbk;

create table if not exists chartest.t_latin1
(chartable varchar(100),
charclient varchar(100),
chardata varchar(100),
data varchar(100),
msg varchar(500),
primary key (chartable,charclient,chardata)
)default charset=latin1;

truncate table chartest.t_utf8;
truncate table chartest.t_gbk;
truncate table chartest.t_latin1;

mysql -e”$sql”

utf8=$(cat utf8.txt)
gbk=$(cat gb2312.txt)

charset=(latin1 utf8 gbk)

for((i=0;i

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论