字符集与字符编码
字符集与字符编码
问题的来源
我们都知道计算机只认识二进制的0和1,这就决定了计算机存储和表示数据也就只能用0和1。既然如此,那世界上那么多种语言文字和各种符号应该如何在计算机中表示呢?我们需要将这些符号转化成二进制数据,计算机才能认识,计算机加工过之后的数据,要从二进制转化成语言文字人类才能认识。于是就需要建立一种人类的语言文字与计算机二进制表示的一种对应关系,这就是字符集与字符编码所要解决的问题。
基本知识
字符
各种文字和符号的统称,包括国家文字、标点符号、图形符号、数字等
字符集
顾名思义,就是字符的集合。我们通常说一个字符集,就表示一个系统支持的所有字符的集合。
字符编码
字符集只规定了哪些字符可以用来表示,而字符编码表明字符集中的各个字符如何在计算机中表示,也就是说字符编码是一种关系模型,指定了从一个字符到二进制的转换规则,同样也制定了从二进制到字符的转换规则。要建立这种模型,一般需要四个步骤:
1. 要有一个字符集,该字符集需要足够表意,也就是说要满足使用。
2. 给每个字符一个数字顺序,英文叫 `Code Point`或 `Code Position`, 也就是说建立一张二维表,将每个字符与一个数字对应起来。
3. 确定表示字符的二进制位数,如 8位、16位、32位
4. 确定二进制字符的存储格式,即确定使用大端法(`Big Endian`)还是小端法(`Little Endian`)
到这里我们以最简单最常用的编码ASCII编码为例:

该编码总共有128个字符,这128个字符叫作一个字符集,每一个字符指定了一个十进制的数字与之对应,同时表明了使用8位二进制来表示这些字符(如图中的#高四位为0010,低四位为0011合并起来就是00100011,也就是十进制51),而大端小端表示法是以字节为单位的,该字符集中所有字符都只用一个字节表示,所以没有大端小端的争议。
注意:
这里字符集与字符编码的概念应当分开。字符集是具有完整表意的字符的集合,而字符编码是在字符集的基础上建立起的一套编码方案。比如当我们提及UTF-8编码时,它不仅指代UTF-8这一套编码方案,也隐含指代UTF-8这一套方案背后的Unicode字符集,当我们提及gbk编码时,我们不仅指代gbk编码方案,也指代其背后的字符集。这里你可能晕了,为什么UTF-8编码跟Unicode扯上关系了?这也是我强调将字符集与字符编码区分看待的原因,虽然大多数的字符集只有一种编码方案,但是也有一部分字符集有多种编码方案,比如Unicode字符集,就有UTF-8、UTF-16、UTF-32等多种编码方案。
字符编码
好了,明白了基本的概念以后,我们开始介绍常用的一些字符编码。
ASCII
其实最基本最常用最基础的编码就是上文提到的ASCII编码了。他是 American Standard Code for Information Interchange,美国信息交换标准代码的简称,是基于拉丁字母的一套电脑编码方案,主要用于英语编码。
他的编码字符集共定义了128个字符,包括95个可显示字符(大小写字母,阿拉伯数字数字,标点符号等)和33个控制字符(回车键、退格、换行键等)。
他的编码方式为:使用8位二进制表示128种状态,每中状态对应一个字符。我们知道7位二进制就已经可以表示128种状态了,是的,ASCII也是只用了8位中的七位,最高位填充0。
ISO8859系列编码
在美国这种以英语为主要用于的国家,ASCII已经足够使用了,但是当计算机传到欧洲之后,就不行了。欧洲国家有很多衍生的拉丁字母,这些字符ASCII就不能表示了。为了表示这些拉丁字符,欧洲就需要在ASCII的基础上扩展,而恰好ASCII编码的最高位被填充为0,也就是没有使用的,于是这些国家就打起了最高位的注意,也就有了ISO8859系列编码。由于欧洲国家各自为政,每个国家都基于ASCII进行扩展,导致了欧洲各国单独编码的混乱局面,也就导致了ISO8859系列的产生。为了解决各国各语言的单独编码的混乱局面,国际标准化组织(ISO)及国际电工委员会(IEC)联合制定的一系列8位元字符集的标准,现时定义了15个字符集如下:
- ISO/IEC 8859-1 (Latin-1) - 西欧语言
- ISO/IEC 8859-2 (Latin-2) - 中欧语言
- ISO/IEC 8859-3 (Latin-3) - 南欧语言。世界语也可用此字符集显示。
- ISO/IEC 8859-4 (Latin-4) - 北欧语言
- ISO/IEC 8859-5 (Cyrillic) - 斯拉夫语言
- ISO/IEC 8859-6 (Arabic) - 阿拉伯语
- ISO/IEC 8859-7 (Greek) - 希腊语
- ISO/IEC 8859-8 (Hebrew) - 希伯来语(视觉顺序)
- ISO/IEC 8859-8-I - 希伯来语(逻辑顺序)
- ISO/IEC 8859-9(Latin-5 或 Turkish)土耳其语。
- ISO/IEC 8859-10(Latin-6 或 Nordic)-北日耳曼语支
- ISO/IEC 8859-11 (Thai) - 泰语
- ISO/IEC 8859-12 印度天城体梵文(搁置)
- ISO/IEC 8859-13(Latin-7 或 Baltic Rim)- 波罗的语族
- ISO/IEC 8859-14(Latin-8 或 Celtic)- 凯尔特语族
- ISO/IEC 8859-15 (Latin-9)西欧语言,加入Latin-1欠缺的芬兰语字母和大写法语重音字母,以及欧元(€)符号
- ISO/IEC 8859-16 (Latin-10) - 罗马尼亚语使用,并加入欧元符号
他的编码方式:与ASCII类似,只不过使用了一个字节中的全部8位来表示256个字符.
国标系列
再后来,电脑传到了中国,我泱泱大国,上下五千年文明,岂是区区256个字符能表示的?所以,必须为中文设计一套编码方案,因此也就有了国标系列。
GB2312-80
很明显,中国的汉字远远超过256个,在1981国家标准总局发布了一套名为**GB2312-80**的字符集,简称**GB2312**,全称为:《信息交换用汉字编码字符集·基本集》。该字符集收录了6763个汉字,包括繁体字和一些生僻字在内的其他汉字并未收集在册。如你所见,GB并不是英文缩写,而是国标两个字的拼音首字母缩写。
**GB2312**中对所收汉字进行了“分区”处理,每区含有94个字符(本来是6_16 = 96个字符,但每个区的第一个和最后一个位置留空),共计94个区。即最多可表示94_94(8836个符号)用所在的区和位来表示字符,例如“万”字(编码为0xCDF2)在45区82位,所以“万”字的区位码是:4582.
01~09区(682个):特殊符号、数字、英文字符、制表符等,包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母等在内的682个全角字符;
10~15区:空区,留待扩展;在附录3,第10区推荐作为 GB 1988–80 中的94个图形字符区域(即第3区字符之半形版本);
16~55区(3755个):常用汉字(也称一级汉字),按拼音排序;
56~87区(3008个):非常用汉字(也称二级汉字),按部首/笔画排序;
88~94区:空区,留待扩展。
编码方式:
为了兼容ASCII字符集,GB2312规定一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个GB2312字符。每个字符以两个字节来表示。第一个字节称为“高位字节”,第二个字节称为“低位字节”。“高位字节”使用了0xA1–0xF7(把01–87区的区号加上0xA0),“低位字节”使用了0xA1–0xFE(把01–94加上0xA0)。
关于中文的半角和全角:
在GB2312中除了包含常用中文字符外,还把数学符号、罗马希腊字母、日文的片假名等都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在ASCII中的那些符号就叫"半角"字符了。
关于转换:
例如:‘中’这个字,他的gb2312编码为0xD6D0,他在第几个区呢?可以算出来,0xD6-0xA0 = 0x36也就是在第54区,他在54区的第多少位呢?0xD0-0xA0 = 0x30也就是第48位,则‘中’字的区位码是5448,即第54区第48个
GBK
**GB2312**的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。但对于人名、古汉语等方面出现的罕用字和繁体字**GB2312**不能处理,于是全国信息技术标准化技术委员会1995年发布了《汉字内码扩展规范》,即GBK(GB即国标,K即扩展)。GBK 向下与 GB 2312 编码兼容,向上支持 ISO 10646.1国际标准,是前者向后者过渡过程中的一个承上启下的产物。
编码方式:
采用单双字节变长编码,英文使用单字节编码,完全兼容ASCII字符编码,中文部分采用双字节编码。总体编码范围为 8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 xx7F 一条线。总计 23940 个码位,共收入 21886 个汉字和图形符号,其中汉字(包括部首和构件)21003 个,图形符号 883 个。
GB18030
全称《信息技术 中文编码字符集》,是中华人民共和国国家标准所规定的变长多字节字符集。其对GB 2312完全向后兼容,与GBK基本向后兼容,并支Unicode(GB 13000)的所有码位。GB 18030共收录汉字70,244个。
其特点如下:
- 采用变长多字节编码,每个字可以由1个、2个或4个字节组成。
- 编码空间庞大,最多可定义161万个字符。
- 完全支持
Unicode,无需动用造字区即可支持中国国内少数民族文字、中日韩和繁体汉字以及emoji等字符。
Unicode
当计算机传到世界各个国家时,为了适合当地语言文字,都会实现各自的一套编码方案。各国和地区在本地使用没有问题,当互联网出现时,各个国家地区相互沟通交流就会出现乱码现象。 为了解决这个问题,历史上存在两个组织尝试创立单一字符集而且相互都不知道对方的存在,即国际标准化组织(ISO)于1984年创建的ISO/IEC(后文简称ISO)和由Xerox、Apple等软件制造商于1988年组成的统一码联盟(后文简称统一码联盟)。ISO工作组制定的标准称为通用字符集(Universal Character Set),简称**UCS**。 统一码联盟制定的标准称为Unicode字符集。1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,但两个项目仍都独立存在,并独立地公布各自的标准。不过统一码联盟和ISO/IEC都同意保持两者标准的码表兼容,并紧密地共同调整任何未来的扩展。由于Unicode这一名字比较好记,因而它使用更为广泛。
编码字符集
Unicode采用计划四个字节表示一个字符,首位恒为0,理论上最多能表示个字符,完全可以涵盖一切语言所用的符号。但目前Unicode字符分为17组编排,每组称为平面(Plane),而每平面拥有65536,即
个代码点。然而目前只用了少数平面。
| 平面 | 始末字符值 | 中文名称 | 英文名称 |
|---|---|---|---|
| 0 | U+0000 - U+FFFF |
基本多文种平面 | Basic Multilingual Plane简称BMP |
| 1 | U+10000 - U+1FFFF |
多文种补充平面 | Supplementary Multilingual Plane简称SMP |
| 2 | U+20000 - U+2FFFF |
表意文字补充平面 | Supplementary Ideographic Plane简称SIP |
| 3 | U+30000 - U+3FFFF |
表意文字第三平面未正式使用 | Tertiary Ideographic Plane简称TIP |
| 4-13 | U+40000 - U+DFFFF |
未正式使用 | |
| 14 | U+E0000 - U+EFFFF |
特别用途补充平面 | Supplementary Special-purpose Plane简称SSP |
| 15 | U+F0000 - U+FFFFF |
保留作为私人使用区A区 | Private Use Area-A,简称PUA-A |
| 16 | U+100000 - U+10FFFF |
保留作为私人使用区B区 | Private Use Area-B,简称PUA-B |
目前最常用的是基本多文种平面(Basic Multilingual Plane, BMP),或称第0平面或0号平面(Plane 0)。编码从U+0000至U+FFFF。
中文在Unicode字符集中的分布
Unicdoe1.1,收集中日韩三个国家的中文字符,共20,902个字,分布在0x4E00–0x9FFF这个区域。
Unicode3.0,于0x3400–0x4DFF加入了6,582个字。
Unicode3.1,于0x20000–0x2A6FF加入了42,711个字。
Unicode4.1,于0x9FA6-0x9FB3和0x9FB4-0x9FBB共加入了22个字。
Unicode5.1,于0x9FBC-0x9FC2加入了7个日语汉字。
...
Unicode10.0,于0x2CEB0-0x2EBEF和0x9FD6-0x9FEA加入7494个汉字。
目前累计unicode中共收录了87,882个中文字符。
但常用中文范围为0x4E00—0x9FA5之内,如验证用户输入是否是中文,即可采用此范围。
Unicode字符在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。例如,如果一个仅包含基本7位ASCII字符的Unicode文件,如果每个字符都使用2字节的原Unicode编码传输,其第一字节的8位始终为0。这就造成了比较大的浪费。Unicode的转换格式(Unicode Transformation Format,简称为UTF),常见编码转换方案如下:
UTF-8编码方案
UTF-8是针对Unicode的一种可变长度字符编码,是互联网中应用中优先采用的编码。它可以用来表示Unicode标准中的任何字符,而且其编码中的第一个字节仍与ASCII相容,使得原来处理ASCII字符的软件无须或只进行少部份修改后,便可继续使用。
UTF-8使用1至4个字节为每个字符编码。其编码规则如下:
- 对于单字节的符号,字节的第一位(字节的最高位)设为0,后面7位为这个符号的
unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。 - 对于n字节的符号
(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码
如下图:
| 范围 | Byte1 |
Byte2 |
Byte3 |
Byte4 |
|---|---|---|---|---|
U+00~U+7F |
0xxxxxxx |
|||
U+080~U+7FF |
110xxxxx |
10xxxxxx |
||
U+0800~U+FFFF |
1110xxxx |
10xxxxxx |
10xxxxxx |
|
U+10000~U+10FFFF |
11110xxx |
10xxxxxx |
10xxxxxx |
10xxxxxx |
举个栗子:
‘中’这个字,在Unicode字符集中的位置是U+4E2D,其二进制为:0100111000101101 ,按照UTF-8的编码规则从表中可以看出他需要三个字节来存储,按照UTF-8的编码规则转化:
0100 111000 101101
11100100 10111000 10101101
转化后为:111001001011100010101101 转化为十六进制为:E4B8AD,可以看出 ‘中’的UTF-8编码为:E4B8AD
UTF-16编码方案
UTF-16编码长度也是不固定的,也属于不定长编码。使用二或四个字节为每个字符编码,其中大部分汉字采用两个字节编码,少量不常用汉字采用四个字节编码。UTF-16 编码有大尾序和小尾序之别,即 UTF-16BE 和 UTF-16LE,在编码前会放置一个 U+FEFF 或 U+FFFE(UTF-16BE 以 FEFF 代表,UTF-16LE 以 FFFE 代表),其中 U+FEFF 字符在 Unicode 中代表的意义是 ZERO WIDTH NO-BREAK SPACE,顾名思义,它是个没有宽度也没有断字的空白。
其编码规则如下:
- 如果字符编码在
0x0000-0xFFFF之间,也就是十进制的0到65535之内,则直接使用两字节表示; - 如果字符编码在
0x10000-10FFFF(UNICODE编码范围最大为0x10FFFF),从0x10000到0x10FFFF之间共有0xFFFFF个编码,也就是需要20个bit就可以标示这些编码。如果用M表示从0-0xFFFFF之间的Unicode码值,先讲M将减去BMP的码数0x10000,得到结果N,将N前10 bit作为高位和16 bit的数值0xD800进行 逻辑or 操作(即相加),将后10 bit作为低位和0xDC00做 逻辑or 操作,这样组成的 4个byte就构成了M的编码。
举个栗子:
'中'这个字,在Unicode字符集中的位置是U+4E2D,其二进制为:0100111000101101 ,按照UTF-16的编码规则可以直接使用其在Unicode字符集中的位置作为编码,即其编码为4E2D
'𫰛'这个字,在Unicode字符集中的位置是U+2BC1B,其二进制为:101011110000011011,按照UTF-16的编码规则可以看出,它比0x10000要大,所以需要使用20bit来表示,先将其减去0x10000,即
101011110000011011
- 010000000000000000
-----------------------------------------------
= 011011110000011011
由于其为18位二进制,不足20位,需要前面补两个0,补0之后的二进制为:00011011110000011011,按照规则将其高位的10bit与0xD800逻辑or操作,低位的10bit与0xDC00逻辑or操作
0001101111 0000011011
or 1101100000000000 1101110000000000
-----------------------------------------------
= 1101100001101111 1101110000011011
转化后为:111001001011100010101101 转化为十六进制为:D86FDC1B,可以看出 ‘中’的UTF-16编码为:D86FDC1B
==============================================================================
如有兴趣可以越多大神写的UTF-16的设计思路,原文链接:https://blog.csdn.net/xinbaobaoer/article/details/56290210
UTF-32编码方案
UTF-32采用定长的编码方案,即每个字符都使用4字节编码表示,足以完全覆盖Unicode字符集。可以说每个字符的Unicode编码也就是其UTF-32编码,Unicode编码中不够四字节的在前面补0即可。
'中'这个字,在Unicode字符集中的位置是U+4E2D,按照UTF-32的编码规则可以直接使用其在Unicode字符集中的位置作为编码,即其编码为00004E2D
'𫰛'这个字,在Unicode字符集中的位置是U+2BC1B,按照UTF-32的编码规则也可以直接使用其在Unicode字符集中的位置作为编码,即其编码为0002BC1B
附录
关于编码分类
其实通过各种编码方案的出现时机可以看出,每种编码方案都是特定时期为了解决当时的特定问题而出现的。根据其特点我们可以将这些编码大体分为三类:单字节字符编码、`ANSI`编码和`Unicode`编码。
单字节字符编码
如ASCII及ISO8859系列,都是单字节字符编码,是最简单的一类编码方案。这些编码方案出现最早,也都被后来的编码方案兼容。不过这一类编码方案都只能正常转化 0~255 范围的字符,这也是其局限性。
ANSI 编码
当计算机传到非英语国家时,最初的一段时间没有统一的国际标准,因此各个国家为了能在计算机中显示自己国家的文字符号,都制定了一套自己的编码方案。如GB2312、BIG5(台湾繁体字)、Shift_JIS等。
当我们在使用Windows系统时,ANSI编码意思却代表“本地编码”。也就是说,在中国代表GBK,在台湾代表Big5,在日本代表JIS,所以windows编程中常说的ANSI字符串,就是指本地编码的字符串。
Unicode编码
Unicode编码是真正的大一统编码,它可以给于世界上任何一种语言中包含的文字和字符一个唯一的编码表示,也是目前世界通用的字符集。
关于Base64编码
在互联网的早期,交换机只能处理标准的ASCII码(也就是在一个字节中 最高位是0),因此在设计邮件传输协议时,也是基于ASCII码,导致后来出现的一些多媒体内容如:文件、视频、图片等是无法使用邮件协议传输的。为了解决这一问题,大神就是设计了一种编码,将这些文件转化为标准的ASCII码之后,再使用邮件传输协议进行传输,内容传输到对端之后再使用对等的解码,就可以将文件还原,这就是Base64出现的原因及它解决的问题。
我们看看Base64是如何解决的。根据RFC 4648的标准,Base64使用64个基本的ASCII可见字符作为其字母表:

Base64其实是将24位的二进制bit转化为四个字符的过程。的编码过程如下:
1. 将输入的数据每3个字节作为一组进行分组;3个字节一组,那么一组就是3*8=24bit,将这24bit按照每组6位重新划分,这样就分成了四组,每组根据其二进制转化为十进制索引,根据索引从Base64字母表中查询,即为该组的编码
2. 我们知道输入数据是以字节为单位,他是8bit的整数倍,但不一定是24bit的整数倍;如果最后一组不足24bit,就需要填充,那么可能有一下三种两种情况:
(1). 最后一组是24bit,最后的编码结果没有=填充
(2). 最后一组是16bit,那么它只能按照每组6位的方式分成3组(此时需要填充两个0 bit),此时不够四组,需要填充一个=组成四组,最后的编码结果有一个=填充;
(3). 最后一组是8bit,那么它只能按照每组6位的方式分成2组(此时需要填充4个0 bit),此时不够四组,需要填充两个=组成四组,最后的编码结果有两个=填充;
注意:=填充的是以字节为单位进行填充,bit填充是以bit为未单位进行填充
以hello为例说明:

以hello!!为例说明:

URLEncoder和MIMEEncoder
由于在对URL和文件名进行Base64编码时,为了不与元数据冲突,在字母表中将+替换成了-,将/替换成了_。
在对MIME进行编码时,字母表与基本的Base64字母表相同,RFC 4648标准规定每隔76个字符,必须有一个换行符,因此它比基本的Base64编码多了一些换行符。
Java语言中的编码
现如今,大部分操作系统内核都是支持Unicode字符集的,因此Java也采用了unicode字符集,使之易于国际化且便于在各个操作系统平台进行移植。
个人认为Java中的Charset指的是编码方案,而非字符集。Java中有UTF-8、UTF-16等Charset,而这些Charset使用的都应该是Unicode字符集。
Java可识别170中编码方式,默认编码方式是获取的JVM编码方式。
final SortedMap<String, Charset> charsets = Charset.availableCharsets();
System.out.println(charsets.size());
============jdk8 输出170================
final Charset charset = Charset.defaultCharset();
System.out.println(charset.name());
============jdk8 windows10 输出UTF-8,默认编码一般取决于底层系统的locale和charset配置===========
如何在Java语言中判断一个字符是中文字符?这是开发中很常见的问题。既然Java采用的是Unicode编码,我们只需要将字符与Unicode编码的码位比较,看看该字符对应的码位是不是Unicode中中文字符的码位即可。不过现在网上大多数都是通过判断字符是不是在0x4E00—0x9FA5来判断是不是中文。不过,这种方式目前尚可,但是如果中文字符的Unicode编码范围扩大了呢?其实Unicode中对不同的语言字符进行了划片,一个片是一种语言,另一个片是另外一种语言。Java语言也对这种划片进行了支持。因此我们可以使用如下正则进行判断:
第一种正则方式:
Pattern UNICODE_CHINESE_PATTERN =
Pattern.compile("[\\p{InCJK Unified Ideographs}&&\\P{Cn}]");
final String chnWord = new String("\uD86F\uDC1B");
System.out.println(chnWord);
System.out.println(UNICODE_CHINESE_PATTERN.matcher(chnWord).matches());
解释:
1.Java对正则进行了扩展,加入了一些Java语言特有的表达式及标识,具体可参见:https://docs.oracle.com/javase/8/docs/api/index.html?java/util/regex/Pattern.html
2. CJK Unified Ideographs 即中日韩统一表意字符。这是unicode中的一个片区,目前是0x4E00—0x9FFF这个范围,InCJK Unified Ideographs,是java正则对unicode划片的支持,使用该表达式即表示0x4E00—0x9FFF范围,具体可参见:https://docs.oracle.com/javase/8/docs/api/java/lang/Character.UnicodeBlock.html#forName(java.lang.String)
3. Cn Unicode 中未被定义字符的编码 \P{Cn} 就表示 Unicode中已经被定义字符的编码,具体可参见:http://www.unicode.org/reports/tr18/#General_Category_Property
第二种片区检测方式:
上述方式也存在问题,就是只能判断常用汉字,并不能判断中文标点符号,因此如果要加入中文标点符号,可使用如下方法:
private static boolean isChinese(char c) {
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS ||
ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS ||
ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A ||
ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B ||
ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION ||
ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS ||
ub == Character.UnicodeBlock.GENERAL_PUNCTUATION) {
return true;
}
return false;
}
不过第二种方式也有问题,像‘𫝴’,‘𫰛’这种生僻字依然辨别不出来,经查,生僻字都在扩展区。jdk8中是没有CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E,CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E这两个扩展区的,因此位于这些扩展区的生僻字也无法判断
final String string1 = new String("𫝴");
Character.UnicodeBlock ub = Character.UnicodeBlock.of(string1.charAt(0));
System.out.println(ub);
输出为 HIGH_SURROGATES,是unicode的高位代理区,个人感觉这个输出是不对的,但是换成JDK11之后,虽然多了两个扩展区,输出却是一样的,目前不清楚原因。
MYSQL中的utf8
MYSQL的utf8实现最多支持3个字节,我们都知道标准的UTF-8是1-4字节的变长编码方案,最长支持4个字节。那为什么MYSQL那么奇葩呢?让我们来回头看看历史:
MySQL 从 4.1 版本开始支持 UTF-8,也就是 2003 年,当时UTF-8的标准刚刚制定RFC3629,所以MYSQL遵循的是1998年制定的UTF-8标准RFC2279。因此Mysql的UTF-8也就无法兼容最新的版本。MYSQL 2010 年发布了一个叫作 utf8mb4 的字符集,兼容了最新的RFC3629标准,mb4就是most bytes 4的意思,专门用来兼容四字节的unicode。utf8mb4作为Mysql utf8的超集,不仅完全兼容了Mysql utf8,也符合最新的UTF-8标准,完全涵盖Unicode字符集。
做过开发的同学可能都知道,我们在指定Mysql的编码时,不仅要指定其character set,还需要指定collation。character set我们都知道是编码字符集,但是collation又是什么呢?
collation指定的是字符排序规则。字符排序规则是Mysql用于字符比较和排序使用的。我就很纳闷,既然字符集已经确定了,你比较字符就按照字符在字符集中的顺序进行比较就好了啊,为啥还要弄一个字符排序规则呢?Mysql人家也有自己的理由:
我们都知道在Mysql中的字符串存储可以使大小写敏感的,也可以是大小写不敏感的。大小写敏感的可以使用字符集中的默认顺序比较,但是大小写不敏感的呢?因此Mysql就很不得不让用户去指定是大小写敏感还是不敏感。人家还说了,使用这一套操作使得Mysql可以:
- 使用各种字符集存储字符串。
- 使用各种排序规则比较字符串。
- 在同一服务器,同一数据库甚至同一表中混合使用具有不同字符集或排序规则的字符串。
- 在任何级别启用字符集和排序规则的规范。
具体可参见Mysql官网说明:https://dev.mysql.com/doc/refman/8.0/en/charset-general.html
不过Mysql排序规则对我们有什么影响呢?对于mysql中那些字符类型的列,如VARCHAR,CHAR,TEXT类型的列,都需要有一个collation类型来告知mysql如何对该列进行排序和比较。简而言之,collation会影响到ORDER BY语句的顺序,会影响到WHERE条件中大于小于号筛选出来的结果,会影响DISTINCT、GROUP BY、HAVING语句的查询结果。另外,mysql建索引的时候,如果索引列是字符类型,也会影响索引创建,只不过这种影响我们感知不到。总之,凡是涉及到字符类型比较或排序的地方,都会和collation有关。
我们先看一看Mysql中支持哪些字符集
SHOW CHARACTER SET;
我们发现它总共有41中字符集可供选择,每一种字符集都有默认的排序规则
我们再看看常用的utf8mb4它有几种collation可供选择:
SHOW COLLATION WHERE Charset = 'utf8mb4';
仅utf8mb4就有74中排序规则供你选择,慌不慌,不知道选哪个吧。
这里简单的说明一下,在collation选项中,带有cs后缀的就是case sensitive,大小写敏感,ci后缀的就是case insensitive大小写不敏感。我们这里说一说国内比较常用的三个:utf8mb4_general_ci(默认)、utf8mb4_unicode_ci和utf8mb4_bin。
图中我们能看到很多国家的语言自己的排序规则。在国内比较常用的是utf8mb4_general_ci(默认)、utf8mb4_unicode_ci、utf8mb4_bin这三个。
utf8mb4_bin的比较方法其实就是直接将所有字符看作二进制串,然后从最高位往最低位比对。因此它是区分大小写的。
utf8mb4_unicode_ci和utf8mb4_general_ci对于中文和英文来说,其实是没有任何区别的。对于我们开发的国内使用的系统来说,随便选哪个都行。只是对于某些西方国家的字母来说,utf8mb4_unicode_ci会比utf8mb4_general_ci更符合他们的语言习惯一些,general是mysql一个比较老的标准了。例如,德语字母“ß”,在utf8mb4_unicode_ci中是等价于"ss"两个字母的(这是符合德国人习惯的做法),而在utf8mb4_general_ci中,它却和字母“s”等价。不过,这两种编码的那些微小的区别,对于正常的开发来说,很难感知到。本身我们也很少直接用文字字段去排序,退一步说,即使这个字母排错了一两个,真的能给系统带来灾难性后果么?从网上找的各种帖子讨论来说,更多人推荐使用utf8mb4_unicode_ci,但是对于使用了默认值的系统,也并没有非常排斥,并不认为有什么大问题。
结论:
个人推荐使用`utf8mb4_unicode_ci`,对于已经用了`utf8mb4_general_ci`的系统,无需烦恼,因为真的影响不大。 官网对别这两种`collation`时也说了,`general_ci`比`unicode_ci`效率更高,但是比较结果相对来说不准确,因为`unicode_ci`支持扩展,而`general_ci`不支持扩展。另外需要注意的一点是,从`mysql 8.0`开始,`mysql`默认的`CHARSET`已经不再是`Latin1`了,改为了`utf8mb4`([参考链接](https://links.jianshu.com/go?to=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Frefman%2F8.0%2Fen%2Fcharset-applications.html)),并且默认的`collation`也改为了`utf8mb4_0900_ai_ci`。`utf8mb4_0900_ai_ci`大体上就是`unicode`的进一步细分,`0900`指代`unicode`比较算法的编号( `Unicode Collation Algorithm version`),`ai`表示`accent insensitive`(发音无关),例如`e, è, é, ê` 和 `ë`是一视同仁的。[相关参考链接1](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.monolune.com%2Fwhat-is-the-utf8mb4_0900_ai_ci-collation%2F),[相关参考链接2](https://links.jianshu.com/go?to=https%3A%2F%2Fdev.mysql.com%2Fdoc%2Frefman%2F8.0%2Fen%2Fcharset-collation-names.html)
HTTP协议中的百分号编码
首先我们看一下以ABNF表示的HTTP的URL格式:
foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/ \________/\_________/ \__/
| | | | |
scheme authority path query fragment
其中query部分是以key=value的格式进行传参,键值对之间以&分割,但是如果你的key 或者 value中出现了=和&这样的内容呢?这肯定会造成服务器解析错误。因此,Url编码的原则就是使用安全的字符(没有特殊用途或者特殊意义的可打印字符)去表示那些不安全的字符。那么,哪些字符需要编码呢?
RFC3986标准规定了URL中只允许包含英文字母(a-zA-Z)、数字(0-9)和-_.~四个特殊字符。RFC3986文档对Url的编解码问题做出了详细的建议,指出了哪些字符需要被编码才不会引起Url语义的转变,以及对为什么这些字符需要编码做出了相应的解释。
特殊字符
不可打印字符
Url中只允许使用可打印字符。US-ASCII码中的10-7F字节全都表示控制字符,这些字符都不能直接出现在Url中。同时,对于80-FF字节(ISO-8859-1),由于已经超出了US-ACII定义的字节范围,因此也不可以放在Url中。
保留字符
Url可以划分成若干个组件,协议、主机、路径等。有一些字符(:/?#[]@)是用作分隔不同组件的。例如:冒号用于分隔协议和主机,/用于分隔 主机和路径,?用于分隔路径和查询参数,等等。还有一些字符(!$&'()*+,;=)用于在每个组件中起到分隔作用的,如=用于表示查询参数中 的键值对,&符号用于分隔查询多个键值对。当组件中的普通数据包含这些特殊字符时,需要对其进行编码。
RFC3986中指定了以下字符为保留字符:
! * ' ( ) ; : @ & = + $ , / ? # [ ]
不安全字符
有一些字符,当他们直接放在Url中的时候,可能会引起解析程序的歧义。这些字符被视为不安全字符,原因有很多。
| 字符 | 说明 |
|---|---|
| 空格 | Url在传输的过程,或者用户在排版的过程,或者文本处理程序在处理Url的过程,都有可能引入无关紧要的空格,或者将那些有意义的空格给去掉 |
| 引号以及<> | 引号和尖括号通常用于在普通文本中起到分隔Url的作用 |
| # | 通常用于表示书签或者锚点 |
| % | 百分号本身用作对不安全字符进行编码时使用的特殊字符,因此本身需要编码 |
| {}| ^ []`~ | 某一些网关或者传输代理会篡改这些字符 |
需要注意的是,对于Url中的合法字符,编码和不编码是等价的,但是对于上面提到的 这些字符,如果不经过编码,那么它们有可能会造成Url语义的不同。因此对于Url而言,只有普通英文字符和数字,特殊字符$-_.+!*'()还有保留 字符,才能出现在未经编码的Url之中。其他字符均需要经过编码之后才能出现在Url中。
但是由于历史原因,目前尚存在一些不标准的编码实现。例如对于~符号,虽然RFC3986文档规定,对于波浪符号~,不需要进行Url编码,但是还是有很多老的网关或者传输代理会
百分号编码方式:
Url编码默认使用的字符集是US-ASCII。例如a在US-ASCII码中对应的字节是0x61,那么Url编码之后得到的就 是%61,我们在地址栏上输入http://g.cn/search?q=%61%62%63,实际上就等同于在google上搜索abc了。又如@符号 在ASCII字符集中对应的字节为0x40,经过Url编码之后得到的是%40。
对于非ASCII字符,需要使用ASCII字符集的超集进行编码得到相应的字节,然后对每个字节执行百分号编码。 对于Unicode字符,RFC文档建议使用utf-8对其进行编码得到相应的字节,然后对每个字节执行百分号编码;“中文”使用UTF-8字符集得到 的字节为0xE4 0xB8 0xAD 0xE6 0x96 0x87,经过Url编码之后得到 %E4%B8%AD%E6%96%87。
如果某个字节对应着**ASCII**字符集中的某个非保留字符,则此字节无需使用百分号表示。 例如“Url编码”,使用UTF-8编码得到的字节是0x55 0x72 0x6C 0xE7 0xBC 0x96 0xE7 0xA0 0x81,由于前三个字节对应着ASCII中的非保留字符“Url”,因此这三个字节可以用非保留字符“Url”表示。最终的Url编码可以简化成 Url%E7%BC%96%E7%A0%81,当然,如果你用%55%72%6C%E7%BC%96%E7%A0%81也是可以的。
注意:由于历史的原因,有一些Url编码实现并不完全遵循这样的原则。
JS中的escape,encodeURI和encodeURIComponent的区别
Javascript中提供了3对函数用来对Url编码以得到合法的Url,它们分别是escape/unescape,encodeURI/decodeURI和encodeURIComponent/decodeURIComponent。他们到底有什么不同?
- 保留字不同
| 方法 | 保留字符 |
|---|---|
escape(69个) |
*/@+-._0-9a-zA-Z |
encodeURI(82个) |
!#$&'()*+,/:;=?@-._~0-9a-zA-Z |
encodeURIComponent(71个) |
!'()*-._~0-9a-zA-Z |
- 兼容性不同
escape函数是从Javascript1.0的时候就存在了,其他两个函数是在Javascript1.5才引入的。但是由于Javascript1.5已经非常普及了,所以实际上使用encodeURI和encodeURIComponent并不会有什么兼容性问题。 - 对
Unicode字符的编码方式不同
这三个函数对于ASCII字符的编码方式相同,均是使用百分号+两位十六进制字符来表示。但是对于Unicode字符,escape的编码方式是%u*xxxx*,其中的xxxx是用来表示unicode字符的4位十六进制字符。这种方式已经被W3C废弃了。但是在ECMA-262标准中仍然保留着escape的这种编码语法。**encodeURI**和**encodeURIComponent**则使用**UTF-8**对非**ASCII**字符进行编码,然后再进行百分号编码。这是RFC推荐的。因此建议尽可能的使用这两个函数替代escape进行编码。 - 适用场合不同
encodeURI被用作对一个完整的URI进行编码,而encodeURIComponent被用作对URI的一个组件进行编码。从上面提到的安全字符范围表格来看,我们会发现,encodeURIComponent编码的字符范围要比encodeURI的大。我们上面提到 过,保留字符一般是用来分隔URI组件(一个URI可以被切割成多个组件,参考预备知识一节)或者子组件(如URI中查询参数的分隔符),如:号用于分隔scheme和主机,?号用于分隔主机和路径。由于encodeURI操纵的对象是一个完整的的URI,这些字符在URI中本来就有特殊用途,因此这些保 留字符不会被encodeURI编码,否则意义就变了。
组件内部有自己的数据表示格式,但是这些数据内部不能包含有分隔组件的保留字符,否则就会导致整个URI中组件的分隔混乱。因此对于单个组件使用encodeURIComponent,需要编码的字符就更多了。
当Html的表单被提交时,每个表单域都会被Url编码之后才在被发送。由于历史的原因,表单使用的Url编码实现并不符合最新的标准。例如对于空 格使用的编码并不是%20,而是+号,如果表单使用的是Post方法提交的,我们可以在HTTP头中看到有一个Content-Type的header, 值为application/x-www-form-urlencoded。大部分应用程序均能处理这种非标准实现的Url编码,但是在客户端Javascript中,并没有一个函数能够将+号解码成空格,只能自己写转换函数。还有,对于非ASCII字符,使用的编码字符集取决于当前文档使用的 字符集。例如我们在Html头部加上
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
这样浏览器就会使用gb2312去渲染此文档(注意,当HTML文档中没有设置此meta标签,则浏览器会根据当前用户喜好去自动选择字符集,用户也可以强制当前网站使用某个指定的字符集)。当提交表单时,Url编码使用的字符集就是gb2312。
浙公网安备 33010602011771号