如何在不使用外部库的情况下正确地将utf-16文本文件读入字符串?

10
我从一开始就使用StackOverflow,并且偶尔会想要发布问题,但我总是要么自己解决,要么最终找到答案...直到现在。这似乎应该相当简单,但我已经在互联网上转了几个小时了,没有成功,所以我来这里求助:
我有一个非常标准的utf-16文本文件,其中混合了英文和中文字符。我希望这些字符最终出现在一个字符串(技术上是一个wstring)中。我看到了很多相关的问题得到解答(在此处和其他地方),但它们要么试图解决更难的问题,即在不知道编码的情况下读取任意文件,或者在各种各样的“Unicode”编码之间进行转换,或者只是普遍混淆了“Unicode”作为一系列编码。我知道我正在尝试读取的文本文件的源头,它将始终是UTF16,它有BOM等,它可以保持原样。
我之前一直在使用这里描述的解决方案,对于所有英语文本文件都有效,但在遇到某些字符后,它停止读取文件。我找到的唯一其他建议是使用ICU,这可能有效,但我真的不想在分发应用程序时包含整个大型库,只是为了在一个地方读取一个文本文件。虽然我不关心系统独立性,但我只需要它在Windows中编译和工作。当然,不依赖于这一事实的解决方案会更好,但是如果使用stl并依赖于Windows体系结构的假设,或者甚至涉及win32函数或ATL的解决方案,我也会非常高兴;我只是不想必须像ICU这样的大型第三方库。
编辑:我在这个特定项目中使用VS2008,所以C++11代码可悲地无法帮助。
编辑2:我意识到我之前借用的代码并没有像我认为的那样在非英语字符上失败。相反,它在我的测试文档中的特定字符上失败,其中包括“:”(全角冒号,U+FF1A)和“)”(全角右括号,U+FF09)。bames53发布的解决方案也基本可行,但被这些相同的字符难住了?
编辑3(和答案!):我之前一直在使用的原始代码基本上是有效的-正如bames53帮助我发现的那样,ifstream只需要以二进制模式打开即可工作。

1
请展示一些代码。你正在调用哪个实际的API?ReadFile?fread?read? - bmargulies
如果您确信文本是UTF16,则不应该有问题。据我所知,中文通常会成为MBCS字符串,这是完全不同的东西。 - Mahmoud Al-Qudsi
3
_wfopen可以打开/翻译UTF-16编码的文件,然后可以通过fread函数将其读入字符串中。原文链接:http://msdn.microsoft.com/fr-fr/library/yeby3zcb%28v=vs.80%29.aspx - Benj
我看不出来为什么你链接的代码不能工作。它读取一个字节文件并将其类型转换为wchar_t*以初始化wstring。我唯一要检查的是文件是否以二进制模式打开,但我不认为在那里犯错误会显示你的症状。 - Mark Ransom
@bmargulies(以及投票支持该评论的任何人):我链接到了我之前使用的代码,那是stl(ifstream/stringstream)。但只要是我可以访问的API,我就不会被绑定在特定的API上。 - neminem
显示剩余2条评论
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';
}

6
在像Windows这样具有双字节wchar_t的平台上,这将从UTF-16转换为UCS-2。特别是VS2010实现将超出BMP范围的字符截断。 - bames53
1
@bames53 的确如此。VS2010 可以正确地将这些字符读入 char32_t,但在 Windows 上无法对 UCS4 字符串进行太多操作。现在摆脱像 _O_U16TEXT 这样依赖于编译器的东西可能还为时过早。 - Cubbi
1
std::consume_header 在 VS2010 中似乎无法正常工作 -- BOM 被消耗了,但字节顺序没有受到影响。我不得不明确地使用 std::little_endian。 - Eugene
2
请注意,在 macOS 上,如果文件编码为 UTF-16 LE 并包含相应的 BOM,则必须显式设置 std::little_endian 而不是 std::consume_header。否则,输出将为 big endian。 - bfx
1
@ChrisGuzak std::codecvt并没有被弃用。codecvt头文件及其内容已经被弃用 - cppreference在https://en.cppreference.com/w/cpp/locale#Locale-independent_unicode_conversion_facets和各个页面上都有说明。 - Cubbi
显示剩余7条评论

10
打开UTF-16文件时,必须以二进制模式打开。这是因为在文本模式下,某些字符会被特殊解释 - 具体来说,0x0d会被完全过滤掉,而0x1a标记了文件的结尾。有一些UTF-16字符将其中一个字节作为字符代码的一半,并且会破坏文件的读取。这不是错误,而是有意行为,并且是分别具有文本和二进制模式的唯一原因。
关于为什么0x1a被视为文件结束,请参阅Raymond Chen的博客文章,追溯Ctrl-Z的历史。基本上,这是向后兼容性失控的结果。

5

编辑:

所以问题似乎在于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之间转换的codecvt facet
  2. 在UTF-8和UTF-16之间进行转换
  3. 在UTF-8和UCS-2或UCS-4之间进行转换,具体取决于目标元素的大小(BMP外的字符可能会被截断)
  4. 使用UTF-16编码方案的一系列char之间进行转换,并且用UCS-2或UCS-4表示
  5. 在UTF-8和UTF-16之间进行转换
  6. 如果定义了宏__STDC_UTF_32__,则这些函数将在当前区域设置的char编码和UTF-32之间进行转换
  7. 如果定义了宏__STDC_UTF_16__,则这些函数将在当前区域设置的char编码和UTF-16之间进行转换

如果定义了__STDC_ISO_10646__,则直接使用codecvt_utf16<wchar_t>进行转换应该没问题,因为该宏指示在所有语言环境中,wchar_t值对应于Unicode charters的短名称(因此意味着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 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的换行符不会自动转换,因此需要手动操作。


好的,事实证明,你的代码帮助我进行了调试 - 它停止读取示例文本文件中与我链接的代码(http://cfc.kizzx2.com/index.php/reading-a-unicode-utf16-file-in-windows-c)完全相同的位置。结果发现它并没有在一个中文字符处停止,而是在第一个全角冒号(U+FF1A)字符处停止读取。删除该字符后,它会在全角右括号(U+FF09)处停止。我感觉到了一种主题... - neminem
1
@neminem我猜我应该更仔细地看一下那个链接,它只是做了和我展示的一样的事情。我猜无论出于什么原因,VS 2008对fstream的实现不喜欢读取字节0xFF。这个字节代表“删除”。试着以二进制模式打开文件std::ifstream fin("...",std::ios::binary); - bames53
2
我的天啊,我花了一整天的时间来尝试解决这个问题,结果原来是如此明显?我试过其他方法,包括以二进制模式打开文件,但我从未尝试过最初的解决方案——只在二进制模式下打开它?你太厉害了。你应该将这个解决方案编辑到你的答案中,以防其他人在以后遇到这个问题(我想象不出我是唯一一个遇到这个问题的人):)。 - neminem
这不是一个 bug - 请看我的回答。 - Mark Ransom
@MarkRansom 这很有道理,但我本来以为只有当0x0D和0x0A一起出现时才会影响Windows。0x1A似乎是设计上的错误,但由于这些东西都没有标准化,最好在任何地方都不要使用文本模式。 - bames53

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接