8

我从一开始就一直在使用 StackOverflow,并且有时很想发布问题,但我总是自己想出来或者最终找到答案......直到现在。这感觉应该相当简单,但我已经在互联网上徘徊了几个小时没有成功,所以我转向这里:

我有一个非常标准的 utf-16 文本文件,混合了英文和中文字符。我希望这些字符以字符串结尾(从技术上讲,是一个 wstring)。我已经看到很多相关的问题得到了回答(这里和其他地方),但他们要么正在寻求解决在不知道编码的情况下读取任意文件的更困难的问题,要么在编码之间进行转换,或者只是对“Unicode”感到困惑" 是一系列编码。我知道我要读取的文本文件的来源,它始终是 UTF16,它有一个 BOM 和一切,它可以保持这种状态。

我一直在使用此处描述的解决方案,该解决方案适用于全英文文本文件,但在遇到某些字符后,它停止读取文件。我发现的唯一其他建议是使用ICU,这可能会起作用,但我真的不想在分发应用程序中包含整个大型库,而只是在一个地方读取一个文本文件。不过,我不关心系统独立性——我只需要它在 Windows 中编译和工作。不依赖该事实的解决方案会更漂亮,当然,但我会很高兴有一个使用 stl 的解决方案,同时依赖于关于 Windows 体系结构的假设,甚至是涉及 win32 函数或 ATL 的解决方案;我只是不想包含另一个像 ICU 这样的大型 3rd 方库。除非我想自己重新实现它,否则我是否仍然完全不走运?

编辑:对于这个特定的项目,我坚持使用 VS2008,所以 C++11 代码很遗憾无济于事。

编辑2:我意识到我之前借用的代码并没有像我想象的那样在非英文字符上失败。相反,它在我的测试文档中的特定字符上失败,其中包括':'(FULLWIDTH COLON,U+FF1A)和')'(FULLWIDTH RIGHT PARENTHESIS,U+FF09)。bames53 发布的解决方案也大多有效,但被那些相同的字符难住了吗?

编辑 3(以及答案!):我一直使用的原始代码 - 大部分都可以工作 - 正如 bames53 帮助我发现的那样,只需要以二进制模式打开 ifstream 就可以工作。

4

3 回答 3

11

C++11 解决方案(据我所知,自 2010 年以来,Visual Studio 在您的平台上支持)将是:

#include <fstream>
#include <iostream>
#include <locale>
#include <codecvt>
int main()
{
    // open as a byte stream
    std::wifstream fin("text.txt", std::ios::binary);
    // apply BOM-sensitive UTF-16 facet
    fin.imbue(std::locale(fin.getloc(),
       new std::codecvt_utf16<wchar_t, 0x10ffff, std::consume_header>));
    // read     
    for(wchar_t c; fin.get(c); )
            std::cout << std::showbase << std::hex << c << '\n';
}
于 2012-05-08T18:25:55.680 回答
8

当您为 UTF-16 打开文件时,您必须以二进制模式打开它。这是因为在文本模式下,某些字符会被特殊解释 - 具体来说,0x0d 被完全过滤掉,0x1a 标志着文件的结尾。有一些 UTF-16 字符将这些字节之一作为字符代码的一半,并且会弄乱文件的读取。这不是一个错误,它是故意的行为,并且是拥有单独的文本和二进制模式的唯一原因。

关于 0x1a 被认为是文件结尾的原因,请参阅Raymond Chen 的这篇博客文章,追溯 Ctrl-Z 的历史。它基本上是向后兼容运行异常。

于 2012-05-09T03:30:52.343 回答
4

编辑:

因此,问题似乎在于 Windows 在文本模式下将某些魔术字节序列视为文件的结尾。这可以通过使用二进制模式读取文件来解决,std::ifstream fin("filename", std::ios::binary);然后像您已经做的那样将数据复制到 wstring 中。



最简单的非便携式解决方案是将文件数据复制到 wchar_t 数组中。这取决于 Windows 上的 wchar_t 是 2 个字节并使用 UTF-16 作为其编码的事实。


以完全可移植的方式将 UTF-16 转换为特定于语言环境的 wchar_t 编码会有些困难。

这是标准 C++ 库中可用的 unicode 转换功能(尽管 VS 10 和 11 只实现了第 3、4 和 5 项)

  1. codecvt<char32_t,char,mbstate_t>
  2. codecvt<char16_t,char,mbstate_t>
  3. 编码cvt_utf8
  4. codecvt_utf16
  5. codecvt_utf8_utf16
  6. c32rtomb/mbrtoc32
  7. c16rtomb/mbrtoc16

以及每个人的作用

  1. 始终在 UTF-8 和 UTF-32 之间转换的编解码器方面
  2. 在 UTF-8 和 UTF-16 之间转换
  3. 根据目标元素的大小在 UTF-8 和 UCS-2 或 UCS-4 之间转换(BMP 之外的字符可能会被截断)
  4. 在使用 UTF-16 编码方案和 UCS-2 或 UCS-4 的字符序列之间进行转换
  5. 在 UTF-8 和 UTF-16 之间转换
  6. 如果定义了宏__STDC_UTF_32__,这些函数会在当前语言环境的字符编码和 UTF-32 之间进行转换
  7. 如果定义了宏__STDC_UTF_16__,这些函数会在当前语言环境的字符编码和 UTF-16 之间进行转换

如果__STDC_ISO_10646__已定义,则直接使用转换codecvt_utf16<wchar_t>应该没问题,因为该宏指示所有语言环境中的 wchar_t 值对应于 Unicode 宪章的短名称(因此意味着 wchar_t 大到足以容纳任何此类值)。

不幸的是,没有任何定义直接从 UTF-16 到 wchar_t。可以使用 UTF-16 -> UCS-4 -> mb (if __STDC_UTF_32__) -> wc,但是您会丢失任何在语言环境的多字节编码中无法表示的内容。当然,无论如何,从 UTF-16 转换为 wchar_t 都会丢失任何在语言环境的 wchar_t 编码中无法表示的内容。


所以它可能不值得移植,相反,您可以将数据读入 wchar_t 数组,或使用其他一些 Windows 特定工具,例如文件上的 _O_U16TEXT 模式。

这应该可以在任何地方构建和运行,但需要做出一系列假设才能真正起作用:

#include <fstream>
#include <sstream>
#include <iostream>

int main ()
{
    std::stringstream ss;
    std::ifstream fin("filename");
    ss << fin.rdbuf(); // dump file contents into a stringstream
    std::string const &s = ss.str();
    if (s.size()%sizeof(wchar_t) != 0)
    {
        std::cerr << "file not the right size\n"; // must be even, two bytes per code unit
        return 1;
    }
    std::wstring ws;
    ws.resize(s.size()/sizeof(wchar_t));
    std::memcpy(&ws[0],s.c_str(),s.size()); // copy data into wstring
}

您可能至少应该添加代码来处理字节序和“BOM”。此外,Windows 换行符不会自动转换,因此您需要手动进行转换。

于 2012-05-08T20:04:29.737 回答