在C++中进行字符串编码转换

最近做了个Jubeat Analyzer的谱面转成Yubiosi谱面的工具。在制作工具的过程中,遇到了一个问题,就是源谱面文件中字符串的编码问题。

Jubeat Analyzer是日本人造的软件,然后它保存的谱面又没有用Unicode,所以在中文操作系统上看就会乱码。对于编码转换问题,其实C++是可以直接支持的。对于一个支持输出宽字符的文件流,把区域设置到japanese就可以了,像这样:

std::locale jpLoc("japanese");
std::wfstream fs("c:\\nanika.txt", std::ios::in);
fs.imbue(jpLoc);

如果问题这么简单就解决的话就好了……

问题是,源文件中不仅仅包含ShiftJIS编码的部分,而且还包含GBK编码的部分。比如某些地方的注释,某些 m="音乐文件名" 的地方。因为如果音乐文件名用的是ShiftJIS,那么在中文系统下就没办法正常读取音乐了。

这种情况下,来一个imbue不能解决问题。因为它读到ShiftJIS不能表示的字符的时候,这个流就给你eof掉了……

既然这样,那我就一行一行读,不同情况不同处理。自己转成Unicode。好吧,怎么转

用MultiByteToWideChar固然简单,但是我想要用C++标准库去做而不是调用系统API。因为你看,wfstream都可以读取ShiftJIS文件到wstring,那么说明内部一定有什么代码实现编码转换的。我想办法调用这编码就行了。

因为宽字符的stream不是可以用imbue去设置区域吗,所以第一个想到的是

std::wstringstream wss;
wss.imbue(jpLoc);
wss << line;
std::wstring wline = wss.str();

可惜这样子根本不行。

那它内部到底调用了什么东西啊。那我用调试器一步一步跟踪,step in 进去,看看它到底干了什么。写了如下代码:

std::locale jpLoc("japanese");
std::wfstream fs("c:\\nanika.txt", std::ios::in);
fs.imbue(jpLoc);
std::wstring wsline;
getline(fs, wsline);

然后开始调试跟踪。一步步深入, 当调用堆栈变成这样子的时候:

在VS2012里,
std::basic_filebuf<wchar_t,std::char_traits<wchar_t> >::uflow() line 501 C++
std::basic_filebuf<wchar_t,std::char_traits<wchar_t> >::underflow() line 460 C++
std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::sgetc() line 153 C++
std::getline<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >(std::basic_istream<wchar_t,std::char_traits<wchar_t> > && _Istr, std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> > & _Str, const wchar_t _Delim) line 414 C++
std::getline<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> >(std::basic_istream<wchar_t,std::char_traits<wchar_t> > & _Istr, std::basic_string<wchar_t,std::char_traits<wchar_t>,std::allocator<wchar_t> > & _Str) line 485 C++

发现了转换的代码。 长这样的:

_Pcvt->in(_State,
&*_Str.begin(), &*_Str.begin() + _Str.size(), _Src,
&_Ch, &_Ch + 1, _Dest)

参数略难懂。虽然输入输出缓冲区的首位指针是很容易懂啦,那其他动东西呢……

果然还是上网去搜,搜出来它是这样说的,

_State是保存转换的状态。这个in函数,它是const的,这样的话就没办法记录转换的状态了。于是它用了这个_State来保存转换的状态。状态是啥?比如一个ShiftJIS的转换器,你送汉字的第一个字节进去, 它自然转不出来什么东西。那它就保存下来,然后等第二个字节。这个_State就是给它保存状态的。

再然后,_Src和_Dest,其实是保存了转换的进度。正常情况下它是会全部转完,但是如果转一半它遇上了什么错误,就会停下来。这个时候_Src和_Dest就能看到它转到哪里了。

然后于此对应的也有一个out函数。功能正好相反(一个是转过去,一个是转回来)。

好,那现在就是那个_Pcvt,我要怎么得到。因为它似乎构造函数还是析构函数的是不给用户用的,正常方法来构造肯定不行,它应该有留什么路吧。上网搜搜,发现标准库里面有这么一个函数给你取得这个对象的:

const std::codecvt<wchar_t, char, int>& std::use_facet< std::codecvt<wchar_t, char, int> >(const std::locale&)

对象拿到以后,就可以自己试验一下那种做法了:

#include <iostream>
#include <locale>
#include <vector>
#include <string>

using namespace std;

int main()
{
    locale jpLoc("japanese");
    typedef std::codecvt<wchar_t, char, std::mbstate_t> MyCodeCvt;
    const MyCodeCvt& cvt = std::use_facet<MyCodeCvt>(jpLoc);

    std::wstring wt(L"日本語");
    std::vector<char> buf(wt.length() * 2 + 1);
    const wchar_t* inpProgress;
    char* outProgress;
    mbstate_t state = 0;

    cvt.out(state,
        &wt[0], &wt[0] + wt.length(), inpProgress,
        &buf[0], &buf[0] + buf.size(), outProgress);
    if (inpProgress != &wt[0] + wt.length())
        std::cerr << "convert failed!" << std::endl;
    else {
        *outProgress = 0;

        std::string jpStr(&buf[0]);
        std::cout << jpStr << std::endl;
    }
    return 0;
}

うふふ♥

补充:

1、如果用locale jpLoc("japanese")构造雕像的话,再fs.imbue(jpLoc)塞到wfstream里面,这样输出数字的时候它也会给你用特殊写法,比如fs << 1000就给你"1,000"了。只要它管字符串编码转换而不管其他的,用locale jpLoc("japanese", std::locale::ctype)来构造。ctype指的是只管字符

2、C++的新标准里面似乎有个std::wstring_convert来给你干这事。更方便

 

 

发表评论