一、踩坑实录
关于字符串编码,遇到过一个比较有意思的问题。
1.1 case背景
服务 A 请求服务 B 时,参数列表带了一个字符串s="中国银行"
。但是请求失败了,服务 B 给服务 A 返回了一个错误信息为 errmsg="s包含特殊字符"
的异常。
已知信息简述如下:
- 服务 A 从上游接收 UTF-8 编码的字符串数据。
- 服务 A 和 服务 B 使用 http 通信,A 对字符串做了URLEncoder(GB18030),B 对接收到的字符串做了URLDecoder(GBK)。
- 在此之前,也有过从 A 到 B 参数为
s="中国银行"
的请求,但没报过这种异常。
1.2 问题定位
首先怀疑可能是 GBK 和 GB18030 两种编码格式不兼容(尽管有些资料会告诉你它们是兼容的),在日志上把数据拷贝下来,做了如下测试:
private static void demo1() throws UnsupportedEncodingException {
String s = "中国银行";
System.out.println(s + " GBK-GBK编解码还原结果 :" + testReduction(s, "GBK", "GBK"));
System.out.println(s + " GB18030-GBK编解码还原结果 :" + testReduction(s, "GB18030", "GBK"));
System.out.println(s + " GB18030-GB18030编解码还原结果:" + testReduction(s, "GB18030", "GB18030"));
}
// 获取字符串 URLEncoder=encode 编码 再 URLDecoder=decode 解码的还原结果
private static String testReduction(String str, String encode, String decode)
throws UnsupportedEncodingException {
String encodeStr = URLEncoder.encode(str, encode);
return URLDecoder.decode(encodeStr, decode);
}
运行结果:
中国银行 GBK-GBK编解码还原结果 :中国银?
中国银行 GB18030-GBK编解码还原结果 :中国银�0�0
中国银行 GB18030-GB18030编解码还原结果:中国银行
即,GB18030 对 行
这个字 URLencode 后再 URLdecode 是没问题的,而 GBK 编码后无法再解回来,基本可以判定:GB18030 支持 行
这个字,GBK 不支持。
1.3 疑问
但是有点不太对劲啊,“行”这个汉字使用频率还是比较高的,属于一级汉字的范畴,GBK 不可能不支持,去 GBK 码表搜 行
也是能搜到的。那为什么会出现这种情况呢?难道 GBK 码表里的 行
和本例中的 行
不是同一个字?
private static void demo2() {
String s1 = "行";// 从日志上拷贝的
String s2 = "行";// 从GBK码表拷贝的
System.out.println(s1.equals(s2));
}
结果为:false,也就是说,GBK 码表里支持的 行
字,和本例中上游传过来的 行
字(日志中拷贝的)确实是不相同的。
知识点来了,java 中的 String 类是按照 unicode 进行编码的,如果两个字符不相同,说明它们的 unicode 码不同。
为了确认这一点,借助工具分别查了一下这两个字的 unicode 码,以及各个字符集对这两个字的支持情况,如下图:
- 日志中拷贝的,即上游传过来的
行
:
- GBK字符集中拷贝的
行
:
如上,与猜想的一致,Unicode 和 GB18030 都支持 行
和 行
,但编码不同;而GBK 仅支持 行
。同步上游将这个字修正为 GBK 支持的“行”字后对外请求可以正常提交,不再赘述。
澄清:GB18030 和 GBK 兼容指的是 GB18030 向下兼容 GBK,不是互相兼容。
那么,问题又来了=>
二、为什么一个字会被编两个 unicode 码呢?
2.1 编码方案的“本地化”和“国际化”
我们都知道,最早 ASCII 是一种单字节编码方案,共支持 128 个字符。
但计算机到了中国后,ASCII 不够用了,就有了 GB 系列的 GB2312、GBK、GB18030 等双字节/多字节编码标准。
同样的,在其他国家/地区,计算机字符编码也遭遇了“水土不服”的情况,于是各个国家/地区为了让自己的文字能够被支持,都制定了各自的编码标准。这样一来,各地区之间的编码标准互不兼容,多语言环境下的文字处理就变的很头疼。
所以,后来就有了 Unicode 编码标准,目的就是制定国际规范,将各国家各地区的字符收录到一起,统一编码并推动各地区实施该规范,以解决全球范围内地区性的编码方案互不兼容带来的一系列问题。
2.2 Unicode 和 CJK
Unicode 的字符来源于各国各地区已有的编码体系,叫做“字源”,比如:中国大陆的G源、中国台湾的T源、日本的J源、韩国的K源等。
为了尽可能使 Unicode 收录的字符完整精简且利于推广,Unicode 从不同的资源中收录字符时遵循两个原则:
- 对字不对形原则,即一个字符只收录一次,同字不同形的字符不予多次录入。目的是尽可能的精简收录到 Unicode 内的字符数量。
- 字源分离原则,若同一字源收录了同一字符的多个字形,则 unicode 需要与字源规范一致,屏蔽上面的“对字不对形”原则,转而同时收录这同一个字符的多个字形。目的是便于 Unicode 推广。
到这里就要把“汉字”单拎出来提一提,中文、日文、韩文里,都有汉字。(越南文和新加坡文里也有汉字)
为了便于汉字的统一处理,ISO 10646 和 Unicode 成立了“中日韩联合研究小组”,基于各国的汉字编码,独自订定规范、制作 ISO 10646 和 Unicode 的统一汉字编码,也就是“中日韩统一表意文字(CJK Unified Ideographs)”,把来自中文、日文、韩文中,起源相同、本义相同、形状一样或稍异的表意文字,赋予其在 ISO 10646 及Unicode 标准中有相同编码。
在 Unicode 码表中,可以看到有一个块区叫做 “CJK Unified Ideographs” ,指的就是上面的统一汉字。除了这个块区外,还有其他的带 CJK 前缀的块区,也都属于 CJK 范畴。
关于“字源分离原则”,举个例子:有一些 CJK 汉字各地字型多有微妙的差异,如“户”字的第一笔,台湾作撇“戶”、中国香港及中国大陆作点“户”、日本作横“戸”,这种程度的差异,理想上是整并为一个字为佳。然而,从之前各种受挫之文字整并计划的经验得知,整合字集与现行通用字集无法一一对应,是推行整合字集的最大阻碍。所以折中一点的办法就是把这几个字都收了。
2.3 延伸
延伸一下,关于前面的问题:为什么一个字被编了两个码,到这里算是有了答案:某个字源同时收录了同一字符的两个字形。
在 Unicode 码表中,其实是可以查到每一个字符的字源的。
比如:
编码为 FA08 的 行
编码为 884C 的 行
编码884C的“行”字字源有5个
- G0表示中国G源(GB2312-80)
- HB1表示香港字源(Big-5, Level 1)
- T1表示台湾资源(TCA-CNS 11643-1992 1st plane)
- J0表示日本字源(JIS X 0208-1990)
- K0表示韩国字源(KS X 1001:2004 (formerly KS C 5601-1987))
- V1表示越南资源(TCVN 6056:1995)
每个字源中对“行”这个字的字形定义都不太一样,但 Unicode 最后只收了一个。
如上分析可知,韩国字源的 KS C 5601-1987 字符集同时收录了 行
和 行
,到 Unicode 也就有了两个编码的 行
字,一个编码 884C ,另一个编码 FA08。
汉字字源说明参见:Unicode Han Database (Unihan)
三、思考和改进
踩了这么个坑,怎么填呢?可以从两个角度去考虑:
- 技术层面,可否提供识别汉字,或者中文汉字的检查能力?
- 业务处理流程中,哪些场景会需要做此类检查?
3.1 技术能力
技术角度,考虑3个问题:
- 能不能识别一个字符是否为汉字?=>能
- 能不能识别一个字符是否为中文汉字?=>不能
- 能不能判断某个字符集是否支持指定汉字字符?=>能
ok,简单写个util,代码如下:
package com.ann.javas.javacores.strings;
import java.io.UnsupportedEncodingException;
public class CharacterUtil {
private final static String GBK = "GBK";
private final static String GB2312 = "GB2312";
private final static String GB18030 = "GB18030";
// 判断字符串是否全部为CJK统一表意文字(仅CJK统一表意文字,不包括扩充集)
public static boolean isAllCJKLetter(String strName) {
char[] ch = strName.toCharArray();
for (char c : ch) {
if (!isCJKLetter(c)) {
return false;
}
}
return true;
}
private static boolean isCJKLetter(char c) {
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
return Character.isDefined(c) && ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS;
}
// 判断字符串是否全部为CJK系列字符(CJK统一表意文字及扩充字符)
public static boolean isAllCJK(String strName) {
char[] ch = strName.toCharArray();
for (char c : ch) {
if (!isCJK(c)) {
return false;
}
}
return true;
}
private static boolean isCJK(char c) {
Character.UnicodeScript uc = Character.UnicodeScript.of(c);
return Character.isDefined(c) && uc == Character.UnicodeScript.HAN;
}
// 判断字符串是否被指定字符集支持
public static boolean isGBK(String str) {
return isSupportedCharset(str, GBK);
}
public static boolean isGB2312(String str) {
return isSupportedCharset(str, GB2312);
}
public static boolean isGB18030(String str) {
return isSupportedCharset(str, GB18030);
}
private static boolean isSupportedCharset(String str, String charsetName) {
try {
return str.equals(new String(str.getBytes(charsetName), charsetName));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return false;
}
}
}
3.2 业务处理
业务流程中,需要做汉字字符检查或字符集过滤的场景并不多,举两个例子如下:
下游服务对字符编码有严格要求
有一些系统由于历史原因,仅支持 GB2312 字符,接口文档上就会明确要求编码格式为 GB2312 。在此场景下,上游就需要加字符集过滤策略。
public boolean scene1(String xmlRequest){
return CharacterUtil.isGB2312(xmlRequest);
}
特殊业务需要检查中文字符
用户实名时,为了尽可能的把错误请求拦截在系统外,需要检查输入的用户姓名是否为 CJK 汉字字符,再严格一点可以同时加上字符集过滤。
public boolean scene2(String userName){
return CharacterUtil.isAllCJKLetter(userName);
}
或
public boolean scene2(String userName){
return CharacterUtil.isAllCJKLetter(userName) && CharacterUtil.isGB2312(userName);
}
四、扩展阅读
字符集和字符编码的内容其实是很多很杂的,前面讲到的只是冰山一角,一个小小的切入点,需要学习的还很多,慢慢积累吧。下面的内容是我在研究过程中的一点点总结和收获,和大家分享一下~
4.1 字符集和字符编码
- Character Set (or Character Repertoire) is the set of characters you can use.
- Character Encoding is the way these characters are stored into memory.
- Charset usually refers to both the character repertoire and the encoding scheme.
说明:
Character Set 和 Charset 在中文中都译为“字符集”,但实际上Character Set 仅指代字符集合,而 Charset 常常指代字符集合和字符编码。
- 一般来说,一个字符集,对应一套编码方案,比如 ASCII、GB2312 。但也有特例,Unicode 字符集对应的编码方案就不止一套:UTF-8、UTF-16等。
- 通常我们所说的 XXX 编码表,实际上既给出了字符集,又给出了字符编码方式。
4.2 GB系列:GB2312、GBK、GB18030
GB2312 (又称 GB2312-80 )是第一个汉字编码国家标准,叫做“中国国家标准简体中文字符集”,全称《信息交换用汉字编码字符集·基本集》,又称 GBo。
GBK(Chinese Internal Code Specification),是 GB2312 标准基础上的内码扩展规范,全称《汉字内码扩展规范》。
GB18030-2000 是 GBK 的取代版本,它的主要特点是在 GBK 基础上增加了 CJK 统一汉字扩充A的汉字。
GB18030-2005 的主要特点是在 GB18030-2000 基础上增加了 CJK 统一汉字扩充B的汉字。
GB18030,现在说的 GB18030 标准实际上就是 GB18030-2005,全称:国家标准 GB18030-2005《信息技术中文编码字符集》,是中华人民共和国现时最新的内码字集,是 GB18030-2000《信息技术信息交换用汉字编码字符集基本集的扩充》的修订版。
4.3 编码标准:Unicode、ISO 10646、GB13000
Unicode 是统一码的意思,由一个名为 ** Unicode 联盟** 的学术学会的机构制订的字符编码系统。Unicode 为世界上的每个字符提供了平台无关、程序无关、语言无关的唯一编码。
ISO 10646 是国际标准化组织 ISO 公布的一套编码标准,即 Universal Multilpe-Octet Coded Character Set(简称UCS),大陆译为《通用多八位编码字符集》,台湾译为《广用多八位元编码字元集》,它与 Unicode 编码完全兼容。ISO 10646.1 是该标准的第一部分《体系结构与基本多文种平面》,我国 1993 年以 GB 13000.1 国家标准的形式予以认可(即 GB 13000.1 等同于 ISO 10646.1)。
GB13000 等同于国际标准的《通用多八位编码字符集 (UCS)》 ISO10646.1,就是等同于 Unicode 的标准,代码页等等的都使用 UTF 的一套标准。
历史上存在两个独立的尝试创立单一字符集的组织,即国际标准化组织(ISO)和多语言软件制造商组成的统一码(Unicode)联盟。前者开发的 ISO/IEC 10646 项目,后者开发的统一码项目。因此最初制定了不同的标准。1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。
4.4 Unicode 和 UTF
Unicode 只是一个用来映射字符和数字的标准。至于字符怎样被编码成内存中的字节,由 UTF(Unicode Transformation Formats) 定义,Unicode 本身并不关心。
UTF-8、UTF-16 是两个最流行的 Unicode 编码方案。
也就是说,Unicode 只是个标准,UTF-8 、 UTF-16 是Unicode标准的实现方式,不要混~
4.5 编码方案
ASCII 是典型的单字节编码方案,即使用单个字节(8 bit)表示一个字符。它占用了一个字节的低 7 位,提供 128 个字符的编码。
EASCII 也是单字节编码方案,由 ASCII 扩充而来,它把 ASCII 没用到的最高位也用了,即占用了单个字节的全 8 位,支持 256 个字符。
GB2312 和 GBK 都是双字节编码方案,将两个字节连在一起表示一个字符。不同的是,GB2312 要求两个字节都 >127(最高 bit 位为 1 );GBK 只要求两个字节中的高字节 >127。
GB2312 把 ASCII 字符集里的可显示字符也给重新编了双字节的码,双字节编码的字符就是我们常说的“全角”,ASCII 里的单字节编码的字符就是“半角”。
GB18030 是一二四字节变长编码方案,使用1个/2个/4个字节来表示一个字符,其中单字节编码部分与 ASCII 兼容,双字节编码部分与 GBK 兼容,四字节编码部分为 GB18030 新增规则。
UTF-8 是变长多字节编码,可以使用 1-6 个字节表示一个 Unicode 字符。方案:
- 单字节的字符,则字节的最高位设为0,对于英语文本,UTF-8 码只占用一个字节,和 ASCII 码完全相同;
- n个字节的字符(n>1),第一个字节(高字节)的前n位设为1,第n+1位设为0,后面每一个字节的前两位都设为10,这n个字节的其余空位填充该字符的 Unicode 码,高位不足补 0 。即:
0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
......
五、参考资料
- ASCII码表
- ASCII & EASCII Table and Description
- GB2312码表
- GBK码表
- GBK编码表
- gb18030-百度百科
- gb18030编码
- 汉字字符集编码查询工具
- Unicode 10.0 Character Code Charts
- Unicode Han Database (Unihan)
- CJK Compatibility Ideographs
- Character encodings for beginners
- The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)
- What's the difference between encoding and charset?