字符集和字符编码[订正]
这个主题已经被N多人讨论过了,这里仅仅是个人总结,不是教程。
字符集和字符编码
潘孙友 2010-12-31 于遵义
目录 一、字符集 二、字符编码 三、Windows平台 3.1 Codepage代码页 3.2 编码转换(API) 3.3 编码转换(CRT) [感谢@loop指出错误] 四、Linux/unix平台 4.1 iconv 4.2 ICU
一、字符集
字符集是一个集合,描述并定义了这个集合中可以出现哪些字符,常见的字符有GB2312、GBK、GB18030、UNICODE等。字符集仅仅是一种规范,一种约定,我们也可以定义自己的字符集。
举例来说,银行IT系统为了字段合法性校验的方便,常内部定义一些小字符集,比如X字符集,N字符集。
x-字符集由以下86个字符组成 a b c d e f g h I j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9 . , - _ ( ) / = ’+ : ? ! ” % & * < > ; @ # (cr)(lf) (space)
不同的字符集之间很可能是有交集的,并且包含越多字符的字符集越通用。中国的BG2312,GBK,GB18030是在不同时期,逐步扩展而来的,所以GB18030是前者的超集。现今最大的字符集是UNICODE,几乎包含了世界上所有语言的字符。
二、字符编码
先有字符集才有字符编码,编码是字符集的具体表示方式,一个字符集可以有多种编码方式,只要这种方式可以涵盖字符集中的所有字符。
比如UNICODE字符集的具体编码方式有很多种,utf-8/utf-16/utf-32;而gb2312就只有常见的一种编码方式GB2312(所以有时候人们常将编码与字符集弄混)。可能有人会说,utf-8中包含了gb2312的所有字符,那不就是以utf-8编码表示了gb2312字符集么,事实是这样,但问题人家utf-8编码方式又不是为你gb2312定制的,人家只是顺手把你给表示了。
不同的编码方式,根据其特性应用在不同场合。以UNICODE字符集的编码方式为例,其中utf-8以尽可能少的存储空间存放字符,自动纠错性能好(由于编码的特殊性,其一个字符出错只会影响其后几个字节,而utf16/32等等长字节的就连错一大片了),适合传输及存储;utf-16/32以等字节表示所有字符,在程序内部更容易处理,一般用于系统内部字符格式。
三、Windows平台
3.1 Codepage代码页
Windows平台有所谓的代码页,用于表示字符编码方式,至少支持哪些,要看你的系统里安装了哪些。通过GetACP我们可以获取当前系统的编码方式。
WINBASEAPI UINT WINAPI GetACP(void);通过以下小段代码,可以获举系统支持的编码方式。
for(int i=0; i<=65001; i++) { CPINFOEXA cpinfo; if (IsValidCodePage(i)){ if (GetCPInfoExA(i, 0, &cpinfo)) printf("%d=[%s]\n", cpinfo.CodePage, cpinfo.CodePageName); } }详细的列表见: http://msdn.microsoft.com/en-us/library/dd317756(v=VS.85).aspx
3.2 编码转换(API)
Windows为我们提供了两个函数用于字符编码的转换。
WINBASEAPI int WINAPI MultiByteToWideChar( __in UINT CodePage, __in DWORD dwFlags, __in_bcount(cbMultiByte) LPCSTR lpMultiByteStr, __in int cbMultiByte, __out_ecount_opt(cchWideChar) __transfer(lpMultiByteStr) LPWSTR lpWideCharStr, __in int cchWideChar); WINBASEAPI int WINAPI WideCharToMultiByte( __in UINT CodePage, __in DWORD dwFlags, __in_ecount(cchWideChar) LPCWSTR lpWideCharStr, __in int cchWideChar, __out_bcount_opt(cbMultiByte) __transfer(lpWideCharStr) LPSTR lpMultiByteStr, __in int cbMultiByte, __in_opt LPCSTR lpDefaultChar, __out_opt LPBOOL lpUsedDefaultChar);
Windows内部应该是完全使用Unicode编码的,系统提供的W后缀的API都接受wchar_t(unsigned short)的字符串,而A系列的API实则是先内部转换为Unicode编码再调用W系统函数。
在windows上做不同编码间的转换,一般是先将原始编码转换为Unicode编码,再将Unicode编码的字符串转换为目标编码。以gbk到utf-8为例:
std::string ctk_gbk2utf8(const char*s) { s = s?s:""; std::wstring unicodestr; std::string utf8str; //gbk转换成utf16 int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0); unicodestr.resize(n); MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length()); //从utf16转utf8 n = WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 ); utf8str.resize(n); WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, (char*)utf8str.c_str(), (int)utf8str.length(), 0, 0 ); return utf8str; } std::string ctk_utf82gbk(const char*s) { s = s?s:""; std::wstring unicodestr; std::string gbkstr; //utf8转换成utf16 int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0); unicodestr.resize(n); MultiByteToWideChar(CP_UTF8, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length()); //从utf16转gbk n = WideCharToMultiByte(936, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 ); gbkstr.resize(n); WideCharToMultiByte(936, 0, unicodestr.c_str(), -1, (char*)gbkstr.c_str(), (int)gbkstr.length(), 0, 0 ); return gbkstr; }测试代码:
const char*gbk = "这是中文. hello world!"; printf("gbk=%s\n", gbk); string utf8str = ctk_gbk2utf8(gbk); printf("utf8str=%s\n", utf8str.c_str()); string gbkstr = ctk_utf82gbk(utf8str.c_str()); printf("gbkstr=%s\n", gbkstr.c_str());
如果要处理像日本或者韩国语言编码,只要换一个那个936为相应的代码就可以了。上面的CP_UTF8可以换为65501。一般情况下,使用系统API转换编码就差不多了。
在未经实践的情况下我听轻信了网上其它文章的言论,实际上是错误的:另外,还有c/c++标准库提供的mbstocws, cwstombs函数,但由于在windows上无法处理当前代码页以外的字符集而变得不是那么好用(即便setlocale,也是没用的)。
3.3 编码转换(CRT)
另外我们可以通过c/c++标准库中的mbstocws, cwstombs函数来转码,我们可通过两组函数来测试,一组为Windows API实际的,一组为 mbstocws, cwstombs实现,效果是一样的。
std::string ctk_gbk2utf8(const char*s) { s = s?s:""; std::wstring unicodestr; std::string utf8str; int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0); unicodestr.resize(n); MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length()); n = WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 ); utf8str.resize(n); WideCharToMultiByte(CP_UTF8, 0, unicodestr.c_str(), -1, (char*)utf8str.c_str(), (int)utf8str.length(), 0, 0 ); return utf8str; } std::string ctk_gbk2big5(const char*s) { s = s?s:""; std::wstring unicodestr; std::string dststr; int n = MultiByteToWideChar(936, 0, s, -1, NULL, 0); unicodestr.resize(n); MultiByteToWideChar(936, 0, s, -1, (wchar_t *)unicodestr.c_str(), (int)unicodestr.length()); n = WideCharToMultiByte(950, 0, unicodestr.c_str(), -1, 0, 0, 0, 0 ); dststr.resize(n); WideCharToMultiByte(950, 0, unicodestr.c_str(), -1, (char*)dststr.c_str(), (int)dststr.length(), 0, 0 ); return dststr; }
std::string ctk_gbk2big5_crt(const char*s) { s = s?s:""; std::string srcstr = s; std::string curLocale = setlocale(LC_ALL, NULL); setlocale(LC_ALL, ".936"); size_t newSize = srcstr.length() + 1; wstring unicodestr; unicodestr.resize(newSize); wmemset((wchar_t*)unicodestr.c_str(), 0, newSize); mbstowcs((wchar_t*)unicodestr.c_str(), srcstr.c_str(), newSize); string newstr; newSize = newSize*2 + 1; setlocale(LC_ALL, ".950"); newstr.resize(newSize); memset((char*)newstr.c_str(), 0, newSize); wcstombs((char*)newstr.c_str(), unicodestr.c_str(), newSize); setlocale(LC_ALL, curLocale.c_str()); return newstr; } std::string ctk_big52gbk_crt(const char*s) { s = s?s:""; std::string srcstr = s; std::string curLocale = setlocale(LC_ALL, NULL); setlocale(LC_ALL, ".950"); size_t newSize = srcstr.length() + 1; wstring unicodestr; unicodestr.resize(newSize); wmemset((wchar_t*)unicodestr.c_str(), 0, newSize); mbstowcs((wchar_t*)unicodestr.c_str(), srcstr.c_str(), newSize); string newstr; newSize = newSize*2 + 1; setlocale(LC_ALL, ".936"); newstr.resize(newSize); memset((char*)newstr.c_str(), 0, newSize); wcstombs((char*)newstr.c_str(), unicodestr.c_str(), newSize); setlocale(LC_ALL, curLocale.c_str()); return newstr; }
四、Linux/unix平台
4.1 iconv
这些平台上,字符编码的转换一般使用iconv,可能是独立的iconv库也可能是glibc自带的版本。 在linux/unix上字符编码的问题比windows更容易碰到,所以经常有人喊搭了个ftp传上来的文件名乱码什么的(顺便提一句,包括IE7在内的之前的IE版本直接访问utf8编码的ftp时会有乱码,通过网络抓包工具会发现是IE发送的utf-8字符串是部分错误的)。
暂时只谈程序中的编码问题,先不管locale之类的(虽然也相关)。
自己“封装”的转码函数。
char *ctk_iconv(const char *fromStr, const int fromLen, char**toStr, const char *fromCode, const char *toCode) { char *buffer; iconv_t cd; const char *inbuf = NULL; size_t inbytesleft = 0; char *outbuf = NULL; size_t outbytesleft = 0; int errorCode = 0; int bufferSize=0; size_t ret = 0; int done = 0; if (fromStr==NULL || fromStr[0]=='\0' || fromLen <=0 ) return NULL; if (fromCode==NULL || fromCode[0]=='\0' ) return NULL; if (toCode==NULL || toCode[0]=='\0' ) return NULL; memset(&cd, 0x00, sizeof(iconv_t)); inbuf = fromStr; inbytesleft = fromLen; errorCode = 0; bufferSize = fromLen*4+1; buffer = (char*)malloc(sizeof(char)*bufferSize); memset(buffer, 0x00, bufferSize); outbuf = buffer; outbytesleft = bufferSize; if ( (iconv_t)-1 == ( cd = iconv_open(toCode, fromCode) ) ) { return NULL; } while ( inbytesleft >0 && done !=1 ) { ret = iconv(cd, (char**)&inbuf, &inbytesleft, &outbuf, &outbytesleft); if ( (size_t)-1 == ret ) { errorCode = errno; switch(errorCode) { case EILSEQ: { if((outbuf<buffer+bufferSize)&&(outbuf>=buffer)) { memcpy(outbuf, inbuf, 1); outbuf += 1; outbytesleft -= 1; inbuf += 1; inbytesleft -= 1; if ( inbytesleft <= 0 ) break; } } break; case EINVAL: { done = 1; } break; case E2BIG: { done = 1; break; } break; default: done = 1; } } } if ( NULL != toStr) *toStr = buffer; iconv_close(cd); return buffer; } std::string ctk_iconv_gbk2utf8(const char*s) { s = s ? s:""; char *utf8str = NULL; ctk_iconv(s, strlen(s), &utf8str, "gbk", "utf-8"); std::string result(""); if (utf8str!=NULL) { result = utf8str; free(utf8str); } return result; } std::string ctk_iconv_utf82gbk(const char*s) { s = s ? s:""; char *gbkstr = NULL; ctk_iconv(s, strlen(s), &gbkstr, "utf-8", "gbk"); std::string result(""); if (gbkstr!=NULL) { result = gbkstr; free(gbkstr); } return result; }
Iconv在windows上也非常好用。
4.2 ICU
IBM出品的ICU也是编码转换的好手,到处有它的身影,php6就使用它来做内码。由于没有亲身使用经历,就不多说了。
http://www-01.ibm.com/software/globalization/icu/index.html