字符编码知识简介和iconv函数的简单使用
字符编码知识简介
我们知道,在计算机的世界其实只有0和1。期初计算机主要用于科学计算,而我们知道一个数,除了用我们常用对10进制表示,也可以用2进制表示,所以只有0和1就可以进行科学计算,但是为了便于计算,大神们还是向计算机中引入的编码,比如通常我们用补码表示一个负数。所以编码这个东西,是从一开始就伴随着计算机的。到现在,我们的生活已经完全离不开计算机了,计算机也不仅仅用于科学计算了,更多地应用系信息处理。那计算机怎样表示与我们生活息息相关的事物呢,一个直接的办法就是编码。比如计算机中只有0和1,没有文字,那么我们就想办法用0和1的序列来代表文字,这就是文字编码。
ASCII编码
计算机这东西是美国人发明的,所以美国人也最先用0和1的序列给英文字母进行了编码(当然还有一些特殊字符或者用于控制字符)。英文只有26个字母,在加上那些特殊字符,也不多。所以美国人选择用8个0或1的序列来表示一个英文字母或者那些特殊字符。这就是ASCII码。
ASCII码一共规定了128个字符的编码,比如空格"SPACE"是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。
ISO-8859-1编码
计算机发展很快,很快欧洲人也开始尝试编码自己的文字,欧洲的语言大多都是拉丁语系的,和英语很像,而且部分重复,所以欧洲人就想到利用ASCII码没有利用的那一位来编码。所以ISO-8859-1仍采用单字节编码(8位),只是将ASCII没有利用的128个位置利用了起来。而且ISO-8859-1在设计时,前7为和ASCII码一致,也就是说ISO-8859-1是完全兼容ASCII的。
GB2312编码
很快,我们国家开始为汉字编码,由于汉字和拉丁系的文字完全不同,而且汉字的个数很多,所以如果像欧洲的ISO-8859-1那样,只利用ASCII没有利用的部分,只能多表示128个字符,而汉字的数量远远大于这个数,所以用单字节编码汉字是不可行的。于是我们国家在1980年发布了GB2312编码标准,采用两个字节来编码汉字。另外GB2312的两个字节中的每个字节都大于80H(ASCII码不超过80H),所以如果发现一个字节小于80H,那就按ASCII编码解析,如果大于80H,那就按照GB2312编码,两个字节一起处理。所以GB2312和ASCII码是没有冲突的,也可以理解为:GB2312是采用变长编码,单子节编码和ASCII完全相同,双子节则用于汉字的编码,而且双字节中的每个自己都不会和ASCII吗冲突。可以说GB2312是完全兼容ASCII编码的。
GBK编码
GB2312采用双字节编码汉字,双字节也就是16位,最多可以编码65536个字符,但是上面提到了,为了不和ASCII产生冲突,GB2312的每个字节都要大于80H,所以实际上可以利用的就只有2*7=14位,也就是128*128=16384,再加上很多编程语言中,都用0表示字符串的结束,所以编码的时候,全0就只能表示’\0’,即使0的个数不一样也不行,所以实际上可以利用的又缩小了,只有127*127=16129个,远小于65535。实际上GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时,GB 2312收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符,也就是说,可以利用的16129个位置也没有全部利用。据说,朱镕基的“镕”字并没有收录到GB2312中。为了表示更多的字符,GBK编码在BG2312的基础之上,加入了对更多字符的编码,利用的就是GB2312中没有利用的部分,而且,GB2312已经利用的部分,GBK和它保持一致,也就是说GBK是完全兼容GB2312的。可以认为GBK就是GB2312的超集。
Unicode字符集
前面提到了ISO-8859-1编码,是利用了ASCII没有利用的部分进行编码,但是,法国人用那部分表示法语,意大利人用那部分表示意大利语……这样由于同样的二进制序列表示的含义却不同,这样,用意大利人的编码解释法语就会乱码。所以大神们又想出了一种统一的编码方式,也就是说一个二进制序列表示的编码是唯一的,不会像ISO-8859-1那样同样的二进制序列即表示法语也表示意大利语。这就是Unicode。
UCS-2和UCS-4编码
需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。比如,汉字"严"的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
为了解决怎样存储Unicode字符集,大神们又提出了两类方式编码方式存储Unicode字符集:定长方式和变长方式。定长方式就是UCS-2和UCS-4,分别用2个字节和4个字节存储。虽然Unicode字符集中对应的二进制数很多超过了2个字节,但是大部分常用的字符对应的二进制数都在2个字节内,所以常用的就是UCS-2。由于采用定长(而且非单字节)所以UCS-2和UCS-4并不兼容ASCII码,所以一段用ASCII编码的英文,如果用USC-2的方式解码,也不行。因为一个是单字节为单位,一个是双子节为单位(前面的GBK,只是用双字节编码汉字,对于半角的英文字母还是和ASCII一样,用单字节编码)。
UTF-8编码
UTF-8是变长字节的Unicode编码方式。
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种Unicode的实现方式。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。
UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
跟据上表,解读UTF-8编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面,还是以汉字"严"为例,演示如何实现UTF-8编码。
已知"严"的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此"严"的UTF-8编码需要三个字节,即格式是"1110xxxx 10xxxxxx 10xxxxxx"。然后,从"严"的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,"严"的UTF-8编码是"11100100 10111000 10100101",转换成十六进制就是E4B8A5。
总结一下:
GBK向下兼容GB2312,GB2312兼容ASCII
UTF-8 兼容 ASCII
UCS-2不兼容ASCII
所以,如果只是英文,GB2312、GBK、UTF-8都一样,但是和UCS-2不一样。
iconv函数简单使用
在linux C中可以使用iconv相关函数进行字符编码的转换。
一共用到三个相关的函数:
iconv_t iconv_open(const char *tocode, const char *fromcode); size_t iconv(iconv_t cd, char **inbuf, size_t *inbytesleft, char **outbuf, size_t *outbytesleft); int iconv_close(iconv_t cd);
三个函数的声明都在头文件<iconv.h>中
inconv_open函数
iconv_t iconv_open(const char *tocode, const char *fromcode);
iconv_open函数用于创建一个转换描述符,参数为目的编码格式和源编码格式。
int iconv_close函数
int iconv_close(iconv_t cd);
iconv_close用于回收iconv_open分配的资源。
iconv函数
size_t iconv(iconv_t cd,
char **inbuf, size_t *inbytesleft,
char **outbuf, size_t *outbytesleft);
iconv函数用于转换,参数为iconv_open 产生的转换描述符、输入字符串、输入字符串的长度、输出字符串、输出字符串的长度。由于该函数中,参数的值都要发生变化,所以都是传递的指针。
iconv函数的处理过程有点类似于libz中的那个函数。iconv每次转换一个多字节字符(总之就是一次转换一个字符,但是由于编码不同,一个字符占的字节数也不同)。每转换一次都会增加 *inbuf、减少*inbytesleft,增加和减少的量都是被转换的字节数;每一次转换也会增加*outbuf、减少* outbytesleft,增加和减少的量都是转换成的字节数;每次转换也都会更新转换描述符cd。注意转换函数一次转换过程中可能没有任何输出,比如待转换的字符的字节没有全部到来的情况。
由于*inbuf和*outbuf都会变化,所以在调用iconv之前应当将这两个值复制一份,以便以后释放空间和返回正确的字符串。
iconv函数可能会因为下面四种情况而终止:
1、 发现非法的多字节序列,比如说发现不符合编码格式的序列,比如GBK要求每个字节都大于80H,但是发现了一个小于80H的字节,就是非法的,这个时候该函数就会停止并返回-1,而且设置errno为EILSEQ。
2、 输入的字节序列被全部转换,这个时候*inbytesleft已经减到了0,此时返回不可逆的转换的字节数(不太明白,反正不会是-1)
3、 出现了不完整的多字节序列,而且到了输入序列的结尾。什么意思呢,就是,比如说,UTF-8编码,第一个字节表明整个字符需要三个字节进行编码,但是只发现了两个字节,输入序列就结束了。此时返回-1,并且设置errno为EINVAL ,而且*inbuf会指向那个不完整的多字节序列的起始地址。
4、 输出缓冲区没有多余的空间了。此时返回-1,并且设置errno为E2BIG
一个源码:
代码的作用是:从文件中读取字符串(编码格式为UCS-2),转换成UTF-8编码。
#include <stdio.h> //标准输入输出头文件 #include <sys/stat.h> //stat结构体和stat函数所在的头文件 #include <sys/types.h> //基本系统数据类型 #include <iconv.h> #include <string.h> #include <stdlib.h> #include <errno.h> /**利用stat函数和stat结构体获取普通文件的长度 * 不用打开文件,访问文件的实际数据部分,只需访问文件的inode节点 * 效率较前面一个函数高 * 可以通过struct stat判断文件是否为普通文件,避免目录 * 成功返回长度,失败返回-1 * */ off_t get_flen(char *file_path) { struct stat st_buffer; int err = stat(file_path,&st_buffer); if(err != 0 || !S_ISREG(st_buffer.st_mode)) { perror("读取文件状态出错或文件不是普通文件"); return -1; } return st_buffer.st_size; } char *to_utf(char *src, size_t src_len,const char * toencode,const char *fromencode) { iconv_t cptr = iconv_open(toencode,fromencode); if(cptr == (iconv_t)-1) { printf("并不支持这种方式\n"); return NULL; } size_t out_len = 2 * src_len; char *out = (char *)malloc(out_len); if(out == NULL) { iconv_close(cptr); return NULL; } memset(out,0,out_len); char *dest = out; size_t err = -1; size_t inlen = src_len; char *in = src; err = iconv(cptr,&in,&inlen,&out,&out_len); if(err != (size_t)-1 ) { iconv_close(cptr); return dest; } free(dest); iconv_close(cptr); return NULL; } int main(int argc, char *argv[]) { off_t len = get_flen(argv[1]); if(len == -1) return -1; printf("文件的长度为:%zd\n",len); FILE *fp = fopen(argv[1],"r"); if(fp == NULL) { printf("文件打开失败!\n"); return -1; } void *src = NULL; src = calloc(1, len + 1); if(src == NULL) { fclose(fp); return -1; } void *src_s = src; if((size_t)len != fread(src,1,len,fp)) { printf("读取文件有问题\n"); free(src_s); fclose(fp); return -1; } printf("文件中转换前为:%s\n",src); //上面的代码都是从文件读入字符 char *out = NULL; out = to_utf((char *)src,(size_t)len,"UTF-8","UCS-2"); free(src_s); if(out == NULL) { fclose(fp); return -1; } printf("转换后为:%s\n",out); fclose(fp); free(out); return 0; }