将会涉及到Unicode、UTF-8、UTF-16编码问题,可以参考网络上的相关介绍,这里试图以相对总结的方式来论述这些知识,并提供C的编码实现。
字符编码
jskyzero
2017/02/06
Unicode
Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。
很多传统的编码方式都有一个共同的问题,即容许电脑处理双语环境(通常使用拉丁字母以及其本地语言),但却无法同时支持多语言环境(指可同时处理多种语言混合的情况)。而统一码为每一个字符而非字形定义唯一的代码(即一个整数)。换句话说,统一码以一种抽象的方式(即数字)来处理字符,并将视觉上的演绎工作(例如字体大小、外观形状、字体形态、文体等)留给其他软件来处理,例如网页浏览器或是文字处理器。
在表示一个Unicode的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符
编码和实现
统一码的编码方式与ISO 10646的通用字符集概念相对应。目前实际应用的统一码版本对应于UCS-2,使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示2^16(即65536)个字符。基本满足各种语言的使用。实际上当前版本的统一码并未完全使用这16位编码,而是保留了大量空间以作为特殊使用或将来扩展。
上述16位统一码字符构成基本多文种平面。最新(但未实际广泛使用)的统一码版本定义了16个辅助平面,两者合起来至少需要占据21位的编码空间,比3字节略少。但事实上辅助平面字符仍然占用4字节编码空间,与UCS-4保持一致。未来版本会扩充到ISO 10646-1实现级别3,即涵盖UCS-4的所有字符。UCS-4是一个更大的尚未填充完全的31位字符集,加上恒为0的首位,共需占据32位,即4字节。理论上最多能表示2^31个字符,完全可以涵盖一切语言所用的符号。
基本多文种平面的字符的编码为U+hhhh,其中每个h代表一个十六进制数字,与UCS-2编码完全相同。而其对应的4字节UCS-4编码后两个字节一致,前两个字节则所有位均为0。
Unicode转换格式
Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)
例如,如果一个仅包含基本7位ASCII字符的Unicode文件,如果每个字符都使用2字节的原Unicode编码传输,其第一字节的8位始终为0。这就造成了比较大的浪费。对于这种情况,可以使用UTF-8编码,这是一种变长编码,它将基本7位ASCII字符仍用7位编码表示,占用一个字节(首位补0)。而遇到与其他Unicode字符混合的情况,将按一定算法转换,每个字符使用1-3个字节编码,并利用首位为0或1进行识别。这样对以7位ASCII字符为主的西文文档就大幅节省了编码长度(具体方案参见UTF-8)。类似的,对未来会出现的需要4个字节的辅助平面字符和其他UCS-4扩充字符,2字节编码的UTF-16也需要通过一定的算法进行转换。
再如,如果直接使用与Unicode编码一致(仅限于BMP字符)的UTF-16编码,由于每个字符占用了两个字节,在麦金塔电脑(Mac)机和个人电脑上,对字节顺序的理解是不一致的。这时同一字节流可能会被解释为不同内容,如某字符为十六进制编码4E59,按两个字节拆分为4E和59,在Mac上读取时是从低字节开始,那么在Mac OS会认为此4E59编码为594E,找到的字符为“奎”,而在Windows上从高字节开始读取,则编码为U+4E59的字符为“乙”。就是说在Windows下以UTF-16编码保存一个字符“乙”,在Mac OS环境下打开会显示成“奎”。此类情况说明UTF-16的编码顺序若不加以人为定义就可能发生混淆,于是在UTF-16编码实现方式中使用了大端序(Big-Endian,简写为UTF-16 BE)、小端序(Little-Endian,简写为UTF-16 LE)的概念,以及可附加的字节顺序记号解决方案,目前在PC机上的Windows系统和Linux系统对于UTF-16编码默认使用UTF-16 LE。
UTF-8
UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。
UTF-8使用一至六个字节为每个字符编码(尽管如此,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节):
- 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
- 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。
- 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。
- 其他极少使用的Unicode 辅助平面的字符使用四至六字节编码(Unicode范围由U+10000至U+1FFFFF使用四字节,Unicode范围由U+200000至U+3FFFFFF使用五字节,Unicode范围由U+4000000至U+7FFFFFFF使用六字节)。
编码
对于UTF-8编码中的任意字节B,如果B的第一位为0,则B独立的表示一个字符(ASCII码); 如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符); 如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节; 如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节; 如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节;
UTF-16
UTF-16是Unicode字符编码五层次模型的第三层:字符编码表(Character Encoding Form,也称为"storage format")的一种实现方式。即把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位,需要1个或者2个16位长的码元来表示,因此这是一个变长表示。
UTF-16可看成是UCS-2的父集。在没有辅助平面字符(surrogate code points)前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。
UTF-16的编码模式
UTF-16的大尾序和小尾序存储形式都在用。一般来说,以Macintosh制作或存储的文字使用大尾序格式,以Microsoft或Linux制作或存储的文字使用小尾序格式。 为了弄清楚UTF-16文件的大小尾序,在UTF-16文件的开首,都会放置一个U+FEFF字符作为Byte Order Mark(UTF-16LE以FF FE代表,UTF-16BE以FE FF代表),以显示这个文本文件是以UTF-16编码,其中U+FEFF字符在UNICODE中代表的意义是ZERO WIDTH NO-BREAK SPACE,顾名思义,它是个没有宽度也没有断字的空白。
辅助平面中的码位从U+10000到U+10FFFF,共计FFFFF个,即220=1,048,576个,需要20位来表示。如果用两个16位长的整数组成的序列来表示,第一个整数(称为前导代理)要容纳上述20位的前10位,第二个整数(称为后尾代理)容纳上述20位的后10位。还要能根据16位整数的值直接判明属于前导整数代理的值的范围(210=1024),还是后尾整数代理的值的范围(也是210=1024)。因此,需要在基本多语言平面中保留不对应于Unicode字符的2048个码位,就足以容纳前导代理与后尾代理所需要的编码空间。这对于基本多语言平面总计65536个码位来说,仅占3.125%. 由于前导代理、后尾代理、BMP中的有效字符的码位,三者互不重叠,搜索是简单的:一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16是自同步(self-synchronizing):可以通过仅检查一个码元就可以判定给定字符的下一个字符的起始码元.
C语言描述-实际演练
- UCS-2_to_UTF-8
#include <stdio.h>
#define UCS_SIZE 2
#define UTF_SIZE 3
void UCS_2_to_UTF_8(FILE * fp) {
FILE * out = fopen("UTF-8", "w"); // write to target
char UCS[UCS_SIZE]; // store UCS-2
char UTF[UTF_SIZE]; // store UTF-8
size_t UTF_length; // store UTF-8 length
unsigned long Unicode; // store Unicde
// check the file is Bid Endian or Little Endian
// Bid Endian : FE FF Little Endian FF FE, true means Bid Endian
int endian = ((unsigned char)fgetc(fp) < (unsigned char)fgetc(fp));
while (!feof(fp)) {
// for every word, read and write to file
for (size_t i = 0; i < UCS_SIZE && !feof(fp); i++) {
UCS[i] = fgetc(fp); // store every two byte in a char type
}
if (feof(fp)) break;
// change UCS-2 to Unicode
Unicode = 0;
if(endian) {
for (size_t i = 0; i < UCS_SIZE; i++)
Unicode = (Unicode << 8) + (unsigned char)UCS[i];
} else {
for (size_t i = UCS_SIZE-1; i >= 0; i--)
Unicode = (Unicode << 8) + (unsigned char)UCS[i];
}
//change Unicode to UTF-8
if ( Unicode <= 0x0000007F ) {
// * U-00000000 - U-0000007F: 0xxxxxxx
*UTF = (Unicode & 0x7F);
UTF_length = 1;
} else if ( Unicode >= 0x00000080 && Unicode <= 0x000007FF ) {
// * U-00000080 - U-000007FF: 110xxxxx 10xxxxxx
*(UTF+1) = (Unicode & 0x3F) | 0x80;
*UTF = ((Unicode >> 6) & 0x1F) | 0xC0;
UTF_length = 2;
} else if ( Unicode >= 0x00000800 && Unicode <= 0x0000FFFF ) {
// * U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
*(UTF+2) = (Unicode & 0x3F) | 0x80;
*(UTF+1) = ((Unicode >> 6) & 0x3F) | 0x80;
*UTF = ((Unicode >> 12) & 0x0F) | 0xE0;
UTF_length = 3;
}
// write UTF-8 to file
if(endian) {
for (size_t i = 0; i < UTF_length; i++) {
fputc((unsigned char)UTF[i], out);
}
} else {
for (size_t i = UTF_length-1; i >= 0; i--) {
fputc(UTF[i], out);
}
}
}
fclose(out);
return;
}
int main(int argc, char * argv[]) {
if(argc != 2) {
printf("Usage: ./UCS_2_to_UTF_8 <file> \n");
return -1;
}
FILE * fp = fopen(argv[1], "r"); // read from file
if(!fp) { // check whether the file can be open
printf("File opening failed\n");
return -1;
}
UCS_2_to_UTF_8(fp);
fclose(fp);
return 0;
}
- UTF-8_to_UCS-2
#include <stdio.h> // for FILE fopen feof fclose
#define UCS_SIZE 2
#define UTF_SIZE 3
void UTF_8_to_UCS2(FILE * fp ) {
FILE * out = fopen("UCS-2", "w"); // write to target
char UCS[UCS_SIZE] = { 0xFE, 0xFF }; // store UCS-2
char UTF[UTF_SIZE]; // store UTF-8
size_t UTF_length; // store UTF-8 length
unsigned long Unicode; // store Unicde
int break_flag = 0;
// out the file Bid Endian or Little Endian
// Bid Endian : FE FF Little Endian FF FE, this is Bid Endian
fputc((unsigned char)UCS[0], out);
fputc((unsigned char)UCS[1], out);
while (!feof(fp)) {
Unicode = 0;
// for every word, read from file and change to Unicode
UTF[0] = fgetc(fp);
if (0 == ((unsigned char)UTF[0] >> 7)) {
// 0zzzzzzz(00-7F)
UTF_length = 1;
Unicode = (unsigned char)UTF[0];
} else if (6 == ((unsigned char)UTF[0] >> 5)) {
// 110yyyyy(C0-DF) 10zzzzzz(80-BF)
UTF_length = 2;
UTF[1] = fgetc(fp);
Unicode = (((unsigned char)UTF[0] & 0x1f) << 6)
+ ((unsigned char)UTF[1] & 0x3f);
} else if(14 == ((unsigned char)UTF[0] >> 4)) {
// 1110xxxx(E0-EF) 10yyyyyy 10zzzzzz
UTF_length = 3;
UTF[1] = fgetc(fp);
UTF[2] = fgetc(fp);
Unicode = (((unsigned char)UTF[0] & 0xf) << 12)
+ (((unsigned char)UTF[1] & 0x3f) << 6)
+ ((unsigned char)UTF[2] & 0x3f);
} else break;
UCS[0] = (Unicode & 0xff00) >> 8;
UCS[1] = (Unicode & 0xff);
fputc((unsigned char)UCS[0], out);
fputc((unsigned char)UCS[1], out);
}
fclose(out);
return;
}
int main(int argc, char * argv[]) {
if(argc != 2) {
printf("Usage: ./UTF-8_to_UCS2 <file> \n");
return -1;
}
FILE * fp = fopen(argv[1], "r"); // read from file
if(!fp) { // check whether the file can be open
printf("File opening failed\n");
return -1;
}
UTF_8_to_UCS2(fp);
fclose(fp);
return 0;
}