如何逐行将utf-16文件读入utf-8 std :: string

时间:2015-03-12 14:28:49

标签: c++11 utf-8 locale utf-16 codecvt

我正在处理需要utf8编码的std :: string变量的代码。我希望能够处理用户提供的文件,该文件可能具有utf-16编码(我不知道设计时的编码,但最终希望能够处理utf8 / 16/32),读取它行-by-line,并将每一行转发给代码的其余部分,作为utf8编码的std :: string。

我有c ++ 11(实际上是c ++ 11的当前MSVC子集)并且使用了1.55.0。我最终需要代码才能在Linux和Windows上运行。目前,我只是在Windows上使用Visual Studio 2013 Update 4进行原型设计,在Windows 7上运行。我对其他依赖项持开放态度,但他们需要建立一个已建立的跨平台(即windows和* nix)轨道记录,不应该是GPL / LGPL。

我一直在假设我似乎无法找到验证的方法,而且我的代码无效。

一个假设是,由于我最终希望std :: string变量中的这些文件中的每一行,我应该使用std :: ifstream,并使用正确构造的codecvt,以便可以转换传入的utf16流到utf8。

这个假设是否切合实际?我想,另一种选择是我必须对文本文件进行一些编码检查,然后根据结果选择wifstream / wstring或ifstream / string,这似乎比我想要开始时没有吸引力。当然,如果这是正确的(或唯一的现实的)路径,我愿意接受它。

我意识到我可能还需要进行一些编码检测,但就目前而言,我并不关心编码检测部分,只关注将utf16文件内容转换为utf8 std :: string。

我尝试了各种不同的locale和codecvt组合,但都没有。以下是我认为可行的最新版本,但不是:

void
SomeRandomClass::readUtf16LeFile( const std::string& theFileName )
{
    boost::locale::generator gen;
    std::ifstream file( theFileName );
    auto utf8Locale = gen.generate( "UTF-8" );
    std::locale cvtLocale( utf8Locale,
                           new std::codecvt_utf8_utf16<char>() );

    file.imbue( utf8Locale );
    std::string line;

    std::cout.imbue( utf8Locale );
    for ( int i = 0; i < 3; i++ )
    {
        std::getline( file, line );
        std::cout << line << std::endl;
    }
}

我在这段代码中看到的行为是每次调用getline()的结果都是一个空字符串,无论文件内容如何。

如果省略上述方法的第3行和第5行,这个相同的代码工作正常(意思是,每个getline()调用在同一文件的utf8编码版本上返回正确编码的非空字符串。)

无论出于何种原因,我都无法在SO或http://en.cppreference.com/或其他地方的任何地方找到任何试图做同样事情的例子。

欢迎所有想法/建议(符合上述要求)。

1 个答案:

答案 0 :(得分:7)

读UTF-16编写UTF-8

您需要澄清的第一个问题是,您正在阅读UTF16的变体:

  • 是UTF-16LE(即在Windows下生成)?
  • 是UTF-16BE(默认情况下由wstream生成)?
  • BOM的UTF16吗?

接下来的问题是要知道你是否真的可以在控制台上输出你的UTF8或UTF16,因为他们知道默认的Windows控制台确实可以引起头部争议。

步骤1:确保问题与win控制台无关

所以这里有一个小代码来读取UTF-16LE并使用本机Windows函数检查内容(您只需在控制台应用中包含<windows.h>):

    wifstream is16(filename);
    is16.imbue(locale(is16.getloc(), new codecvt_utf16<wchar_t, 0x10ffff, little_endian>()));
    wstring wtext, wline;
    for (int i = 0; i < 10 && getline(is16, wline); i++)
        wtext += wline + L"\n";
    MessageBoxW(NULL, wtext.c_str(), L"UTF16-Little Endian", MB_OK);

如果您的文件是包含BOM的UTF-16,请将litte_endian替换为consume_header

第2步:将utf16-string转换回utf8字符串

你必须使用字符串转换器:

    wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter;

    wifstream is16(filename);
    is16.imbue(locale(is16.getloc(), new codecvt_utf16<wchar_t, 0x10ffff, little_endian>()));
    wstring wline;
    string u8line; 
    for (int i = 0; i < 10 && getline(is16, wline); i++) {
         u8line = converter.to_bytes(wline);
         cout << u8line<<endl; 
    }

这将在win控制台上显示ascii字符。但是所有的utf8编码都会显示为垃圾(除非你设置控制台显示unicode字体,否则你比我更成功)。

第3步:使用文件检查utf8编码

由于win控制台非常糟糕,最好的方法是将您直接生成的字符集写入文件并使用文本编辑器(lke Notepad ++)打开此文件,它可以显示编码。

Nota bene: 所有这些只使用标准库(中间人MessageBoxW()除外)及其区域设置完成。

进一步的步骤

如果你想检测编码,首先要看的是文件的最开头是否有BOM(为二进制输入打开,默认&#34; C&#34;语言环境) :

char bom_utf8[]{0xEF, 0xBB, 0xBF};
char bom_utf16be[] { 0xFE, 0xFF};
char bom_utf16le[] { 0xFf, 0xFe};
char bom_utf32be[] { 0, 0, 0xFf, 0xFe};
char bom_uff32le[] { 0xFf, 0xFe, 0, 0};

只需加载前几个字节,然后与此数据进行比较。

如果您找到了,那就没问题了。如果没有,您将不得不遍历该文件。

如果你期望西方语言的快速近似如下:如果你发现很多空字节(> 25%<50%),它可能是utf16。如果你发现超过50%的空值,它可能是utf32。

但更精确的方法可能有意义。例如,要验证文件是否为UTF16,您只需要实现一个小型状态机,它可以检查第一个字在0xD8和0xDB之间的高字节,下一个字的高字节在0xDC和0xDF之间。如果它的小端或大端,那么什么是高,什么是低。

对于UTF8它是类似的做法,但是状态机有点复杂,因为第一个字符的位模式定义了必须跟随多少个字符,并且每个字符必须有一个位模式(c & 0xC0) == 0x80