如何判斷文本的編碼格式以及編碼格式轉換

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章