如何判断文本的编码格式以及编码格式转换

0 前话

我相信不少程序员遇到过这样的问题:在程序里写了一段代码读文件里面的文本内容,一运行显示出来却是乱码。

为什么会乱码?
这是因为那个文件的编码格式和代码里处理文本时认为的编码格式不一样。比如,你新建了一个MFC工程,把Character Set设置为了Use Unicode Caracter Set(MFC工程默认为这个设置),然后你写了一段代码去读一个文本文档,这个时候MFC直接认为你这个文本文档就是unicode编码格式,当你的文本文档不是unicode编码时就会出现乱码。
这个道理其实很简单。假如你只懂中文,而且只会用中文去处理你看到的文档信息,有一天,你看到一篇英文文档,你把英文当成中文拼音看,你的解读就会和原文有很大出入,有的英文单词可能刚好和某个拼音对应,比如“he”英文的意思是“他”而按拼音来你可以理解为“呵”“河”“喝”之类的,但你绝对不会认为是“他”的意思。有的英文单词并不能构成完整的拼音,这个时候你就会心里嘀咕“麻蛋,这啥玩意儿?!”,于是你就一团乱麻了。当你用中文去解读日文时,看到那些奇形怪状的符号你肯定会更加一团乱麻,因为用拼音都无法解读。

那么,什么是编码格式?有哪些编码格式?如何判断从文本文档读取的文本内容是什么编码格式的?如何转换编码格式?
本文就以上几个问题展开讨论,并尝试给出解决方案。其实本人也是小白,有什么说的不对的地方请各位指正,谢谢!

本文内容及代码参考了网上其他网友写的一些文章,参考链接会在本文适当地方或文末给出,如有侵权,请联系我。
如果要转载本文,请以链接形式注明出处。

1 字符集和字符编码

1.1 字符集

字符集(Charcater Set或Charset)是一个系统支持的所有抽象字符的集合,也就是一系列字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。常见的字符集有:ASCII字符集、GB2312字符集(主要用于处理中文汉字)、GBK字符集(主要用于处理中文汉字)、Unicode字符集等。

1.2 字符编码

字符编码(Character Encoding)是一套法则,使用该法则能够对自然语言的字符的一个字符集(如字母表或音节表),与计算机能识别的二进制数字进行配对。即它能在符号集合与数字系统之间建立对应关系,是信息处理的一项基本技术。通常人们用符号集合(一般情况下就是文字)来表达信息,而计算机的信息处理系统则是以二进制的数字来存储和处理信息的。字符编码就是将符号转换为计算机能识别的二进制编码。

1.3 字符集和字符编码的关系

一般一个字符集等同于一个编码方式,ANSI体系(ANSI是一种字符代码,为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符)的字符集如ASCII、ISO 8859-1、GB2312、GBK等等都是如此。一般我们说一种编码都是针对某一特定的字符集。
一个字符集上也可以有多种编码方式,例如UCS字符集(也是Unicode使用的字符集)上有UTF-8、UTF-16、UTF-32等编码方式。

1.4 字符编码的发展历史

从计算机字符编码的发展历史角度来看,大概经历了三个阶段:

第一个阶段:ASCII字符集和ASCII编码。
计算机刚开始只支持英语(即拉丁字符),其它语言不能够在计算机上存储和显示。ASCII用一个字节(Byte)的7位(bit)表示一个字符,第一位置0。后来为了表示更多的欧洲常用字符又对ASCII进行了扩展,又有了EASCII,EASCII用8位表示一个字符,使它能多表示128个字符,支持了部分西欧字符。

第二个阶段:ANSI编码(本地化)
为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 ‘中’ 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。
不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。
不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。

第三个阶段:UNICODE(国际化)
为了使国际间信息交流更加方便,国际组织制定了 UNICODE 字符集,为各种语言中的每一个字符设定了统一并且唯一的数字编号,以满足跨语言、跨平台进行文本转换、处理的要求。UNICODE 常见的有三种编码方式:UTF-8(1个字节表示)、UTF-16((2个字节表示))、UTF-32(4个字节表示)。

1.5 Big Endian和Little Endian

big endian和little endian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。如果将49写在前面,就是little endian。

2 检测文本的编码格式

下面对notepad中几种常见的编码格式(ANSI、UTF-8、UTF-8 无BOM、UCS-2 Big Endian、UCS-2 Little Endian)进行讲解。

2.1 原理

Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。BOM是一个有点小聪明的想法:

在UCS编码中有一个叫做”ZERO WIDTH NO-BREAK SPACE”的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符”ZERO WIDTH NO-BREAK SPACE”。

这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符”ZERO WIDTH NO-BREAK SPACE”又被称作BOM。

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符”ZERO WIDTH NO-BREAK SPACE”的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。

Windows就是使用BOM来标记文本文件的编码方式的。

2.2 检测策略

根据2.1节所述,可以得到以下检测策略:
1. 如果2个字节是0xFF 0xFE,则以Unicode(LE)的方式读取
2. 如果2个字节是0xFE 0xFF,则以Unicode BE的方式读取
3. 如果前2个字节是0xEF 0xBB,那么判断第3个字节是不是0xBF,如果是的话就以UTF-8的方式进行读取。
4. 判断是否符合UTF-8的编码规范,如果符合就以UTF-8的方式进行读取
如果以上都不是,则以ANSI的方式进行读取。

2.2 代码实现

下面用C语言实现检测文本的编码格式。

// 枚举编码格式
enum EncodingType {
    ENCODINGTYPE_ANSI = 0,    // ANSI
    ENCODINGTYPE_ULE,         // UCS Little Endian
    ENCODINGTYPE_UBE,         // UCS Big Endian
    ENCODINGTYPE_UTF8,        // UTF-8
    ENCODINGTYPE_UTF8_NOBOM,  // UTF-8 No BOM
}

// 检测是否为UTF-8无BOM格式编码
// src为文本内容,len为文本的长度
BOOL CheckUTF8NoBOM(const void* pBuffer, long size)
{     
    bool IsUTF8 = true;     
    unsigned char* start = (unsigned char*)pBuffer;     
    unsigned char* end = (unsigned char*)pBuffer + size;     
    while (start < end)     
    {     
        if (*start < 0x80) {   
            // (10000000): 值小于0x80的为ASCII字符  
            start++;     
        } else if (*start < (0xC0)) {
            // (11000000): 值介于0x80与0xC0之间的为无效UTF-8字符
            IsUTF8 = false;     
            break;     
        } else if (*start < (0xE0)) {     
            // (11100000): 此范围内为2字节UTF-8字符
            if (start >= end - 1) break;     
            if ((start[1] & (0xC0)) != 0x80) {     
                IsUTF8 = false;     
                break;     
            }     
            start += 2;     
        } else if (*start < (0xF0)) {
            // (11110000): 此范围内为3字节UTF-8字符
            if (start >= end - 2) break;     
            if ((start[1] & (0xC0)) != 0x80 || (start[2] & (0xC0)) != 0x80) {     
                IsUTF8 = false;     
                break;     
            }     
            start += 3;     
        } else {     
            IsUTF8 = false;     
            break;     
        }     
    }     
    return IsUTF8;     
}

// 从文本中获取编码格式
// src为文本内容,len为文本的长度
EncodingType GetEncodingTypeFromStr(const TCHAR *src, long len)
{
    const PBYTE pBuffer = (const PBYTE)src;
    if (pBuffer[0] == 0xFF && pBuffer[1] == 0xFE)
        return ENCODINGTYPE_ULE;
    if (pBuffer[0] == 0xFE && pBuffer[1] == 0xFF)
        return ENCODINGTYPE_UBE;
    if (pBuffer[0] == 0xEF && pBuffer[1] == 0xBB && pBuffer[2] == 0xBF)
        return ENCODINGTYPE_UTF8;
    if (CheckUTF8NoBOM(src, len))
        return ENCODINGTYPE_UTF8_NOBOM;
    else return ENCODINGTYPE_ANSI;
}

3 编码格式转换

下面给出几种常见的编码格式的转换的C语言实现。

wstring StrToWstr( UINT CodePage,const string& str )
{
    int len = str.length();
    wstring  wStr = L"";
    if(len <= 0) return wStr;

    int  unicodeLen = ::MultiByteToWideChar( CodePage,0,str.c_str(),-1,NULL,0 );

    wchar_t *  pUnicode;
    pUnicode = new  wchar_t[unicodeLen+1];
    memset(pUnicode,0,(unicodeLen+1)*sizeof(wchar_t));
    ::MultiByteToWideChar( CodePage,0,str.c_str(),-1,(LPWSTR)pUnicode,unicodeLen );

    wStr = ( wchar_t* )pUnicode;
    delete  pUnicode;
    return  wStr;
}

string WstrToStr(UINT CodePage, const wstring& wStr )
{
    int len = wStr.length();
    string  str = "";
    if(len <= 0) return str;

    char*     pElementText;
    int    iTextLen;

    iTextLen = WideCharToMultiByte( CodePage,0,wStr.c_str(),-1,NULL,0,NULL,NULL );

    pElementText = new char[iTextLen + 1];
    memset( ( void* )pElementText, 0, sizeof( char ) * ( iTextLen + 1 ) );
    ::WideCharToMultiByte( CodePage,0,wStr.c_str(),-1,pElementText,iTextLen,NULL,NULL );

    str = pElementText;
    delete[] pElementText;

    return str;
}

wstring ANSIToUnicode( const string& strANSI )
{
    return StrToWstr( CP_ACP,strANSI );
}
wstring UTF8ToUnicode( const string& strUTF8 )
{
    return StrToWstr( CP_UTF8,strUTF8 );
}
string UnicodeToANSI( const wstring& strUnicode )
{
    return  WstrToStr(CP_ACP, strUnicode );
}
string UnicodeToUTF8( const wstring& strUnicode )
{
    return  WstrToStr(CP_UTF8, strUnicode );
}

4 参考文章

http://blog.csdn.net/luoweifu/article/details/49382969
http://www.fmddlmyy.cn/text6.html
http://www.cnblogs.com/lkpp/p/encoding_detection.html
http://blog.csdn.net/apple_8180/article/details/7007114
http://blog.csdn.net/turingo/article/details/8136644
http://blog.csdn.net/bladeandmaster88/article/details/54767487

你可能感兴趣的:(编程基础)