輝夜's profileKaguya Houraisan's glori...PhotosBlogListsMore Tools Help

Blog


    March, 2009

    C++ STL IO流 与 Unicode (UTF-16 UTF-8) 的协同工作

    凡用到文件读写,输入输出,就得和编码、Unicode 打交道。这系列实验来测试一下 C++ STL 的 IO流 对 ANSI 编码、Unicode 编码的支持特性,看能否找到一个自动识别编码,自动转码的解决方案。从基础开始,一步一步来:
     
    平台 Win32 XP sp3 + VS2008. (+ Boost 1.36.0)
     
    实验 01:
    #include<string>
    #include<iostream>
    #include<locale>
    using namespace std;
     
    locale prevloc;
    locale loc("chs");
     
    string str1("string class");
    string str2("汉字与字符");
    wstring wstr1(L"wstring class");          //去掉L前缀则编译错误
    wstring wstr2(L"汉字与字符");
     
    prevloc = cout.imbue(locale(""));
    cout<<"Default Locale: "<<prevloc.name()<<endl;
    cout<<"System Locale: "<<locale("").name()<<endl;
    cout<<"C风格字符串\n"<<L"w-string\n"<<str1<<'\n'<<str2<<'\n'<<endl;
     
    prevloc = wcout.imbue(loc);   //若去掉此句,则wstr2无法正常输出
    wcout<<"Default Locale: "<<prevloc.name().c_str()<<endl;    //若不加 .c_str() 则编译错误
    wcout<<"chs Locale Name: "<<loc.name().c_str()<<endl;
    wcout<<"C-string\n"<<"C风格字符串\n"<<L"宽字符串\n"<<wstr1<<'\n'<<wstr2<<'\n'<<endl;
     
    结论:
            1.cout 与 string 配合使用,wcout 与 wstring 配合使用,交错则编译错误(类型问题)
            2.wstring 初始化时需用 L"xxx" 的宽字符形式,同样 string 初始化时不能加 L 前缀
            3.默认locale ("C")下 cout 可以正常输出 C风格字符串与std::string类型,包括汉字也能正常显示
        但对 L"xxx" 宽字符串无能为力
              默认locale ("C")下 wcout 不能输出中文,包括C风格字符串、宽字符串与std::wstring
        设定系统 locale ("chs")后,正常输出宽字符串与std::wstring,但 C风格字符串 中的汉字无法显示
     
            总之,string cout "C-style 字符串" 自成体系
                      wstring wcout L"宽字符串" 自成体系,但 wcout 要选择 locale 后才能正常输出中文。
     
    实验 02:
    cout.imbue(locale(""));
    wcout.imbue(locale(""));
     
    string  str3 ( "abc汉字");
    wstring wstr3(L"abc汉字");
     
    cout<<"str1 length: "<<str1.length()<<'\n'; // 12
    cout<<"str2 length: "<<str2.length()<<'\n'; // 10
    cout<<"str3 length: "<<str3.length()<<'\n'; // 7
    cout<<str2[0]<<' '<<str2[1]<<'\n'// 输出:?
    cout<<endl;
    wcout<<L"wstr1 length: "<<wstr1.length()<<'\n'; // 13
    wcout<<L"wstr2 length: "<<wstr2.length()<<'\n'; // 5
    wcout<<L"wstr3 length: "<<wstr3.length()<<'\n'; // 5
    wcout<<wstr2[0]<<' '<<wstr2[1]<<'\n';   // 输出:汉 字
     
    结论:
            4.std::string 内部以 char 类型储存字符,当有汉字时以双字节存储,此时 length() 给出
        字符串所占字节数而不是字符数
              std::wstring 内部以 wchar_t 类型存储字符,字母汉字统一都是双字节,此时 length()
        给出是正确的字符数。
            5.当std::string中有汉字存在时,通过下标访问不能得到正确的字符。这是显而易见的,
        一方面字符宽度不统一无法随机访问,另一方面 std::string[] 返回 char 类型。std::wstring
        不存在此问题。
     
    实验 03:
    // test.txt 为 ANSI 编码(GB2312),内容为以上 str1 ~ str3 的3行。
    #include<fstream>
     
    string str;
    wstring wstr;
     
    ifstream fin("test.txt");
    //fin.imbue(locale(""));
    while(fin>>str)
        cout<<str<<'\n';
    fin.close();
     
    wifstream wfin("test.txt");
    //wfin.imbue(locale(""));
    //wfin.imbue(locale(".936"));
    while(wfin>>wstr)
        wcout<<wstr<<'\n';
    wfin.close();
     
    结论:
           6.std::ifstream 读取 ANSI 编码正常,std::wifstream 读取 ANSI 编码错误...默认 locale("C") 不能识别中文字符
              std::wifstream 设置 imbue(locale("")) 或 locale(".936") 后正常读取。936 为 GB2312 的代码页。
     
     实验 04:
     test.txt 为 Shift-JIS 编码,内容为
     うみねこのなく頃に

     程序代码同实验3
     ifstream 输出为
     偆傒偹偙偺側偔崰偵
     wifstream 设定 imbue(locale("")) 后输出相同
     
    结论:
           7.显而易见的,其他地区的编码无法正确识别。这也是很多日本游戏和文本文件运行
        或读取时产生乱码的原因。
     
     实验 05:
     test.txt 为 Shift-JIS 编码,内容同上
     ifstream 与 wifstream 都添加 imbue(locale("jpn")) 或 locale(".932")
    932 为 Shift-JIS 的代码页
     输出为:
     偆傒偹偙偺側偔崰偵
     うみねこのなく頃に
     
     
    结论:
           8.这里可以看出一个显著性差异。wifstream 在读取时按照 Shift-JIS 编码将其转换为
        Unicode 储存,在 wcout 输出时又按照 ANSI (GB2312) 转换,其结果是 —— 正确显示
        了其他地区编码的字符。而 ifstream 与 cout 则缺少那两步转换,结果与上例相同
        以后的实验将不再考虑 ifstream 而只实验 wifstream。
     
     实验 06:
     test.txt 存为 UTF-16 编码(Win32 默认的 little endian),内容同上。
     wifstream 设定为 imbue(locale(".1200"))
     1200 为 UTF-16 的 code page
     
     结果,运行出错...发现是 imbue(locale(".1200")); 这句的问题
     试着将 ".1200" 改为 ".936" 则运行正常,输出乱码。(936是 GB2312 的代码页)
     翻 MSDN 时在 Code Page 那页1200 UTF-16 后面发现一行小字:
     "available only to managed applications"...郁闷
     看来用 locale 转Unicode的想法到此结束了?记得 STL 书中貌似说过,locale 的名
     字在各平台上是不统一的,因为关系到各平台的支持问题。这样的话,要么自己写
     代码,要么就只好用 API 显式转换了:MultiByteToWideChar
     另外,在 setlocale 函数说明中也写到,UTF-8 和 UTF-7 等每字符有可能大于2字节
     的编码不被支持,所以 UTF-8 也只能用 MultiByteToWideChar 转咯...
     目前大概只能得出结论 C++ STL locale 在 Win32 平台上支持不完善吧
     
     实验 07: 用 API 重写读文件部分代码
    #include<windows.h>

    HANDLE hFile;
    if(INVALID_HANDLE_VALUE != (hFile = CreateFileW(L"test.txt",
            GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL))){

        int iFileLength, iUniTest, i;
        iFileLength = GetFileSize(hFile,NULL);
        char *pBuffer, *pText;
        pBuffer = new char[iFileLength+2];
        DWORD dwBytesRead;

        ReadFile(hFile,pBuffer,iFileLength,&dwBytesRead,NULL);
        CloseHandle(hFile);
        pBuffer[iFileLength] = '\0';
        pBuffer[iFileLength + 1] = '\0';
     
        iUniTest = IS_TEXT_UNICODE_SIGNATURE | IS_TEXT_UNICODE_REVERSE_SIGNATURE;
        if(IsTextUnicode(pBuffer,iFileLength,&iUniTest)){
            pText = pBuffer + 2;
            iFileLength -= 2;
            if(iUniTest & IS_TEXT_UNICODE_REVERSE_SIGNATURE){
                for(i = 0;i < iFileLength; i+=2)
                    swap(pText[i],pText[i+1]);
            }
            wstr = (wchar_t*)(pBuffer+2);
        }
        delete [] pBuffer;
        wcout<<wstr<<'\n';
    }
     
            输出正确。以上程序段自动识别 Unicode 编码文件开头的 0xFFFE 标记判断是 Little Endian 还是
        Big Endian 并做相应转换。但是代码量较大,且与 C++ 的 IO流 很不搭调...
     
    结论:
           9.可以看到,只是把输入内容去掉UTF-16开头的0xFFFE,直接把内存指针改为
        wchar_t* 后 std::wstring 即可正确识别,说明程序中的宽字符存储格式实际上用的就是
        UTF-16 little endian
     
     实验 08:
     不死心又去翻了 boost 库,发现 codecvt_null 这个好东西,看下实现是把文件存储内容
     按照 wchar_t 为单位直接读入内存不做任何转换。这其实不正好是 UTF-16 需要做的么
     以下把 test.txt 存为 UTF-16 little endian 再次实验
    #include<boost/archive/codecvt_null.hpp>

    wifstream wfin(L"test.txt");
    locale utf16(loc, new boost::archive::codecvt_null<wchar_t>);
    wfin.imbue(utf16);
    while(wfin>>wstr){
        wcout<<wstr<<endl;
    }
    wfin.close();
     
    输出正确。
     
    结论:
           10. 看来可以把 codecvt_null 作为 UTF-16 的 codecvt_facet 读入 locale
        来使用,避免使用类似上面 API 那么多代码。
     
     实验 09:
     将 test.txt 存为 UTF-16 Big Endian ,内容不变。程序不变
     
    无法输出任何内容。
    结论:
           11. wcout 不认识 big endian 的 wchar_t ...
        看来想读取 UTF-16 Big Endian,仅靠 codecvt_null 还不够。稍微翻了一下
        《C++ 输入输出流与本地化》这本书,现在可以考虑写一个自己的 codecvt_facet
        了。有了 codecvt_null 的代码,稍作改动即可用于 UTF-16 big endian。虽说有了
        现在的知识自己写个 utf-16 的codecvt_facet 也可以,但效率大概比不上 boost 里的。
     
    代码准备:用类似的方法写出了自己的 codecvt_utf16 和 codecvt_utf16_reverse 两个
    codecvt_facet...然后继续实验。自己写的内容放入咱自己的头文件吧:codecvt_utf.h,
    内容加入自己的 namespace : tvt
     
     实验 10: 用 codecvt_utf.h 代替 codecvt_null.hpp。用 codecvt_utf16 和
     codecvt_utf16_reverse 实现 little endian 与 big endian 的输入。

    wifstream wfin(L"test.txt");
    locale utf16(loc,new tvt::codecvt_utf16<wchar_t>);
    wfin.imbue(utf16);
    while(wfin>>wstr){
        wcout<<wstr<<endl;
    }
    wfin.close();
    ///////////////////////////////////////
    wifstream wfin(L"test.txt");
    locale utf16(loc,new tvt::codecvt_utf16_reverse<wchar_t>);
    wfin.imbue(utf16);
    while(wfin>>wstr){
        wcout<<wstr<<endl;
    }
    wfin.close();
     
    第一段程序读取 UTF-16 little endian 编码的 text.txt 正确输出
    第二段程序读取 UTF-16 big endian 编码的 text.txt 正确输出
     
    UTF-16 的转码顺利完成。下面考虑 UTF-8 ,写法类似。在 boost 库中继续寻找,发现
    这个东东 boost/detail/utf8_codecvt_facet.hpp 。看下说明,不支持直接使用此文件,这文件
    是专门提供其他 boost 组件使用的。仅 include 它的话编译出问题。再寻找到同名的 cpp 文件
    后即可看到 do_in do_out 这两个转码关键的虚函数。有了上面 UTF-16 的基础,我们类似可写
    出 UTF-8 的转码 codecvt_facet。我给他起名为 codecvt_utf8, 依然加入 codecvt_utf.h 文件。
    现在此文件有一两百行了。经试验可正确输入 UTF-8 编码。
     
    对应编码有了处理方法后,下一个问题是编码识别。
     
    实验 11:
    wchar_t wc;
    wchar_t buf[2];
    wifstream wfin(L"text.txt");
    wfin.read(&wc,1);
    wfin.read(&buf[0],2);
     
    将 wc 和 buf 的内容按2进制或16进制输出。
    结论:
           12. wistream.read(buffer,count) 操作每次读入 count 个字节,但将每个字节存入一个
     wchar_t 类型的 buffer[i] 中。其实 buffer 中每个 wchar_t 的高位都字节是 0 ...
     
     实验 12:
     加入判断条件,在 wfin 中自动加入合适的 utf16 facet,使得自动识别并读取
     little endian 和 big endian 编码的文件:

    wchar_t buf[2];
    wifstream wfin(L"test.txt");
    wfin.read(buf,2);

    if(buf[0] == wchar_t(0xFF) && buf[1] == wchar_t(0xFE)){
        cout<<"little endian"<<endl;
        wfin.imbue(locale(loc,new tvt::codecvt_utf16<wchar_t>));
    }
    else if(buf[0] == wchar_t(0xFE) && buf[1] == wchar_t(0xFF)){
        cout<<"big endian"<<endl;
        wfin.imbue(locale(loc,new tvt::codecvt_utf16_reverse<wchar_t>));
    }
    while(wfin>>wstr){
        wcout<<wstr<<endl;
    }
     
    对于两种编码的 text.txt 都实现了自动识别并正确读取。输出正确!
     
    结论:
           13.UFT-16在传输时几乎都会加上 0xFFFE 等传输标志很容易判断,即使没有, Win32 下
        也有 IsTextUnicode 这 API 用专门方法判断。UTF-8 就很麻烦了,开头不一定都有 BOM 标
        记,与各地区字符集一样都可以用一个或多字节表示一个字符,编码长度不固定,如果是
        很长一段 ASCII 字符,那么用 UTF-8 和 GB2312 编码出来结果一样,就很难分辨
     
    代码准备:经过一段时间思考,打算用这种算法。先读取前3字节,若是 BOM 头标记最好。若
    不是则排除 UTF-16 ,下面集中力量分辨 UTF-8 与 ANSI 。从头开始寻找第一个 >127 的字节
    若此字节内容 < 0xC0 或 >0xEF 则可判断不是 UTF-8 。否则,根据 UTF-8 的规则,在后面1 或
    2 字节中看开头两位是不是 10 。若不是则断定不是 UTF-8 ,否则就算得到一个 UTF-8 字符。
    如果能够找到 10个 满足条件的 UTF-8 字符就判断为 UTF-8 编码。若未到 10 个即遇到文件结
    尾,那么找到 UTF-8 字符数大于 1 即断定为 UTF-8 否则断定为 ANSI ...
    用这种方式选择对应转码 facet:
    wistrm.imbue(std::locale(wistrm.getloc(), new codecvt_utf8));
     
    按以上想法写成函数 int IsStreamUnicode(std::wistream &wistrm); UTF-16 LE 返回1,BE 返回2,
    UTF-8 返回3,否则返回 0 (判断为ANSI)
     
    实验 13:
     
    std::wifstream wfin(L"test.txt");
    if(!tvt::IsStreamUnicode(wfin))
        wfin.imbue(loc);
    while(wfin>>wstr)
        wcout<<wstr<<endl;
     
     在我试验的各种情况下,均能自动识别 UTF-16 LE UTF-16 BE UTF-8 与 ANSI 编码
     并正确设定转码 locale .
     
     
    -------------------------------------------------------------------------------------
    8小时后,关于后续实验的补充:
     
    使用中发现某些情况下 UTF-16 的读写出现问题,特别是有换行符或某字节中编码刚好
    等于控制符时。经过反复测试认定是 读写mode 问题。在读写 Unicode 文件时,
    wifstream 与 wofstream 都设定为 ios_base::binary 模式即可。后来又补充了一个添加
    BOM 头的小东西。为了使用简便把 utf_16 的 template 也去掉了。最终情形使用起来
    像这个样子:
     
    #include<iostream>
    #include<fstream>
    #include<codecvt_utf.h>
    using namespace std;
     
    wstring wstr;
    wcout.imbue(locale(""));
     
    // Open the Input and Output Files:
    std::wifstream wfin(L"test.txt", ios_base::binary);
    std::wofstream wfout(L"testout.txt", ios_base::binary);
     
    // Set Output Format and Write BOM tag:
    wfout.imbue(locale(locale(""), new tvt::codecvt_utf16));
    wfout<<tvt::utf_bom;
     
    // Detect the Format of the Input File
    if(!tvt::IsStreamUnicode(wfin))
        wfin.imbue(locale(""));
     
    // Read and Write
    //while(wfin>>wstr){
    //    wcout<<wstr<<endl;
    //    wfout<<wstr<<endl;
    //}
     
    // Another way:
    while(getline(wfin,wstr)){
        wcout<<wstr<<endl;
        wfout<<wstr<<endl;
    }
     
    // Close Files:
    wfin.close();
    wfout.close();
     
    读写测试全部通过!
     
    感谢 记事本、EditPlus 和 HxDen 的大力支持...

     至此,关于 Unicode 编码和 C++ STL IO流 的协作算是大功告成了吧,呵呵。以后有需要再
    在实践中改进

     花了整整一天时间 + 8 小时 = = 还算有价值吧,因为在网上看到很多人都在问且没有结果
     
     ===========分隔线============
     另附:现在来看用 c++ 的 IO stream locale 系列实现转码并不是一个经济的选择,如果用 STLport 的话还好些,用 VC STL 则存在较严重的效率问题:

    Comments (10)

    Please wait...
    Sorry, the comment you entered is too long. Please shorten it.
    You didn't enter anything. Please try again.
    Sorry, we can't add your comment right now. Please try again later.
    To add a comment, you need permission from your parent. Ask for permission
    Your parent has turned off comments.
    Sorry, we can't delete your comment right now. Please try again later.
    You've exceeded the maximum number of comments that can be left in one day. Please try again in 24 hours.
    Your account has had the ability to leave comments disabled because our systems indicate that you may be spamming other users. If you believe that your account has been disabled in error please contact Windows Live support.
    Complete the security check below to finish leaving your comment.
    The characters you type in the security check must match the characters in the picture or audio.

    To add a comment, sign in with your Windows Live ID (if you use Hotmail, Messenger, or Xbox LIVE, you have a Windows Live ID). Sign in


    Don't have a Windows Live ID? Sign up

    STL 标准库确实对国际化部分有很多欠缺,好消息是 boost 中要加入 locale 组件了,虽然现在还未进入 release ,但好像进度很快,估计再等2版之内就会出现
    Nov. 2
    [To:王冬]您好。
    首先,GB2312中是包含日文假名的。
    GB2312字符集中除常用简体汉字字符外还包括希腊字母、日文平假名及片假名字母、俄语西里尔字母等字符等。
    其次,我刚试了那段 debug 和 release 都是正常的,当然用的还是 vs2008,手头没有2005就没法验证了。wcout 在输出宽字符前也是需要设定 locale("") 的,看会不会是哪里设置有问题。
    Aug. 16
    冬 王wrote:
    8.这里可以看出一个显著性差异。wifstream 在读取时按照 Shift-JIS 编码将其转换为
    Unicode 储存,在 wcout 输出时又按照 ANSI (GB2312) 转换,其结果是 —— 正确显示
    了其他地区编码的字符。
    对于这条结论,有点质疑
    因为,将Unicode格式的日文字符转为gb2312是不可能成功的,因为gb2312的字符集应该不包含日文字符
    同时,我使用.net2005的环境中,wifstream 设置为locale(".932"),利用debug可以正常读取,但是无法利用wcout正常输出
    不知道是不是2008能正确解决这个问题。
    Aug. 14
    抱歉很久没注意到有留言
    上面文件的代码其实基本上都是boost对应文件原内容照搬,只用稍作改动而已。
    直接贴代码的话总觉得不是自己写的拿不出手,找个空间上传算了
    http://www.fileden.com/files/2008/5/10/1904760/codecvt_utf.zip
    由于我不是经常要和unicode打交道,所以测试得并不多,只是前一段写一个程序用了一下,至今使用没发现什么问题而已啦
    注意点是 1.把文件分为h和cpp两部分避免多次使用时重编译,直接在项目中加入两个文件使用即可。
    2. 未考虑不同平台下 wchar_t 大小不同的问题,我默认是按 win32 下双字节来写的,在linux下大概会有问题。
    现在自己水平还很有限,很多地方没照顾到呢……
    Apr. 14
    Moirawrote:
    請問是否能夠借閱 codecvt_utf.h 的完成品呢? 以下有3個地方能貼程式碼
    http://rafb.net/paste/http://nopaste.info/http://gist.github.com/
    另外 Sound Horizon 6th Story Concert「Moira」~其れでも、お征きなさいよ仔等よ~LIVE DVD 通常盤 DVD発売日: 2009/03/25
    Mar. 29
    Neil Wangwrote:
    原来是游戏
    你还好这类游戏?
    Mar. 10
    Peipei Dongwrote:
    真牛总是在这里蛰伏……
    Mar. 8
    東方 Project 第八作
    東方永夜抄 最終面BOSS 蓬萊山輝夜
    图片来源于某张 SpellCard 图
    Mar. 8
    Neil Wangwrote:
    话说你头像是哪个里面的?
    Mar. 8
    Neil Wangwrote:
    老大,你已经无敌了
    Mar. 8

    Trackbacks

    The trackback URL for this entry is:
    http://dantvt.spaces.live.com/blog/cns!D87988A6CAC0A480!925.trak
    Weblogs that reference this entry
    • None