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

Correctly reading a utf-16 text file into a string without external libraries?

本文关键字:文件 读取 字符串 文本 utf-16 外部 情况下      更新时间:2023-10-16

我从一开始就在使用 StackOverflow,偶尔会想发布问题,但我总是要么自己弄清楚,要么最终找到答案......直到现在。这感觉应该相当简单,但是我已经在互联网上徘徊了几个小时都没有成功,所以我转向这里:

我有一个非常标准的 utf-16 文本文件,混合了英文和中文字符。我希望这些字符最终出现在一个字符串中(从技术上讲,是一个 wstring(。我已经看到很多相关问题的回答(在这里和其他地方(,但他们要么希望解决在不知道编码的情况下读取任意文件或在编码之间转换的更困难的问题,要么只是普遍混淆"Unicode"是一系列编码。我知道我尝试读取的文本文件的来源,它将始终是 UTF16,它有一个 BOM 和所有内容,它可以保持这种状态。

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

编辑:我在这个特定的项目中坚持使用VS2008,所以C++11代码可悲地无济于事。

编辑2:我意识到我之前借用的代码并没有像我想象的那样在非英语字符上失败。相反,它在我的测试文档中的特定字符上失败,其中包括":"(全角冒号,U + FF1A(和"("(全角右括号,U + FF09(。Bames53 发布的解决方案也大多有效,但被这些相同的角色难倒了?

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

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

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

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

有关0x1a被视为文件末尾的原因,请参阅Raymond Chen的这篇博客文章,其中追溯了Ctrl-Z的历史。它基本上是向后兼容性横行无忌。

编辑:

因此,问题似乎是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

以及每个人做什么

  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(如果__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 sizen"; // 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换行符不会自动转换,因此您需要手动转换。