在没有外部库的情况下正确读取utf-16文本文件到字符串中?

时间:2012-05-08 18:08:23

标签: c++ winapi unicode utf-16

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

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

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

编辑:我在使用VS2008时遇到了这个特殊的项目,所以C ++ 11代码可能无济于事。

编辑2:我意识到我以前借过的code并没有像我认为的那样在非英语字符上失败。相反,它在我的测试文档中的特定字符失败,其中包括':'(FULLWIDTH COLON,U + FF1A)和')'(FULLWIDTH RIGHT PARENTHESIS,U + FF09)。 bames53的发布解决方案也大部分都有效,但却被那些相同的角色所困扰?

编辑3(和答案!):我使用的原始代码-did-主要是工作 - 正如bames53帮助我发现的那样,ifstream只需要以二进制模式打开才能工作。

3 个答案:

答案 0 :(得分:10)

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';
}

答案 1 :(得分:8)

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

由于0x1a被认为是文件末尾的原因,请参阅此blog post from Raymond Chen跟踪Ctrl-Z的历史记录。它基本上是向后兼容性运行。

答案 2 :(得分: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. codecvt_utf8
  4. codecvt_utf16
  5. codecvt_utf8_utf16
  6. c32rtomb / mbrtoc32
  7. c16rtomb / mbrtoc16
  8. 每个人做什么

    1. 始终在UTF-8和UTF-32
    2. 之间转换的codecvt方面
    3. 在UTF-8和UTF-16之间进行转换
    4. 根据目标元素的大小(BMP外的字符可能被截断)在UTF-8和UCS-2或UCS-4之间进行转换。
    5. 使用UTF-16编码方案和UCS-2或UCS-4在一系列字符之间进行转换
    6. 在UTF-8和UTF-16之间进行转换
    7. 如果定义了宏__STDC_UTF_32__,则这些函数在当前语言环境的char编码和UTF-32之间进行转换
    8. 如果定义了宏__STDC_UTF_16__,则这些函数在当前语言环境的char编码和UTF-16之间进行转换
    9. 如果定义了__STDC_ISO_10646__,那么使用codecvt_utf16<wchar_t>直接转换应该没问题,因为该宏指示所有语言环境中的wchar_t值对应于Unicode包的短名称(因此暗示wchar_t足够大)持有任何这样的价值)。

      不幸的是,没有任何定义直接从UTF-16转到wchar_t。可以使用UTF-16 - &gt; UCS-4 - &gt; mb(如果__STDC_UTF_32__) - &gt; 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换行符不会自动转换,因此您需要手动执行此操作。