iconv字符编码转换全攻略

时间:2022-04-01 23:52:11

        iconv(http://www.gnu.org/software/libiconv/)是一个开源的字符编码转换库,可以“方便”的完成几乎所有的编码转换工作。说简单是因为,它常用的接口就三个,iconv_open  iconv   iconv_close,但是即便是只有三个接口,要想使用正确也不容易。这里把一些基本概念和使用细节记录下来,希望能成为一篇最实用的入门教程。
 
一、字符编码基本概念
      更详细的内容可以参考百度百科(http://baike.baidu.com/view/1204863.htm),或是自行google。这里会记录最核心的几个概念。
      1、ASCII编码,就是英文显示文字所需要的256个字符(比如,英文字母、数字、标点符号等等)

      2、ANSI编码,像中文,肯定不能只用256个字符就代表所有汉字。因此对ASCII码表进行了扩展,使用两个(或多个)字节,代表一个汉字。类似的,不同的国家和地区制定了不同的标准,这些使用 2 个字节来代表一个字符的各种延伸编码方式,称为 ANSI 编码。也就是说,ANSI是一种对ASCII码表进行扩展的泛称,不同语言操作系统,其代表的编码方式不一样。比如中文操作系统,ANSI编码就代指GB2312;日文操作系统ANSI编码就代指JIS。
      
      3、Unicode编码,Unicode是一个超大的集合,也是一个统一的标准,可以容纳世界上的所有语言符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,“汉”这个字的Unicode编码是U+6C49。

      4、代码页(codepage),Unicode是一个世界统一的标准,也就是说,如果一个文本是用Unicode方式编码的,那么它可以同时显示中文、日文、阿拉伯文等等,并且是在任何系统上都可以正常显示的。但是由于ANSI编码之间互不兼容,因此就需要有一个标识来表明不同的ANSI编码到Unicode之间的映射关系(也就是不同编码之间的映射关系),这个就是代码页。比如简体中文的代码页是CP_936(中文系统默认的代码页),这个也就是windows API中MultiByteToWideChar第一个参数所代表的含义。如果不标明代码页,系统是不知道如何进行编码转换的。

    5、SBCS(单字节字符集),MBCS(多字节字符集),DBCS(宽字节字符集),分别对应上面提到的ASCII编码、ANSI编码、Unicode编码。

    6、中文常见编码:GB2312(CP_20936)->GBK(CP_936)->GB18030(CP_54936),三种编码方式向下兼容,也就是说GB18030包含GB2312的所有字符。GB18030在2000年取代GBK成为正式国家标准。

    7、UCS(Unicode Character Set):UCS-2规定了2个字节代表一个文字,还有UCS-4规定了4个字节代表一个文字。我们工作中几乎总是在和UCS-2打交道。

   8、UTF(UCS Transformation Format):UCS只是规定的如何编码,但是没有规定如何传输、保存这个编码。UTF则规定了由几个字节保存这个编码。UTF-7,UTF-8,UTF-16都是比较常见的编码方式。UTF-8编码与Unicode编码并不相同,但是它们之间可以通过计算进行转换,而不像ANSI和Unicode之间必须通过一个映射表来人为规定其对应关系。UTF-16完全对应于UCS-2,并可通过计算代表一部分UCS-4文字。还有UTF-32则是完全对应于UCS-4,不过很不常见就是了。

   9、UTF-8是与ASCII码兼容的,英文字母1个字节,汉字通常是3个字节;UTF-16的所有字符都是用2个字节进行保存,其编码与Unicode是等价的。UTF-16又分为UTF-16LE(little endian)和UTF-16BE(big endian),比如一个字母'a',如果按utf-8来存,就是0x61;如果按utf-16le来存就是0x61 0x00(低有效位在前);如果按utf-16be来存就是0x00 0x61(高有效位在前)。这个也就是我们用记事本另存文件的时候可以选择的几个编码方式的含义。

   10、BOM(byte order mark),上面提到的utf-8 utf-16le utf16-be都是unicode编码,但是系统依然无法正确解析一个文本文件,即便已经知道它是unicode编码。所以就有了这样的规定:在文本文件的最开头插入几个字节的标识,来说明编码方式。utf-8的BOM是0xef 0xbb 0xbf,utf-16le的BOM是0xff 0xfe,utf16-be的BOM是0xfe 0xff。事实上BOM并不是必须的,它仅仅是帮助程序自动判断编码方式使用的,如果我们手动选择编码方式(像ANSI一样),即便没有BOM,也是可以正常显示的。反过来说,程序读文本文件的时候要先读文本开始的三个字节判断下编码方式。


二、iconv支持的编码格式:

European languages
ASCII, ISO-8859-{1,2,3,4,5,7,9,10,13,14,15,16}, KOI8-R, KOI8-U, KOI8-RU, CP{1250,1251,1252,1253,1254,1257}, CP{850,866,1131}, Mac{Roman,CentralEurope,Iceland,Croatian,Romania}, Mac{Cyrillic,Ukraine,Greek,Turkish}, Macintosh
Semitic languages
ISO-8859-{6,8}, CP{1255,1256}, CP862, Mac{Hebrew,Arabic}
Japanese
EUC-JP, SHIFT_JIS, CP932, ISO-2022-JP, ISO-2022-JP-2, ISO-2022-JP-1
Chinese
EUC-CN, HZ, GBK, CP936, GB18030, EUC-TW, BIG5, CP950, BIG5-HKSCS, BIG5-HKSCS:2004, BIG5-HKSCS:2001, BIG5-HKSCS:1999, ISO-2022-CN, ISO-2022-CN-EXT
Korean
EUC-KR, CP949, ISO-2022-KR, JOHAB
Armenian
ARMSCII-8
Georgian
Georgian-Academy, Georgian-PS
Tajik
KOI8-T
Kazakh
PT154, RK1048
Thai
ISO-8859-11, TIS-620, CP874, MacThai
Laotian
MuleLao-1, CP1133
Vietnamese
VISCII, TCVN, CP1258
Platform specifics
HP-ROMAN8, NEXTSTEP
Full Unicode
UTF-8 
UCS-2, UCS-2BE, UCS-2LE 
UCS-4, UCS-4BE, UCS-4LE 
UTF-16, UTF-16BE, UTF-16LE 
UTF-32, UTF-32BE, UTF-32LE 
UTF-7 
C99, JAVA
Full Unicode, in terms of uint16_t or uint32_t (with machine dependent endianness and alignment)
UCS-2-INTERNAL, UCS-4-INTERNAL
Locale dependent, in terms of `char' or `wchar_t' (with machine dependent endianness and alignment, and with OS and locale dependent semantics)
char, wchar_t 
The empty encoding name "" is equivalent to "char": it denotes the locale dependent character encoding.
When configured with the option --enable-extra-encodings, it also provides support for a few extra encodings:
European languages
CP{437,737,775,852,853,855,857,858,860,861,863,865,869,1125}
Semitic languages
CP864
Japanese
EUC-JISX0213, Shift_JISX0213, ISO-2022-JP-3
Chinese
BIG5-2003 (experimental)
Turkmen
TDS565
Platform specifics
ATARIST, RISCOS-LATIN1

    通过第一部分的讲解,这些编码格式应该看着比较清晰了。比如gb2312-->unicode的转化就是GBK(或者是gb18030  cp936,我们之前说过,大多数情况这些是等价的)到ucs-2(或者是utf-16,如果文本信息中没有BOM就要特别指定utf-16le或是utf-16be)的转化。这些就是我们将要用到的编码转换的参数。

三、iconv函数详解
1、iconv_t iconv_open (const char* tocode, const char* fromcode);

如果转换编码不支持(通常是写错了),那么就返回-1,否则返回一个句柄。tocode和fromcode传的就是上面列表中的参数。补充,如果在tocode后面追加"//TRANSLIT"(比如"utf-8//TRANSLIT"),那么如果一个字符无法被转换,则会自动寻找相似字符进行替换。如果追加的是"//IGNORE",则会忽略无法转换的字符。

2、size_t iconv (iconv_t cd,
const char* *
inbuf, size_t * inbytesleft,
char* *
outbuf, size_t * outbytesleft);
        真正用于转换的函数,cd就是iconv_open返回的句柄,要注意,iconv会修改传入的参数,所以要保存好原始outbuf指针。转换完毕后inbuf会指向无法成功转换而被截断的第一个字符,inbutesleft顾名思义就是有多少字符尚未转换,如果全部转换成功当然就是0了,outbuf指向输出缓存的转换后的字符的末尾,outbutesleft表明输出缓存尚有多少自己剩余。
这个函数有很多细节需要注意。如果inbuf中遇到非法字节序列会截断,这时inbuf就指向被截断的第一个字节。这种情况一般出现在编码指定错误或者是数据源被截断的时候。比如我们指定gb2312转ut-8,但是数据源里出现阿拉伯字符,这个时候就会发生截断。          如果outbuf空间不足,也会发生截断,不过这种情况相对少见,因为我们程序中会保证输出缓存有足够空间。
       另一种情况相对比较“正常”,就是被转的字符集不包含源字符集的字符,比如utf-8到gb2312的转换就很有可能发生这种情况。这时tocode的追加参数就起作用了,iconv会自动进行替换或者忽略。
       如果转换成功(没有发生截断),iconv返回的是不可逆的字符总数(也就是被替换或是忽略的字符总数,如果一切正常,应该返回0),如果转换失败,返回-1.


3、int iconv_close (iconv_t cd);
释放句柄资源。

四、utf-8-->gb18030转换示例
    这个代表了ansi和utf-8或者是ansi之间互相转换的代码写法。
iconv_t cd = iconv_open("gb18030//TRANSLIT", "utf-8");
const char* inbuffer; // 输入源,要转换的字符串
int srcLen = strlen(inbuffer);
int outLen = 1024;
static char s_outbuffer[outLen];
memset(s_outbuffer, 0, outLen);
const char* srcStart = inbuffer;
char* tempOutBuffer = s_outbuffer;//要有这个临时变量,否则iconv会直接改写s_outbuffer指针
size_t ret = iconv(cd, (const char**)&srcStart, (size_t *)&srcLen, &tempOutBuffer, (size_t *)&outLen);
iconv_close(cd);


五、unicode->gb18030转换示例
      这个代表了宽字符和多字节之间的转换,也就是wchar_t和char之间的转换,也就是WideCharToMultiByte所完成的操作。

FILE* fp = fopen("test-utf16be.txt", "rb"); // 文件是unicode文件,这里要用"rb"来读取,否则有些字节等同于EOF文件会被截断
char srcBuffer[1024] = {0};
fread(srcBuffer, sizeof(srcBuffer) - 1, 1, fp);

// 读取BOM信息
// char x[2];
// fread(&x[0], 1, 1, fp);
// fread(&x[1], 1, 1, fp);

// 带有BOM信息,直接用"utf-16";否则应该特别指定"utf-16le"或是"utf-16be",这个要区分清楚,否则会出现乱码
iconv_t cd = iconv_open("gb18030//TRANSLIT", "utf-16");
int srcLen = 1024;// srcLen和outLen代表的都是字节数,如果是wchar_t就是字符数*sizeof(wchar_t)
int outLen = 1024;
static char s_outbuffer[outLen];
memset(s_outbuffer, 0, outLen);
const char* srcStart = (const char*)srcBuffer;// srcBuffer同样可以是wchar_t,这里也是这样强制转换
char* tempOutBuffer = s_outbuffer;//要有这个临时变量,否则iconv会直接改写s_outbuffer指针
size_t ret = iconv(cd, (const char**)&srcStart, (size_t *)&srcLen, &tempOutBuffer, (size_t *)&outLen);
iconv_close(cd);