青儿哥哥 阅读(10) 评论(0)

字符编码知识简介和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;

}