字符集和字符编码[订正]

这个主题已经被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
 posted on 2011-08-06 13:30  chingliuyu  阅读(343)  评论(0编辑  收藏  举报