参考文章
- http://unicode.org/faq/utf_bom.html
- 深入分析 Java 中的中文编码问题
- (wikipedia) Plane (Unicode)
- https://codepoints.net/
- (知乎) Unicode字符集中有哪些神奇的字符?
- Java Language Specification 的 3.1 小节
- (阮一峰) 字符编码笔记:ASCII,Unicode 和 UTF-8
- (MySQL 官方文档) 10.9.3 The utf8 Character Set (Alias for utf8mb3)
- Unicode 中有多少个 code point
- Families
- Unicode surrogate programming with the Java language
- () Unicode和UTF-8、UTF-16、UTF-32
问题
- 什么是 Unicode?
- Unicode 中的 code point 与 Java 中的 char 有何关系?
- utf-8,utf-16,utf-32 是什么?
- 在 Java 中如何遍历 String 里的 code point?
- MySQL 中的 utf8 和 utf8mb4 的区别是什么?
重要名词
-
plane
和Basic Multilingual Plane
-
code point
与code unit
-
high surrogate
和low surrogate
问题1: 什么是 Unicode?
Unicode provides a unique number for every character,
no matter what the platform,
no matter what the program,
no matter what the language.引自 https://www.unicode.org/standard/WhatIsUnicode.html
Unicode 中定义了一些自然数(包括0
)和 code point
之间的映射关系
Unicode 中的平面(plane
)
在 Unicode 标准中, 每个
plane
由65536
个 code point 组成. 总共有17
个plane
, 编号从0
到16
,Plane 16
里最后一个code point
是U+10FFFF
.Plane 0
被称为 Basic Multilingual Plane (BMP), 其中包含了最常用的code point
.Plane 1
到Plane 16
被称为 "supplementary planes".[1]
Unicode Planes
其中4个 plane 的分配情况(白底色表示未分配, 其他底色表示已分配)
-
BMP(Basic Multilingual Plane, 即
Plane 0
):U+0000..U+FFFF
-
SMP(Supplementary Multilingual Plane, 即
Plane 1
):U+10000..U+1FFFF
-
SIP(Supplementary Ideographic Plane 即
Plane 2
):U+20000..U+2FFFF
-
SSP(Supplementary Special-purpose Plane, 即
Plane 14
):U+E0000..U+EFFFF
问题2: Unicode 中的 code point
与 Java 中的 char
有何关系?
The Unicode standard was originally designed as a fixed-width 16-bit character encoding. It has since been changed to allow for characters whose representation requires more than 16 bits. The range of legal code points is now
U+0000
toU+10FFFF
, using the hexadecimalU+n
notation. Characters whose code points are greater thanU+FFFF
are called supplementary characters. To represent the complete range of characters using only 16-bit units, the Unicode standard defines an encoding called UTF-16. In this encoding, supplementary characters are represented as pairs of 16-bit code units, the first from the high-surrogates range, (U+D800
toU+DBFF
), the second from the low-surrogates range (U+DC00
toU+DFFF
). For characters in the rangeU+0000
toU+FFFF
, the values of code points and UTF-16 code units are the same.
java 中的1个 char
相当于1个 code unit
, 1个 code point
对应 1或2个 code unit
, 具体如下
- 当
code point
在U+0000..U+D7FF
或U+E000..U+FFFF
范围内时, 用 1 个 code unit(或 1个Java中的char
)来表示这个code point
- 当
code point
在U+10000..U+10FFFF
范围内时, 用 2 个 code unit(或 2个Java 中的char
)来表示这个code point
如何用2个char
来表示非BMP的code point
:
-
U+10000..U+10FFFF
范围内一共有 2^20 个code point
(0x10FFFF-0x10000+1=0x10000, 即2^20), 所以用20个bit可以区分这些code point
- 高代理(
high surrogate
)有1024
种可能取值, 低代理(low surrogate
)也有1024
种可能取值, 所以高代理和低代理组成的对(high, low)
会有1024 * 1024
种可能取值(而1024 * 1024 = 2 ^ 20
)
- 绿色边框的高代理区域共有1024个
code point
- 蓝色边框的低代理区域共有1024个
code point
当code point
在 U+10000..U+10FFFF
范围内时, 对应的 2个code unit
的计算方法(伪代码)
delta = cp - 0x10000 // 计算给定的 code point 和 0x10000 的差值
temp_high = (delta >> 10) // 取高10位
temp_low = (delta & 0x3FF) // 取低10位
high = temp_high + 0xD800 // 加上高代理的偏移量
low = temp_low + 0xDC00 // 加上低代理的偏移量
jdk 中高代理和低代理的计算
我们可以参考 Character.highSurrogate(int)
和 Character.lowSurrogate(int)
的源码
高代理(high surrogate)的计算
解释如下
public static char highSurrogate(int codePoint) {
// MIN_HIGH_SURROGATE = '\uD800'
// MIN_SUPPLEMENTARY_CODE_POINT = 0x10000
// 计算步骤: 1. 计算差值; 2. 取差值高10个bit; 3. 加上高代理区域的偏移量
// 计算步骤合在一起: ((codePoint - 0x10000) >>> 10) + 0xD800
return (char) ((codePoint >>> 10)
+ (MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT >>> 10)));
}
低代理(low surrogate)的计算
解释如下
public static char lowSurrogate(int codePoint) {
// MIN_LOW_SURROGATE = '\uDC00'
// 由于 (codePoint - 0x10000) 的低10个bit和 codePoint 的低10个bit是一样的
// 所以可以直接计算 codePoint 的低10个bit
// 那么我们在取出 codePoint 的低10个bit后, 加上低代理区域的偏移量 0xDC00
return (char) ((codePoint & 0x3ff) + MIN_LOW_SURROGATE);
}
如何向 StringBuilder append 一个 code point?
问题3: utf-8,utf-16,utf-32 是什么?
Q: What is a UTF?
A: A Unicode transformation format (
UTF
) is an algorithmic mapping from every Unicode code point (except surrogate code points) to a unique byte sequence. The ISO/IEC 10646 standard uses the term “UCS transformation format” for UTF; the two terms are merely synonyms for the same concept.引自 https://www.unicode.org/faq/utf_bom.html
UTF 可以将 Unicode 中每个的code point
(除了U+D800..U+DFFF
范围内的code point
)映射为不同的字节序列
图片来源
(wikipedia) BOM
- UTF-32BE
将code point
对应的整数用大端法的4个字节表示即可
例如用 UTF-32BE 对 U+1F602 进行编码, 得到的字节序列为
0x00
0x01
0xF6
0x02
用 Java 来实现 UTF-32BE 编码并验证
package com.naive.wow;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;
public class NaiveUTF32 {
public static void main(String[] args) throws UnsupportedEncodingException {
Random random = new Random();
for (int i = 0; i < 100; i++) {
// 产生一个随机的 code point
int cp = (int) (random.nextDouble() * 0x10FFFF);
// U+D800..U+DFFF 上的 code point 不用验证
if (cp >= 0xD800 && cp <= 0xDFFF) {
continue;
}
String s = new String(new int[]{cp}, 0, 1);
// bytes 中保存了正确的编码结果
byte[] bytes = s.getBytes("UTF-32BE");
// 将 cp 看成一个大端法表示的4字节数(不过 Java 中的 int 本来就是4字节的), 就可以得到其对应的 UTF-32BE 编码
// calculated 中保存了我们自己计算的编码结果
byte[] calculated = new byte[]{
0, // 第一个字节中的每一位都是 0
(byte) ((cp >> 16) & 0xFF), // 计算第二个字节
(byte) ((cp >> 8) & 0xFF), // 计算第三个字节
(byte) (cp & 0xFF), // 计算第四个字节
};
// 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
assert Arrays.equals(calculated, bytes);
}
}
}
- UTF-16BE
举个例子
求 UTF-16BE 对 U+1F602 进行编码的结果
下面的计算过程是在 Python3
中生成的
>>> cp = 0x1f602
>>> delta = cp - 0x10000
>>> high = (delta >> 10) + 0xD800
>>> low = (delta & 0x3FF) + 0xDC00
>>> print(hex(high))
0xd83d
>>> print(hex(low))
0xde02
>>>
所以对 U+1F602
的编码结果为
0xD8
0x3D
0xDE
0x02
package com.naive.wow;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;
public class NaiveUTF16 {
public static void main(String[] ars) throws UnsupportedEncodingException {
Random random = new Random();
for (int i = 0; i < 100; i++) {
// 产生一个随机的 code point
int cp = (int) (random.nextDouble() * 0x10FFFF);
// U+D800..U+DFFF 上的 code point 不用验证
if (cp >= 0xD800 && cp <= 0xDFFF) {
continue;
}
String s = new String(new int[]{cp}, 0, 1);
// bytes 中保存了正确的编码结果
byte[] bytes = s.getBytes("UTF-16BE");
if (cp < 0x10000) {
// 如果 cp 在 BMP 上, 则对应1个 code unit(也就是2个byte)
byte[] calculated = new byte[]{
(byte) (cp >> 8), (byte) (cp & 0xFF)
};
// 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
assert Arrays.equals(calculated, bytes);
} else {
// 如果 cp 不在 BMP 上, 则对应2个 code unit(也就是4个byte)
int high = ((cp - 0x10000) >> 10) + 0xD800;
int low = ((cp - 0x10000) & 0x3FF) + 0xDC00;
byte[] calculated = new byte[]{
(byte) (high >> 8), (byte) (high & 0xFF),
(byte) (low >> 8), (byte) (low & 0xFF)
};
// 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
assert Arrays.equals(calculated, bytes);
}
}
}
}
- UTF-8
图片来源
简述:
a. 蓝色框中为起始的code point
b. 绿色框中为终止的code point
c. 红色框中为需要填写的 bit
举个例子
求 UTF-8 对 U+1F602 进行编码的结果
下面的计算过程是在 Python3
中生成的
>>> cp = 0x1F602
>>> b0 = 0b11110000 + ((cp >> 18) & 0b111)
>>> print(hex(b0))
0xf0
>>> b1 = 0b10000000 + ((cp >> 12) & 0b111111)
>>> print(hex(b1))
0x9f
>>> b2 = 0b10000000 + ((cp >> 6) & 0b111111)
>>> print(hex(b2))
0x98
>>> b3 = 0b10000000 + (cp & 0b111111)
>>> print(hex(b3))
0x82
>>>
所以对 U+1F602
的编码结果为
0xF0
0x9F
0x98
0x82
package com.naive.wow;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Random;
public class NaiveUTF8 {
public static void main(String[] args) throws UnsupportedEncodingException {
Random random = new Random();
for (int i = 0; i < 100; i++) {
// 产生一个随机的 code point
int cp = (int) (random.nextDouble() * 0x10FFFF);
// U+D800..U+DFFF 上的 code point 不用验证
if (cp >= 0xD800 && cp <= 0xDFFF) {
continue;
}
String s = new String(new int[]{cp}, 0, 1);
// bytes 中保存了正确的编码结果
byte[] bytes = s.getBytes("UTF-8");
if (cp < 0x80) {
// U+0000..U+007F: 7个bit 可以表示. 二进制表示形如 0xxxxxxx
byte[] calculated = new byte[]{
(byte) cp
};
// 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
assert Arrays.equals(calculated, bytes);
} else if (cp < 0x800) {
// U+0080..U+07FF: 11个bit 可以表示. 二进制表示形如 110xxxxx 10xxxxxx
byte[] calculated = new byte[]{
(byte) (0b11000000 + (cp >> 6)),
(byte) (0b10000000 + (cp & 0x3F))
};
// 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
assert Arrays.equals(calculated, bytes);
} else if (cp < 0x10000) {
// U+0800..U+FFFF: 16个bit 可以表示. 二进制表示形如 1110xxxx 10xxxxxx 10xxxxxx
byte[] calculated = new byte[]{
(byte) (0b11100000 + (cp >> 12)),
(byte) (0b10000000 + ((cp >> 6) & 0x3F)),
(byte) (0b10000000 + (cp & 0x3F))
};
// 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
assert Arrays.equals(calculated, bytes);
} else {
// U+10000..U+10FFFF: 21个bit 可以表示. 二进制表示形如 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
byte[] calculated = new byte[]{
(byte) (0b11110000 + (cp >> 18)),
(byte) (0b10000000 + ((cp >> 12) & 0x3F)),
(byte) (0b10000000 + ((cp >> 6) & 0x3F)),
(byte) (0b10000000 + (cp & 0x3F))
};
// 如果两者不一致, 会抛出异常(注意运行时要开启 -ea 选项才能启用断言功能)
assert Arrays.equals(calculated, bytes);
}
}
}
}
问题4: 在 Java 中如何遍历 String 里的 code point?
方法1
public class Traverse {
public static void main(String[] args) {
// s 中的 code point 数量为 3
// s 中的 char 数量为 5
String s = "\uD83d\uDE02" + " " + "\uD83d\uDE02";
int pos = 0;
while (pos < s.length()) {
int cp = s.codePointAt(pos);
// 如果 cp 在 BMP, 则 pos += 1. 如果 cp 不在 BMP, 则 pos += 2
pos += Character.isBmpCodePoint(cp) ? 1 : 2;
System.out.println(Integer.toHexString(cp));
}
}
}
方法2 (Java 8 中支持)
public class Traverse {
public static void main(String[] args) {
String s = "\uD83d\uDE02" + " " + "\uD83d\uDE02";
for (int cp : s.codePoints().toArray()) {
System.out.println(Integer.toHexString(cp));
}
}
}
问题5: MySQL 中的 utf8 和 utf8mb4 的区别是什么?
可以参考(MySQL官网) Unicode Support的相关介绍
- MySQL 中的 utf8 Character Set 是 utf8mb3 Character Set 的别名, 仅支持 BMP 中的 code point (MySQL官网) The utf8mb3 Character Set (3-Byte UTF-8 Unicode Encoding)
- MySQL 中的 utf8mb4 Character Set 支持 BMP 和其他平面的 code point (MySQL官网) The utf8mb4 Character Set (4-Byte UTF-8 Unicode Encoding)
验证
- 建表(注意 CHARSET=utf8)
CREATE TABLE `Naive` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(4) NOT NULL COMMENT '名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
- 执行 insert 语句
insert into Naive(`name`)values("");
执行后会看到报错
- 查看 utf-8 编码
在 https://codepoints.net/ 查看的 utf-8 编码, 与报错信息吻合
- 建立支持非 BMP 字符的表
CREATE TABLE `Good` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(4) NOT NULL COMMENT '名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
- 执行 insert 语句
insert into Good(`name`)values("");
- 确认结果
执行如下 select 语句
select * from Good;
结果为
- more
可以试试如下的两个 insert 语句
insert into Good(`name`)values("");
insert into Good(`name`)values("");
彩蛋
如何查询不认识的字符
在 https://codepoints.net/ 可以查询字符。
例如若想查询 这个
code point
的相关信息,可以访问https://codepoints.net/,会看到如下的信息(包括 utf-8
/utf-16
/utf-32
编码的结果以及各种语言中如何表示这个 code point
)
Plane 0
中的一些字符
⠀
: U+2800
可参见 codepoints 网站的相关描述
⼧
: U+2F27
可参见 codepoints 网站的相关描述
䷀
: U+4DC0
可参见 wikipedia 中的 Yijing Hexagram Symbols
巭
: U+5DED
可参见 codepoints 网站的相关描述
Plane 1
中的一些字符
: U+1F022 (位于
Plane 1
)
可参见 codepoints 网站的相关描述
: U+1F0A1 (位于
Plane 1
)
可参见 codepoints 网站的相关描述
: U+1F602 (位于
Plane 1
)
可参见 codepoints 网站的相关描述
Plane 2
中的一些字符
: U+2000B
可参见 codepoints 网站的相关描述
: U+2F8B2
可参见 [codepoints 网站的相关描述]
(https://codepoints.net/U+2F8B2)
: U+2F9C4
可参见 [codepoints 网站的相关描述]
(https://codepoints.net/U+2F9C4)
其他
3个code point
拼接在一起
ಠ_ಠ
: 里面有3个code point(2个U+0CA0 (ಠ
), 1个 U+005F(_
))
这是1个code point
吗?
: 由7个 code point
组成(具体如下)
-
: U+1F468
- zero-width joiner (ZWJ): U+200D
-
: U+0x1F469
- zero-width joiner (ZWJ): U+200D
-
: U+1F467
- zero-width joiner (ZWJ): U+200D
-
: U+1F466
看起来一样?
: U+1F46A
: 由5个 code point
组成(具体如下)
-
: U+1F468
- zero-width joiner (ZWJ): U+200D
-
: U+0x1F469
- zero-width joiner (ZWJ): U+200D
-
: U+1F466
不同肤色
(Girl): U+1F467
(Girl: Light Skin Tone): U+1F467, U+1F3FB
(Girl: Medium-Light Skin Tone): U+1F467, U+1F3FC
(Girl: Medium Skin Tone): U+1F467, U+1F3FD
(Girl: Medium-Dark Skin Tone): U+1F467, U+1F3FE
(Girl: Dark Skin Tone): U+1F467, U+1F3FF
它们一样吗?
与汉字一
相似的一些code point
-
-
: U+002D -
˗
: U+02D7 -
‐
: U+2010 -
‒
: U+2012 -
–
: U+2013 -
—
: U+2014 -
―
: U+2015
来源
与 a
相似的一些 code point
-
a
: U+FF41 -
: U+1D41A
-
: U+1D44E
-
: U+1D482
-
: U+1D5BA
-
: U+1D5EE
-
: U+1D622
来源
Flags
(China): U+1F1E8, U+1F1F3
(Hong Kong SAR China): U+1F1ED, U+1F1F0
(Macau SAR China): U+1F1F2, U+1F1F4
(United States): U+1F1FA, U+1F1F8
可以用以下26个 code point
来组成 flag
U+1F1E6..U+1F1FF
:U+1F1E6
:U+1F1E7
:U+1F1E8
:U+1F1E9
:U+1F1EA
:U+1F1EB
:U+1F1EC
:U+1F1ED
:U+1F1EE
:U+1F1EF
:U+1F1F0
:U+1F1F1
:U+1F1F2
:U+1F1F3
:U+1F1F4
:U+1F1F5
:U+1F1F6
:U+1F1F7
:U+1F1F8
:U+1F1F9
:U+1F1FA
:U+1F1FB
:U+1F1FC
:U+1F1FD
:U+1F1FE
:U+1F1FF
例如中国为 cn, 将上面的 和
放在一起就可以看到
动手实战
查看一个字符的 utf-8 编码对应的字节序列
除了可以在 https://codepoints.net/ 上查询外, 也可以自己用 Python3 的程序来做到
下面是 utf8.py
#!/usr/local/bin/python3
import sys
f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-8'))
f.close()
我们在命令行执行
./utf8.py ''
后, 在 utf-8 编码下的对应的字节序列就会输出到 名为
result
的文件中
然后在命令行用od
命令可以查看其中的内容(具体如下)
od -t x1 result
查看一个字符的 utf-16/utf-32 编码对应的字节序列
而查看 utf-16/utf-32 编码对应的字节序列也是类似的. 下面是对应的 Python3 程序
utf16be.py
#!/usr/local/bin/python3
import sys
f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-16be'))
f.close()
utf32be.py
#!/usr/local/bin/python3
import sys
f = open('result', 'wb')
f.write(sys.argv[1].encode('utf-32be'))
f.close()