Unicode再探

原创
2016/11/08 13:05
阅读数 175

简介

为什么需要编码呢?我们知道计算机内部使用高低电位表示0,1通过0,1组合来表示数字,但是我们看到的都是图形,不管是图片,英文字母,中文文字,数字都是图形。我们之所以看见图形,是因为计算机的显示系统把计算机内部的0,1转换成了图形,然后通过像电子枪这样的工具把图形绘制到屏幕上。但是0,1和图形之间的对应关系应该是怎么的呢?这显然需要一个大家都遵守的规则来转换,这个规则就是编码。就是数字和图形的对应关系。例如,ASCII码中就使用48这个数字来表示字符(图形)"0"。在屏幕上我们看到的是"0",但是在计算机内部存储的却是110000(高低电位)这样的二进制。 那么问题又来了,刚刚开始的时候的统一是有局限的啊,比如ASCII只规定了字符"a"的编码是1100001,但是中国人要使用的文字,比如说"中"字,该存储为什么呢?所以中国人就自己弄了自己的字符集编码GB2312,GB18030等。当然要利用这些编码是需要图形系统的支持,就比如说"中"字,我随便弄一个编码,那也得图形能够绘制"中"这个图形对吧? 现在很多国家都有自己的字符集编码了,但是问题又来了,现在都流行国际化的套路了,的跟上时代的脚步啊,这样你有你的编码,我有我的编码不统一就是最大的问题,因为同样的一个编码xxxxxxxx在不同的编码系统中可能对应的是不同的图形,这显然是不利于交流的,所以就有了Unicode。如果大家都使用Unicode那么数字和图形就是一对一的关系。 Unicode包含更大的字符集(图形),也就意味着要使用更多的0,1来区分编码(更多的字节)。这样有的人肯定就不愿意了,比如说老美,老美肯定会想本来我本来1个字节能搞定的事情为什么要用2字节或者4字节。Unicode的设计者显然也考虑到了这样的问题,所以把Unicode的字符集设计的很巧妙,只需要一些特殊的转换方式就能避免这样的浪费。这些巧妙的转换方式就包括了UTF-8这样的编码方式。

##几个基本概念

Unicode

Unicode是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案,也是一个字符集。Unicode只是规定如何编码,并没有规定如何传输、保存这个编码。 Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。从这个名称中我们也可以看出Unicode也是一个字符集。UCS又分为UCS-2和UCS-4。

UCS-2

UCS-2就是用两个字节编码,UCS-2有2^16=65536个码位

UCS-4

UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码,UCS-4有2^31=2147483648个码位。 UCS-4根据最高位为0的最高字节分成2^7=128个group(组)。每个group再根据次高字节分为256个plane(平面)。每个plane根据第3个字节分为256 rows(行),每行包含256个cells(单元)。当然同一行的cells只是最后一个字节不同,其余都相同。 group 0的plane 0被称作Basic Multilingual Plane, 即BMP。或者说UCS-4中,高两个字节为0的码位被称作BMP。 Unicode标准计划使用group 0 的17个平面: 从平面0到平面16,即数字0-0x10FFFF。 每个平面有2^16=65536个码位。Unicode计划使用了17个平面,一共有 1765536=1114112(10FFFF)个码位。其实,现在已定义的码位只有238605个,分布在平面0、平面1、平面2、平面14、平面15、平面16。其中平面15和平面16上只是定义了两个各占65534个码位的专用区(Private Use Area),分别是0xF0000-0xFFFFD和0x100000-0x10FFFD。所谓专用区,就是保留给大家放自定义字符的区域,可以简写为 PUA。平面0也有一个专用区:0xE000-0xF8FF,有6400个码位。平面0的0xD800-0xDFFF,共2048个码位,是一个被称作代理区(Surrogate)的特殊区域。238605-655342-6400-2408=99089。余下的99089个已定义码位分布在平面0、平面 1、平面2和平面14上,它们对应着Unicode目前定义的99089个字符,其中包括71226个汉字。平面0、平面1、平面2和平面14上分别定义 了52080、3419、43253和337个字符。平面2的43253个字符都是汉字。平面0上定义了27973个汉字。

BMP

将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。

UTF

UTF是"UCS Transformation Format"的缩写。UCS表示字符集,是字符和数字编码的对应集合。UTF是从字面意思来看就是UCS字符集的转换格式,就是UCS字符集编码的一种转换的编码方式,Unicode已经有数字编码了,为什么要转换格式呢?答案是为了节约存储空间和提高传输效率。就以UTF8为例,编码一个ascii字符UTF8使用1个字节,如果不使用任何技巧,直接存Unicode码,USC-2需要2个字节,USC-4需要4个字节。既然有的节省了空间,那么肯定就有需要花跟多空间的,不过UTF转换之后的一般情况下是能够节省更多空间的。所以为了兼容和国际化我们使用Unicode字符集,为了节省空间我们使用UTF编码。后面会详细介绍几个重要的UTF编码方式。

UCD

UCD是Unicode字符数据库(Unicode Character Database)的缩写。UCD由一些描述Unicode字符属性和内部关系的纯文本或html文件组成。

Block

Block是Unicode字符的一个属性。属于同一个Block的字符有着相近的用途。Block表中的开始码 位、结束码位只是用来划分出一块区域,在开始码位和结束码位之间可能还有很多未定义的码位。

Script

Unicode中每个字符都有一个Script属性,这个属性表明字符所属的文字系统。有两个Script值有着特殊的含义: Common:Script属性为Common的字符可能在多个文字系统中使用,不是某个文字系统特有的。例如:空格、数字等。 Inherited:Script属性为Inherited的字符会继承前一个字符的Script属性。主要是一些组合用符号,例如:在“组合附加符号”区(0x300-0x36f),字符的Script属性都是Inherited。

##UTF编码方式

UTF-8

  UCS-4 range (hex.)           UTF-8 octet sequence (binary)
   0000 0000-0000 007F   0xxxxxxx
   0000 0080-0000 07FF   110xxxxx 10xxxxxx
   0000 0800-0000 FFFF   1110xxxx 10xxxxxx 10xxxxxx
   0001 0000-001F FFFF   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
   0020 0000-03FF FFFF   111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
   0400 0000-7FFF FFFF   1111110x 10xxxxxx ... 10xxxxxx

UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与 ASCII编码完全相同。UTF-8编码的最大长度是6个字节。可能其他的一下资料第4字节模板为010000-10FFFF,并且最多就是4个字节,是因为Unicode定义使用的基本只有17个平面,前面已经提到过17个平面每一个平面有65536个码位,所以总共有65536*17=1114112个码位,所以用010000-10FFFF表示即可。 下面通过一个具体的例子来说明一下UTF-8是怎样对Unicode进行编码(转码)的。 我们在windows下用记事本打开输入一个汉字"中",然后使用unicode的编码保存,我们用16进制编辑器打开发现内容为FF FE 2D 4E。记事本为内容加了2个字节FF FE表示字节序,表示使用的是小端机的保存模式。就是说明汉字"中"的实际的Unicode编码为4E 2D。 "中"字的Unicode编码是0x4E2D。0x4E2D在0x0800-0xFFFF之间,使用用3字节模板了: 1110xxxx 10xxxxxx 10xxxxxx。 将0x4E2D转换为二进制是:0100 1110 0010 1101 注意转换过程中高位补0,最后截取低位的x位数字,x的数量就是,上面模板中对应范围x的数量,比如0x0800-0xFFFF范围对应模板有16个x,就截取低16位。然后用这个比特流依次代替模板中的x,得到: 11100100 10111000 10101101,即"中"的UTF-8编码为:E4 B8 AD UTF-8为什么编码最大长度为6字节呢?这显然是精心设计过的,6字节模板中x的位数为31位,刚好可以把USC-4表示完。每一个范围也是计算过的,转换的二进制位数,绝对不会超过模板中x的位数。

UTF-16

UTF-16编码不是只使用16位来编码,而是以16位无符号整数为单位进行编码。我们把Unicode编码记作U。编码规则如下:

  1. 如果U<0x10000(65536),U的UTF-16编码就是U对应的16位无符号整数
  2. 如果U≥0x10000(65536),我们先计算U=U-0x10000(65536)然后将U写成二进制形式:

yyyy yyyy yyxx xxxx xxxx, U的UTF-16编码(二进制)就是: 110110yyyyyyyyyy 110111xxxxxxxxxx。

为什么U(Unicode的编码值)可以被写成20个二进制位呢?在Unicode现在定义最常用的的17个平面中的最大码位是0x10ffff(1114111),减去0x10000后,U的最大值是0xfffff,每一个16进制为可以使用4个二进制位表示,所以肯定可以用20个二进制位表示。 我们还是使用"中"字为例,来说明UTF-16编码方式: "中"字的Unicode编码是0x4E2D,0x4E2D小于0x10000,所以UTF-16直接使用0x4E2D就可以了。 对于U≥0x10000的字符我们基本用不到,我尝试了也没有找到打印的方法,如果感兴趣的同学可以尝试找一些这样的字符打印一下,例如:左边一个"口",右边一个"才"这个字(UTF-16 0xD842,0xDFB9),又例如:左边一个"口",右边一个"乙"这个字(UTF-16 0xD842,0xDF99)。 对于Java对于非BMP平面的字符,输出的也不是字符本身,而是字符的代理对(下面介绍)的16进制编码。

代理区(Surrogate)

UTF-16编码对于大于65536(2^16)显然是不能用1个16位无符号的整数表示了,所以使用了2个16位无符号的整数表示,根据UTF-16编码的规则, 第1个16位模板是这样的:

110110 yyyyyyyyyy 这种模板的范围显然是: 110110 0000000000 - 110110 1111111111 (0xD800-0xDBFF)

第2个16位的模板是这样的:

110111 xxxxxxxxxx 这种模板的范围显然是: 110111 0000000000 - 110111 1111111111 (0xDC00-0xDFFF)

然后弄Unicode的那一帮人给(0xD800-0xDFFF)这个合起来的范围起了一个名字叫做代理区(Surrogate), (0xD800-0xDBFF)叫做高位代理,叫做低位代理 其中(0xDB80-0xDBFF)这个区域又被称为高位专用代理,这是因为把(0xDB80-0xDBFF)按照UTF-8的编码方式反编码回去获得的Unicode编码刚好在15,16这2个专用自定义区(Private User Area)

UTF-32

UTF-32编码最简单,因为UCS最多使用4个字节,所以UTF-32编码以32位无符号整数为单位,Unicode的UTF-32编码就是Unicode码值对应的32位无符号整数。

Java Character的一些测试

import org.junit.Test;

public class UTest {

    @Test
    public void testU() {
        char s = '圝';
        System.out.println((int) s);
        // System.out.println((char)(100000));//大于65535的数据被截断了
        System.out.println(Character.MAX_CODE_POINT);
        char c = Character.forDigit(14, 16);
        System.out.println(c);
        //输出指定位置上的unicode的字符,如果不是BMP(简单来说BMP就是unicode<65536)
        //就输出的是UTF-16编码的代理对,2个无符号16位
        char[] chars = Character.toChars(20013);// 4E2D 中
        for (char ch : chars)
            System.out.println(ch);

        chars = Character.toChars(134192);// 0x20C30
        for (char ch : chars) {
            int up = (int) ch;
            System.out.println(Integer.toHexString(up));
        }
        //获取低位代理
        char low = Character.lowSurrogate(0x20C30);
        System.out.println(Integer.toHexString((int) low));
        //获取高位代理
        char high = Character.highSurrogate(0x20C30);
        System.out.println(Integer.toHexString((int) high));

        //知道字符代理对,计算字符unicode编码中的位置
        int codePointAt = Character.codePointAt(intToCharArray(0xD842, 0xDFB9), 0);
        System.out.println(codePointAt);

    }

    public static byte[] charToByte(char c) {
        byte[] b = new byte[2];
        b[0] = (byte) ((c & 0xFF00) >> 8);
        b[1] = (byte) (c & 0xFF);
        return b;
    }

    public static char[] intToCharArray(int high, int low) {
        checkSurrogate(high, low);
        char[] result = new char[2];
        result[0] = (char) high;
        result[1] = (char) low;
        return result;
    }
    
    /**
     * 根据UTF-16编码规则,高位代理范围为:
     * 11011000 00000000到11011011 11111111,即0xD800-0xDBFF
     * 低位代理范围为:
     * 11011100 00000000到11011111 11111111,即0xDC00-0xDFFF
     * @param high
     * @param low
     * @return
     */
    private static void checkSurrogate(int high,int low)
    {
        if(0xD800>high || high>0xDBFF)
            throw new IllegalArgumentException("非法的高位代理:"+high);
        if(0xDC00>low || low>0xDFFF)
            throw new IllegalArgumentException("非法的低位代理:"+low);
    }

}

Unicode好文推荐

展开阅读全文
打赏
0
3 收藏
分享
加载中
更多评论
打赏
0 评论
3 收藏
0
分享
返回顶部
顶部