美文网首页
你到底行不行,字符集踩坑小记

你到底行不行,字符集踩坑小记

作者: 李眼镜 | 来源:发表于2017-11-23 02:04 被阅读0次

    一、踩坑实录

    关于字符串编码,遇到过一个比较有意思的问题。

    1.1 case背景

    服务 A 请求服务 B 时,参数列表带了一个字符串s="中国银行"。但是请求失败了,服务 B 给服务 A 返回了一个错误信息为 errmsg="s包含特殊字符" 的异常。

    已知信息简述如下:

    1. 服务 A 从上游接收 UTF-8 编码的字符串数据。
    2. 服务 A 和 服务 B 使用 http 通信,A 对字符串做了URLEncoder(GB18030),B 对接收到的字符串做了URLDecoder(GBK)。
    3. 在此之前,也有过从 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 码,以及各个字符集对这两个字的支持情况,如下图:

    1. 日志中拷贝的,即上游传过来的
      日志中拷贝的,即上游传过来的`行`
    1. GBK字符集中拷贝的
      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 从不同的资源中收录字符时遵循两个原则:

    1. 对字不对形原则,即一个字符只收录一次,同字不同形的字符不予多次录入。目的是尽可能的精简收录到 Unicode 内的字符数量。
    2. 字源分离原则,若同一字源收录了同一字符的多个字形,则 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 的 行

    编码为FA08的“行”字字源只有一个 编码为FA08的“行”字字源只有一个,K0指的就是韩国K源(字符集:KS C 5601-1987),参考: K源

    编码为 884C 的 行

    编码884C的“行”字字源有5个

    编码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)

    三、思考和改进

    踩了这么个坑,怎么填呢?可以从两个角度去考虑:

    1. 技术层面,可否提供识别汉字,或者中文汉字的检查能力?
    2. 业务处理流程中,哪些场景会需要做此类检查?

    3.1 技术能力

    技术角度,考虑3个问题:

    1. 能不能识别一个字符是否为汉字?=>能
    2. 能不能识别一个字符是否为中文汉字?=>不能
    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 常常指代字符集合和字符编码。

    1. 一般来说,一个字符集,对应一套编码方案,比如 ASCII、GB2312 。但也有特例,Unicode 字符集对应的编码方案就不止一套:UTF-8、UTF-16等。
    2. 通常我们所说的 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 个字符。

    GB2312GBK 都是双字节编码方案,将两个字节连在一起表示一个字符。不同的是,GB2312 要求两个字节都 >127(最高 bit 位为 1 );GBK 只要求两个字节中的高字节 >127。

    GB2312 把 ASCII 字符集里的可显示字符也给重新编了双字节的码,双字节编码的字符就是我们常说的“全角”,ASCII 里的单字节编码的字符就是“半角”。

    GB18030 是一二四字节变长编码方案,使用1个/2个/4个字节来表示一个字符,其中单字节编码部分与 ASCII 兼容,双字节编码部分与 GBK 兼容,四字节编码部分为 GB18030 新增规则。

    UTF-8 是变长多字节编码,可以使用 1-6 个字节表示一个 Unicode 字符。方案:

    1. 单字节的字符,则字节的最高位设为0,对于英语文本,UTF-8 码只占用一个字节,和 ASCII 码完全相同;
    2. n个字节的字符(n>1),第一个字节(高字节)的前n位设为1,第n+1位设为0,后面每一个字节的前两位都设为10,这n个字节的其余空位填充该字符的 Unicode 码,高位不足补 0 。即:
    0xxxxxxx
    110xxxxx 10xxxxxx
    1110xxxx 10xxxxxx 10xxxxxx
    11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
    ......
    

    五、参考资料

    相关文章

      网友评论

          本文标题:你到底行不行,字符集踩坑小记

          本文链接:https://www.haomeiwen.com/subject/xxyavxtx.html