UTF-8解码

要想了解UTF-8编码规则,请参考我的文章:http://blog.csdn.net/sheismylife/article/details/8570015

在我的另一篇文章"UTF-8编码实测"http://blog.csdn.net/sheismylife/article/details/8571726中,我使用了boost::locale库的代码来解码UTF-8. 现在来仔细研究一下解码的算法:


如何分辨leading byte和continuation bytes呢?关键在于任何一个continuation byte都以10开始。下面的函数可以帮助判断是否为continuation byte:

bool is_trail(char ci) {
  unsigned char c = ci;
  return (c & 0xC0) == 0x80;
}
因为0xC0二进制格式是1100 0000,和c按位与后也就是低六位全部设置为0,进保留c的高两位.

而0x80二进制格式是1000 0000, 如果两者相等,说明c的高两位是10,因此c是continuation byte。返回true。

有了这个函数,判断一个字节是否为leading byte也很简单:

bool is_lead(char ci) {
  return !is_trail(ci);
}


再看一下函数trail_length, 该函数通过分析一个leading byte来确定continuation bytes的长度

int trail_length(char ci) {
  unsigned char c = ci;
  if(c < 128)
    return 0;
  if(BOOST_LOCALE_UNLIKELY(c < 194))
    return -1;
  if(c < 224)
    return 1;
  if(c < 240)
    return 2;
  if(BOOST_LOCALE_LIKELY(c <=244))
    return 3;
  return -1;
}

如果c < 128, 说明就是一个ASCII字符,用一个字节表示。因此conitunuation bytes长度为0,utf-8编码总长度为1.

因为11011111就是223,只要c在区间[128, 224), 说明continuation bytes长度为1,utf-8编码总长度为2.

继续推理,11101111等于239, 因此在[224, 240)区间的,continuation bytes长度为2,utf-8编码总长度为3.

11110111等于247, 因此在[240, 248)区间的,continuation bytes长度为3,utf-8编码总长度为4. 但是这里限定的区间实际上是[240, 244] 不明白为什么,可能还有什么编码规则我不清楚。


现在来看一下utf.hpp中的代码,http://www.boost.org/doc/libs/1_53_0/libs/locale/doc/html/utf_8hpp_source.html

00192         template<typename Iterator>
00193         static code_point decode(Iterator &p,Iterator e)
00194         {
00195             if(BOOST_LOCALE_UNLIKELY(p==e))
00196                 return incomplete;
00197 
00198             unsigned char lead = *p++;
00199 
00200             // First byte is fully validated here
00201             int trail_size = trail_length(lead);
00202 
00203             if(BOOST_LOCALE_UNLIKELY(trail_size < 0))
00204                 return illegal;
00205 
00206             //
00207             // Ok as only ASCII may be of size = 0
00208             // also optimize for ASCII text
00209             //
00210             if(trail_size == 0)
00211                 return lead;
00212             
00213             code_point c = lead & ((1<<(6-trail_size))-1);
00214 
00215             // Read the rest
00216             unsigned char tmp;
00217             switch(trail_size) {
00218             case 3:
00219                 if(BOOST_LOCALE_UNLIKELY(p==e))
00220                     return incomplete;
00221                 tmp = *p++;
00222                 if (!is_trail(tmp))
00223                     return illegal;
00224                 c = (c << 6) | ( tmp & 0x3F);
00225             case 2:
00226                 if(BOOST_LOCALE_UNLIKELY(p==e))
00227                     return incomplete;
00228                 tmp = *p++;
00229                 if (!is_trail(tmp))
00230                     return illegal;
00231                 c = (c << 6) | ( tmp & 0x3F);
00232             case 1:
00233                 if(BOOST_LOCALE_UNLIKELY(p==e))
00234                     return incomplete;
00235                 tmp = *p++;
00236                 if (!is_trail(tmp))
00237                     return illegal;
00238                 c = (c << 6) | ( tmp & 0x3F);
00239             }
00240 
00241             // Check code point validity: no surrogates and
00242             // valid range
00243             if(BOOST_LOCALE_UNLIKELY(!is_valid_codepoint(c)))
00244                 return illegal;
00245 
00246             // make sure it is the most compact representation
00247             if(BOOST_LOCALE_UNLIKELY(width(c)!=trail_size + 1))
00248                 return illegal;
00249 
00250             return c;
00251 
00252         }

decode函数负责从字符串中解析一个utf-8编码(可能包含1-4字节),返回对应的code point,一个uint32_t的整数。

总是假定第一个字节是leading byte,然后用trail_length获取continuation bytes的长度,如果为0,说明就是ASCII字符,直接返回。

210行到250行处理的都是非ASCII字符。先注意switch/case用法,这里没有break语句,也就是说,如果trail_size为3,会先执行case 3:里面的语句,然后再依次执行case 2和 case 1(不需要匹配)。这是来自switch/case的特殊语法。参考:http://msdn.microsoft.com/en-<wbr>us/library/k0t5wee3.aspx</wbr>

当然用循环能更好的表达,但是不知道Artyom为什么选择这种写法?性能更高么?至少有一条,如果有代码扫描工具的话,会认为这里三段代码重复,会报警告信息。:)

现在再看一下之前UTF-8编码一文中引用的wiki的例子:

现在看一个来自wiki的例子演示如何对字符€进行UTF-8编码:
step 1:获取€的Unicode code point,是0xU+20AC
step 2:0xU+20AC范围在U+07FF和U+FFFF之间,因此用三字节表示。
step 3:0xU+20AC的二进制码是:10000010101100,14位长,要想表示3字节编码,必须凑成16 bits.因此高位补上两个0,变成2字节16位长:0010000010101100,我下面称为数值串。
step 4: 根据规则,添加一个leading byte,开头是1110,那么这个leading byte还有4个bit需要填充,从数值串高位取4个bit来,leading byte变成了:
11100010,而数值串值为000010101100
step 5: 第一个continuation byte高位应该是10,还缺少6 bits,从数值串中按高位取6 bit,这样第一个continuation byte为:
10000010,而数值串变为101100
step 6: 第二个continuation byte高位也应该是10,还缺少6 bits, 从数值串取6 bits,这样第二个continuation byte为:
10101100
最终编码形成的三字节:
11100010 10000010 10101100
写成16进制就是0xE282AC

解码也就是编码的逆过程,从leading byte中抽取低4位,从两个continuation bytes中都抽取低6位,拼接成16bits的整数,然后转换类型变成uint32_t的整数。

下面的代码其实就是取出低四位:

code_point c = lead & ((1<<(6-trail_size))-1);
这是个很好的技巧,总结一下可以写成一个函数, 函数接受两个参数,一个是要提取bit的x,一个是提取的位数。

uint8_t GetLowNBit(uint8_t x, uint8_t n) {
  return x & ((1<<n)-1);
}

为什么这里用6-trail_size,纯粹是观察出来的规律。

2字节utf-8编码时,leading byte以110开头,因此要提取低5位数值, 6-trail_size=6-1=5,刚好能够提取低5位。

3字节utf-8编码,leading byte以1110开头,因此要提取低4位数值,6-trail_size=6-2=4.

4字节utf-8编码,leading byte以11110开头,因此要提取低3位数值,6-trail_size=6-3=3.

所以这里用6,Artyom的观察力很敏锐。


提取出leading byte的低位数据后,现在要提取continuation bytes的低位数据,也就是那个switch/case的功能。


这就很简单了,tmp & 0x3F就是取出低6位数据,因为continuation byte永远都是10开头。每次取出低6位后,将c左移6位然后按位或,就达到合并bit成新的整数的目的。

前面已经解释过swtich/case在without break的用法。这里就会运行两次,取出后面两个continuation bytes的低6位数据,并合并。


UTF-8解码算法分析完成。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值