Unicode中的UTF-8,UTF-16,UTF-16LE,UTF-16BE编码及转换
J。。。
原⽂1链接:
原⽂2链接:
⽂章1
最近遇到的⿇烦事
charset⾥的问题, ⼀般我们都⽤unicode来作为统⼀编码, 但unicode也有多种表现形式
⾸先, 我们说的unicode, 其实就是utf-16, 但最通⽤的却是utf-8,
原因: 我猜⼤概是英⽂占的⽐例⽐较⼤, 这样utf-8的存储优势⽐较明显, 因为utf-16是固定16位的(双字节), ⽽utf-8则是看情况⽽定, 即可变长度, 常规的128个ASCII只需要8位(单字节), ⽽汉字需要24位
UTF-16, UTF-16LE, UTF-16BE, 及其区别BOM
同样都是unicode, 为什么要搞3种这么⿇烦?
先说UTF-16BE (big endian), ⽐较好理解的, 俗称⼤头
unicode汉字
⽐如说char 'a', ascii为
0x61, 那么它的utf-8, 则为 [0x61], 但utf-16是16位的, 所以为[0x00, 0x61]
再说UTF-16LE(little endian), 俗称⼩头, 这个是⽐较常⽤的
还是char 'a', 它的代码却反过来: [0x61, 0x00], 据说是为了提⾼速度⽽迎合CPU的胃⼝, CPU就是这到倒着吃数据的, 这⾥⾯有汇编的知识, 不多说
然后说UTF-16, 要从代码⾥⾃动判断⼀个⽂件到底是UTF-16LE还是BE, 对于单纯的英⽂字符来说还⽐较好办, 但要有特殊字符, 图形符号, 汉字, 法⽂, 俄语, ⽕星语之类的话, 相信各位都很头痛吧, 所以, unicode组织引⼊了BOM的概念, 即byte order mark, 顾名思义, 就是表名这个⽂件到底是LE还是BE的,
其⽅法就是, 在UTF-16⽂件的头2个字节⾥做个标记: LE [0xFF, 0xFE], BE [0xFE, 0xFF]
理解了这个后, 在java⾥遇到utf-16还是会遇到⿇烦, 因为要在⽂件⾥⾯单独判断头2个再字节是很不流畅的
⼩结:
Java代码
1. InputStreamReader reader=new InputStreamReader(fin, charset)
1. 如果这个UTF-16⽂件⾥带有BOM的话, charset就⽤"UTF-16", java会⾃动根据BOM判断LE还是BE, 如果你在这⾥指定了"UTF-16LE"或"UTF-16BE"的话, 猜错了会⽣成乱七⼋糟的⽂件, 哪怕猜对了, java也会把头2个字节当成⽂本输出给你⽽不会略过去, 因为[FF FE]或[FE FF]这2个代码没有内容, 所以, windows会⽤"?"代替给你
2. 如果这个UTF-16⽂件⾥不带BOM的话, 则charset就要⽤"UTF-16LE"或"UTF-16BE"来指定LE还是BE的编码⽅式
另外, UTF-8也有BOM的, [0xEF, 0xBB, 0xBF], 但可有可⽆, 但⽤windows的notepad另存为时会⾃动帮你加上这个, ⽽很多⾮windows平台的UTF8⽂件⼜没有这个BOM, 真是难为我们这些程序员啊
错误的例⼦
1. ⽂件A, UTF16格式, 带BOM LE,
InputStreamReader reader=new InputStreamReader(fin, "utf-16le")
会多输出⼀个"?"在第⼀个字节, 原因: java没有把头2位当成BOM
2. ⽂件A, UTF16格式, 带BOM LE,
InputStreamReader reader=new InputStreamReader(fin, "utf-16be")
会出乱码, 原因: 字节的⾼低位弄反了, 'a' 在⽂件⾥ [0x61, 0x00], 但java以为'a'应该是[0x00 0x61]
3. ⽂件A, UTF16格式, 带BOM BE,
InputStreamReader reader=new InputStreamReader(fin, "utf-16le")
会出乱码, 原因: 字节的⾼低位弄反了, 'a' 在⽂件⾥ [0x00, 0x61], 但java以为'a'应该是[0x61 0x00]
4. ⽂件A, UTF16格式, 带BOM BE,
InputStreamReader reader=new InputStreamReader(fin, "utf-16be")
会多输出⼀个"?"在第⼀个字节, 原因: java没有把头2位当成BOM
5. ⽂件A, UTF16格式, LE 不带BOM,
InputStreamReader reader=new InputStreamReader(fin, "utf-16")
会出乱码, 因为utf-16对于java来说, 默认为be(1.6JDK, 以后的说不准)
但windows的notepad打开正常, 因为notepad默认为le, - -#
6. ⽂件A, UTF16格式, BE 不带BOM,
InputStreamReader reader=new InputStreamReader(fin, "utf-16")
恭喜你, 蒙对了
但winodws的notepad打开时, 每个字符中间都多了⼀个" ", 因为notepad把它当成ASNI了
在windows下输出unicode⽂件
通过java出来unicode⽂件, 也容易混淆
Java代码
1. FileOutputStream fout=new FileOutputStream(file);
2. OutputStreamWriter writer=new OutputStreamWriter(fout, charset);
1. charset为"UTF-16"时, java会默认添加BOM [0xFE, 0xFF], 并以BE的格式编写byte
2. charset为"UTF-16BE"时, java不会添加BOM, 但编码⽅式为 BE
3. charset为"UTF-16LE"时, java不会添加BOM, 但编码⽅式为 LE
以上通过 Byte("utf-16"), Byte("utf-16be"), Byte("utf-16le") 可以验证
⽽windows的notepad默认的unicode为 LE, 并带BOM,
所以, 推荐输出 UTF-16LE, 并⼈为添加BOM, 即:
Java代码
1. byte[] bom={-1, -2};    //FF FE, java的byte⽤的是补码, 验证: b=127, b+=1, ⽽b=-128
2. fout.write(bom);
⽂章2
Unicode是制定的编码标准,⽬前得到了绝⼤部分操作系统和的⽀持。官⽅对Unicode的定义是:Unicode provides a unique number for every character。可见,Unicode所做的是为每个字符定义了⼀个相应的数字表⽰。⽐如,“a“的Unicode值是0x0061,“⼀”的Unicde值是0x4E00,这是最简单的情况,每个字符⽤2个字节表⽰。
<定义了百万个以上的字符,如果将所有的字符⽤统⼀的格式表⽰,需要的是4个字节。“a“的Unicode表⽰就会变成0x00000061,⽽“⼀“的Unicode值是0x00004E00。实际上,这就是UTF32,Linux操作系统上所使⽤的Unicode⽅案。
但是,仔细分析可以发现,其实绝⼤部分字符只使⽤2个字节就可以表⽰了。英⽂的Unicode范围是0x0000-0x007F,中⽂的Unicode范围是0x4E00-0x9F**,真正需要扩展到4个字节来表⽰的字符少之⼜少,所以有些系统直接使⽤2个字节来表⽰Unicode。⽐如Windows系统上,Unicode就是两个字节的。对于那些需要4个字节才能表⽰的字符,使⽤⼀种代理的⼿法来扩展(其实就是在低两个字节上做⼀个标记,表⽰这是⼀个代理,需要连接上随后的两个字节,才能组成⼀个字符)。这样的好处是⼤量的节约了存取空间,也提⾼了处理的速度。这种Unicode表⽰⽅法就是UTF16。⼀般在Windows平台上,提到Unicode,那就是指UTF16了。
⾄于UTF16-LE和UTF16-BE,与计算机的CPU构架有关。LE指Little Endian,⽽BE指Big Endian。关
于这⽅⾯的信息,⽹上有很多相关的帖⼦。我们⼀般的X86系统都是Little Endian的,可以认为UTF16=UTF16-LE.
由于对于欧洲和北美,实际上使⽤的编码范围在0x0000-0x00FF之间,只需要⼀个字符就可以表⽰所有的字符。即使是使⽤UTF16来作为内存的存取⽅式,还是会带来巨⼤的空间浪费,因此就有了UTF8的编码⽅式。这是⼀种很灵活的编码,对于只需要1个字节的字符,就使⽤⼀个字节,对于中⽇韩等原本需要两个字节才能表⽰的字符,则通过⼀个UTF16-UTF8的实现相互之间的转换(⼀般需要3个字节才能表⽰),⽽对于需要4个字节才能表⽰的字符,UTF8可以扩展到6个字节每个字符。UTF8使⽤的算法很有意思,⼤致映射关系如下:
UTF-32                                    UTF8
0x00000000 - 0x0000007F          0xxxxxxx
0x00000080 - 0x000007FF          110xxxxx 10xxxxxx
0x00000800 - 0x0000FFFF          1110xxxx 10xxxxxx 10xxxxxx
0x00010000 - 0x001FFFFF          11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
0x00200000 - 0x03FFFFFF          111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
0x04000000 - 0x7FFFFFFF          1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 可以发现这和IP的分址算法很是相像。
由于UTF8可以⽅便的转换为UTF16和UTF32(不需要码表,转换算法可以在上到C)。⽽且UTF8在每个操作系统平台上的实现都是⼀样的,也不存在跨平台的问题,所以UTF8成为跨平台的Unicode很好的解决⽅案。当然,对于中⽂来说,由于每个字符需要3个字节才能表⽰,还是有点浪费的。
UTF8⽂本头为 EF BB BF
UTF16 ⽂本头: Big-Endian的FEFF; 表明这个字节流是;Little-Endian的FFFE
int convertUTF8UTF16(unsigned char* utf8, int& size8, char* utf16, int& size16)
{
int count =0, i;
char tmp1, tmp2;
unsigned short int integer;
unsigned short int *p;
for(i=0;i<size8;i+=1)
{
p = (unsigned short int*)&utf16[i];
if( utf8[count] < 0x80)
{
// <0x80
integer = utf8[count];
count++;
}
else if( (utf8[count] < 0xDF) && (utf8[count]>=0x80))
{
integer = utf8[count] & 0x1F;
integer = integer << 6;
integer += utf8[count+1] &0x3F;
count+=2;
}
else if( (utf8[count] <= 0xEF) && (utf8[count]>=0xDF))
{
integer = utf8[count] & 0x0F;
integer = integer << 6;
integer += utf8[count+1] &0x3F;
integer = integer << 6;
integer += utf8[count+2] &0x3F;
count+=3;
}
else
{
printf("error!/n");
}
*p = integer;
}
size8 = count;
size16 = i;
return size16;
}
int convertUTF16UTF8(char* utf16, int& size16, char* utf8, int& size8) {
int i=0, count=0;
char tmp1, tmp2;
unsigned short int integer;
for(i=0;i<size16;i+=2)
{
integer = *(unsigned short int*)&utf16[i];
if( integer<0x80)
{
utf8[count] = utf16[i] & 0x7f;
count++;
}
else if( integer>=0x80 && integer<0x07ff)
{
tmp1 = integer>>6;
utf8[count] = 0xC0 | (0x1F & integer>>6);
utf8[count+1] = 0x80 | (0x3F & integer);
count+=2;
}
else if( integer>=0x0800 )
{
tmp1 = integer>>12;
utf8[count] = 0xE0 | (0x0F & integer>>12);
utf8[count+1] = 0x80 | ((0x0FC0 & integer)>>6);            utf8[count+2] = 0x80 | (0x003F & integer);
count += 3;
}
else
{
printf("error/n");
}
}
size16 = i;
size8  = count;
return count;
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。