美文网首页
unicode 与 utf8

unicode 与 utf8

作者: onedam | 来源:发表于2022-03-21 18:09 被阅读0次

    unicode 只有一种定义.给每个符号都分配一个数字. 然后由不同的具体实现.例如
    utf-8 utf-16.... 都是对一种数据结构: byte !!! 字节来编码 规定一种兼容asicc 与现实妥协的方法. 例如utf8 用变长字节,来实现unicode. (伟大的实现) 到现在什么gbk 其他国家的自己编码都基本上绝迹了. 为整个人类节省了劳力.
    (0xffffffff 4294967295 肆拾贰亿玖仟肆佰玖拾陆万柒仟贰佰玖拾伍).
    目前最高位定为0 则0x7fffffff 2147483647 贰拾壹亿 足够任何字符了.所以每年unicode都会更新. 基本上所有通用符号都在unicode了. 例如 麻将 易经 卦图.
    https://fuhaoku.net/block/Mahjong

    Emoji字符是Unicode字符集中一部分,特定形象的Emoji表情符号对应到特定的Unicode字节。

    clojure 代码(clojure内部调用的是java)
          (String. (byte-array [0xF0 0x9F 0x80 0xA1])) => "🀡"  ; 麻将九饼的utf-8编码.
          (Character/codePointAt "🀡" 0) => 127009 (在unicode中的序号)
           大于了0xffffffff 6万5.. 一个char(2个字节)存不下怎么办? java用2个char 4个字节ok了
          "\uD83C\uDC21" =>  "🀡" 
    
          (String. (byte-array [0xF0 0x9F 0x80 0xA4])) => "🀤"
          (String. (byte-array [0xF0 0x9F 0x82 0xA1])) => "🂡"  ; 扑克牌黑桃A
          (int \䷁) => 19905 
    

    注意: 上面程序中 \ 和 java中的 ' 单引号 字面量写法只支持2个字节的编码
    设计JAVA的时候用的是Unicode编码方式,用两个字节的代码宽度=>编码=>世界上所以语言的字符。
    char类型是按照Unicode规范实现的一种数据类型,固定16bit大小。现如今,Unicode字符集已经进行了扩展,表示的范围已经超过了16bit。Unicode字符集的数值范围扩大到了[U+0000,U+10FFFF]。
    https://www.jianshu.com/p/4c29d96d5e06

    十分遗憾的是,现在不可避免的事情发生了,Unicode字符超过了65536个,对此16位的char类型已经不能满足表示所以Unicode字符的需要了。
    (int \🀡 => 的时候出错了. 因为🀡的序号是12万多了. 超过了0xffffffff 65536了.是4个字节了..
    java 采用了大于6万5的时候 编码到4个字节了. char 字面量 litral 是为了方便书写
    超过65536的书写的时候不支持单引号而已了...
    大于的时候怎么办, java 解决方案 也是utf-16 . 用2个char 也就是4个字节. ok了

    UTF-16的编码方法:

    1、如果字符编码U小于0x10000,也就是十进制的0到65535之内,则直接使用两字节表示;

    2、如果字符编码U大于0x10000,由于UNICODE编码范围最大为0x10FFFF,从0x10000到0x10FFFF之间 共有0xFFFFF个编码,也就是需要20个bit就可以标示这些编码。用U'表示从0-0xFFFFF之间的值,将其前 10 bit作为高位和16 bit的数值0xD800进行 逻辑or 操作,将后10 bit作为低位和0xDC00做 逻辑or 操作,这样组成的 4个byte就构成了U的编码。

    每个字符都分配一个数字编号(编码),
    Unicode编码和Unicode实现 这两个是不同的.
    例如
    "冯" 的数字编号(序号)是 20911 16进制0x51af 然后在计算机中对这个数字 来编码

    java中的各种编码 代码实现 sun.nio.cs;


    image.png

    sun.nio.cs.UTF_8 extends Unicode 这个类实现了utf8的算法. 根据前一个字节二进制多少个1 确定有几个自己,然后根据utf-8 规范把 codepoint 扣出来. 找到具体的int 或long 数.

    通俗实现

    https://blog.csdn.net/brk1985/article/details/52126912
    先从字符编码讲起。
    1、美国人首先对其英文字符进行了编码,也就是最早的ascii码,用一个字节的低7位来表示英文的128个字符,高1位统一为0;
    2、后来欧洲人发现尼玛你这128位哪够用,比如我高贵的法国人字母上面的还有注音符,这个怎么区分,得,把高1位编进来吧,这样欧洲普遍使用一个全字节进行编码,最多可表示256位。欧美人就是喜欢直来直去,字符少,编码用得位数少;
    3、但是即使位数少,不同国家地区用不同的字符编码,虽然0--127表示的符号是一样的,但是128--255这一段的解释完全乱套了,即使2进制完全一样,表示的字符完全不一样,比如135在法语,希伯来语,俄语编码中完全是不同的符号;
    4、更麻烦的是,尼玛这电脑高科技传到中国后,中国人发现我们有10万多个汉字,你们欧美这256字塞牙缝都不够。于是就发明了GB2312这些汉字编码,典型的用2个字节来表示绝大部分的常用汉字,最多可以表示65536个汉字字符,这样就不难理解有些汉字你在新华字典里查得到,但是电脑上如果不处理一下你是显示不出来的了吧。
    5、这下各用各的字符集编码,这世界咋统一?俄国人发封email给中国人,两边字符集编码不同,尼玛显示都是乱码啊。为了统一,于是就发明了unicode,将世界上所有的符号都纳入其中,每一个符号都给予一个独一无二的编码,现在unicode可以容纳100多万个符号,每个符号的编码都不一样,这下可统一了,所有语言都可以互通,一个网页页面里可以同时显示各国文字。
    6、然而,unicode虽然统一了全世界字符的二进制编码,但没有规定如何存储啊,亲。x86和amd体系结构的电脑小端序和大端序都分不清,别提计算机如何识别到底是unicode还是acsii了。如果Unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,文本文件的大小会因此大出二三倍,这对于存储来说是极大的浪费。这样导致一个后果:出现了Unicode的多种存储方式。
    7、互联网的兴起,网页上要显示各种字符,必须统一啊,亲。utf-8就是Unicode最重要的实现方式之一。另外还有utf-16、utf-32等。UTF-8不是固定字长编码的,而是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。这是种比较巧妙的设计,如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
    8、注意unicode的字符编码和utf-8的存储编码表示是不同的,例如"严"字的Unicode码是4E25,UTF-8编码是E4B8A5,这个7里面解释了的,UTF-8编码不仅考虑了编码,还考虑了存储,E4B8A5是在存储识别编码的基础上塞进了4E25
    9、UTF-8 使用一至四个字节为每个字符编码。128 个 ASCII 字符(Unicode 范围由 U+0000 至 U+007F)只需一个字节,带有变音符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及马尔代夫语(Unicode 范围由 U+0080 至 U+07FF)需要二个字节,其他基本多文种平面(BMP)中的字符(CJK属于此类-Qieqie注)使用三个字节,其他 Unicode 辅助平面的字符使用四字节编码。
    10、最后,要回答你的问题,常规来看,中文汉字在utf-8中到底占几个字节,一般是3个字节,最常见的编码方式是1110xxxx 10xxxxxx 10xxxxxx。

    UTF-8 编解码实现

    https://taoshu.in/c-utf-8.html

    我们在前文UTF-8往事中提到,Ken 和 Rob 用一个晚上就实现了 UTF-8 编解码的算法。代码非常精炼,很值得一读,分享给大家。

    在开始之前,我们先简单回顾一下 UTF-8 的编码规则。先看编码表:

    UTF-8 编码回顾

    这是 UTF-8 编码规则表:

    Bytes Bits Hex Min  Hex Max  Byte Sequence in Binary
      1     7  00000000 0000007f 0vvvvvvv
      2    11  00000080 000007FF 110vvvvv 10vvvvvv
      3    16  00000800 0000FFFF 1110vvvv 10vvvvvv 10vvvvvv
      4    21  00010000 001FFFFF 11110vvv 10vvvvvv 10vvvvvv 10vvvvvv
      5    26  00200000 03FFFFFF 111110vv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv
      6    31  04000000 7FFFFFFF 1111110v 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv 10vvvvvv
    

    第一列表示编码所需字节数,第二列为能表示的 Unicode 的最大二进制位数,第三列和第四列为能表示的 Unicode 范围,最后一列表示编码后的字节布局。把编码中字母 v 表示的部分连接起来就是对应的 Uniocde 编码。

    还是举一下前文的例子。汉字「吕」的 Unicode 编码是 U+5415,对应二进制为 0101010000010101,总共有 15 位。因为两字节最多表示 11 位,三字节最多表示 16 位,所以使用三字节编码。对应二进制拆成(从低位到高位)三部分,分别是 0101, 010000, 010101,再拼上编码前缀得到 11100101, 10010000, 10010101,对应十六进制为 0xe5, 0x90, 0x95,这就是汉字「吕」的 UTF-8 编码。

    现在我们开始读 c 代码。理解代码的关键是理解数据结构。

    数据结构

    Ken 引入一个 Tab 列表。前面说的编码规则表一共有六条,每条一个 Tab。每个 Tab 包含五个字段。其中 lmasklval 最简单,对应所能表示的 Unicode 的最大值和最小值。Tab 所在的行号代表表示该范围 Unicode 所需要的字节数。剩下的 cmask, cvalshift 则没那么容易理解了。

    还是使用汉字「吕+U5415」讲解。它的 UTF-8 编码是 11100101, 10010000, 10010101。解码的时候,我们先读到第一个字节 11100101,这时算法需要判后面还有几个字节。判断的依据则是当前字节从高位开始连续 1 的个数。对于 11100101 而言,显然其高位连续 1 的数量为 3,但这个事情对于计算机就没有那么「显然」了。Ken 引入两个辅助数字 cmask = 11110000cval = 11100000,让计算机判断 11100101&cmask == cval 是否相等。如果相等,则证明该字节的高四位一定是 1110

    cmaskcval 取值规则非常简单。将 UTF-8 首字节vvv 部分置 0 就得到 cval。将 cval 最高位的 01 就得到了对应的 cmask。以四字节编码为例,首字节为 11110vvvvvv0,得到对应的 cval11110000,也就是 0xf0;将 11110000 的第四位置 1,得到对应了 cmask111110000,也就是 0xf8。其他的依此类推。

    shift 是最不容易理解的。我们放在下面分析 UTF-8 编码算法的时候再讲。

    数据结构代码如下:

    typedef
    struct
    {
      int   cmask;
      int   cval;
      int   shift;
      long  lmask;
      long  lval;
    } Tab;
    
    static
    Tab  tab[] =
    {
      0x80, 0x00, 0*6, 0x7F,       0,         /* 1 byte sequence */
      0xE0, 0xC0, 1*6, 0x7FF,      0x80,      /* 2 byte sequence */
      0xF0, 0xE0, 2*6, 0xFFFF,     0x800,     /* 3 byte sequence */
      0xF8, 0xF0, 3*6, 0x1FFFFF,   0x10000,   /* 4 byte sequence */
      0xFC, 0xF8, 4*6, 0x3FFFFFF,  0x200000,  /* 5 byte sequence */
      0xFE, 0xFC, 5*6, 0x7FFFFFFF, 0x4000000, /* 6 byte sequence */
      0, /* end of table */
    };
    

    弄清楚了数据结构,我们开始看算法。

    解码算法

    解码算法就是将 UTF-8 字节序列转化成 Unicode。代码使用 wchar_t 表示 Unicode。在我的 Mac 上一个 wchar_t 占四个字节。代码请看注释。依然使用「吕+U5415」的 UTF-8 编码 11100101, 10010000, 10010101 进行讲解。

    /* s 指向 UTF-8 字节序列,n 表示字节长度 */
    /* p 指向一个 wchar_t 变量 */
    /* mbtowc 对 s 进行解码,得到的 Unicode 存到 p 指向的变量 */
    int
    mbtowc(wchar_t *p, char *s, size_t n)
    {
      long l;
      int c0, c, nc;
      Tab *t;
    
      if(s == 0)
        return 0;
    
      nc = 0;
      if(n <= nc)
        return -1;
    
      /* c0 保存第一个字节内容,后面会移动 s 指针,此处备份一下 */
      /* 汉字「吕」的编码是 `11100101`, `10010000`, `10010101` */
      /* 此时 l = c0 = 11100101 */
      c0 = *s & 0xff;
      /* l 保存 Unicode 结果 */
      l = c0;
    
      /* 根据 UTF-8 的表示范围从小到大依次检查 */
      for(t=tab; t->cmask; t++) {
        /* nc 以理解为 tab 的行号 */
        /* tab 行号跟这个范围内 UTF-8 编码所需字节数量相同 */
        nc++;
    
        /* c0 指向第一个字节,不会变化 */
        /* l 在 n == 1 和 n == 2 时左移 6 位两次 */
        /* 到 nc == 3 时才会进入该分支 */
        /* 此时的 l 已经是 11100101+010000+010101 了 */
        if((c0 & t->cmask) == t->cval) {
          /* lmaxk 表示三字节能表示的 Unicode 最大值 */
          /* 使用 & 操作,移除最高位的 111 */
          /* 所以 l 最终为 00000101+010000+010101 */
          /* 也就是 l = 0x5415,对应 Unicode +U5415 */
          l &= t->lmask;
    
          if(l < t->lval)
            return -1;
    
          /* 保存结果并反回 */
          *p = l;
          return nc;
        }
    
        if(n <= nc)
          return -1;
    
        /* s 指向下一个字节 */
        s++;
        /* 0x80 = 10000000 */
        /* UTF-8 编码从第二个字节开始高两位都是 10 */
        /* 这一步是为了把最高位的 1 去掉 */
        c = (*s ^ 0x80) & 0xFF;
        /* n == 1 时 */
        /* c = 00010000 */
        /* n == 2 时 */
        /* c = 00010101 */
    
        /* 0xc0 = 1100000 */
        /* 这一上检查次高位是否为 1,如果是 1,则为非法 UTF-8 序列 */
        if(c & 0xC0)
          return -1;
    
        /* c 只有低 6 位有效 */
        /* 根据 UTF-8 规则,l 左移 6 位,将 c 的低 6 位填入 l */
        l = (l<<6) | c;
        /* n == 1 时 */
        /* l 的值变成 11100101+010000 */
        /* n == 2 时 */
        /* l 的值变成 11100101+010000+010101 */
      }
      return -1;
    }
    

    解码算法讲完了。继续说编码算法。

    编码算法

    编码算法就是把 Unicode 转换成 UTF-8 字节序列。还是以汉字「吕+U5415」为例。

    /* wc 存有一个 Unicode */
    /* s 指向存放 UTF-8 的内存 */
    /* wctomb 返回最终 UTF-8 编码的字节长度 */
    int
    wctomb(char *s, wchar_t wc)
    {
      long l;
      int c, nc;
      Tab *t;
    
      if(s == 0)
        return 0;
    
      /* l = wc = 101010000010101 */
      l = wc;
      nc = 0;
      /* tab 每一行表示的 Unicode 的范围是递增的 */
      for(t=tab; t->cmask; t++) {
        nc++; /* 记录行数,也就是字节数 */
        /* 找到第一个可以表示的 Tab */
        /* 对于「吕+U5412」nc == 3 */
        if(l <= t->lmask) {
          /* c->shift = 2 * 6 */
          /* c->cval = 11100000 */
          c = t->shift;
          /* UTF-8 共需要 3 个字节 */
          /* 第 2 个和第 3 个字节各有 6 个有效位 */
          /* 所以将 l 右移 2 * 6 位得到的结果需要存到第一个字节 */
          /* 首字节高 4 位还要存储后续字节长度标识 */
          *s = t->cval | (l>>c);
    
          /* 处理剩余字节 */
          while(c > 0) {
            c -= 6;
            /* s 依次指向下一个字节 */
            s++;
            /* 0x3f = 00111111 */
            /* 0x80 == 10000000 */
            /* c == 0 时,取 l 的低 6 位并将其高两位置成 10 */
            /* 此时 s 指向的位置存有 10010101 */
            /* c == 1 时,取 l 的次低 6 位并将其高两位置成 10 */
            /* 此时 s 指向的位置存有 10010000 */
            *s = 0x80 | ((l>>c) & 0x3F);
          }
    
          /* 最终 s 最初指向的区域存有 11100000, 10010101, 10010000 */
          return nc;
        }
      }
      return -1;
    }
    

    相关文章

      网友评论

          本文标题:unicode 与 utf8

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