问题 求istreambuf_iterator 澄清,阅读Unicode字符的完整文本文件


在Scott Meyers的“Effective STL”一书中,有一个将整个文本文件读入std :: string对象的好例子:

std::string sData; 

/*** Open the file for reading, binary mode ***/
std::ifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

/*** Read in all the data from the file into one string object ***/
sData.assign (std::istreambuf_iterator <char> (ifFile),
              std::istreambuf_iterator <char> ());

请注意,它以8字节字符的形式读取。这非常有效。最近虽然我需要读取包含Unicode文本的文件(即每个字符两个字节)。但是,当我尝试(天真地)更改它以将数据从Unicode文本文件读取到std :: wstring对象时,如下所示:

std::wstring wsData; 

/*** Open the file for reading, binary mode ***/
std::wifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

/*** Read in all the data from the file into one string object ***/
wsData.assign (std::istreambuf_iterator <wchar_t> (ifFile),
               std::istreambuf_iterator <wchar_t> ());

我得到的字符串虽然是宽字符,但仍然具有备用空值。例如,如果文件包含Unicode字符串“ABC”,则文件的字节(忽略Unicode引导字节0xFF,0xFE)为:     <'A'> <0> <'B'> <0> <'C'> <0>

上面的第一个代码片段将正确地导致(char)字符串的以下内容:
    sData [0] ='A'
    sData [1] = 0x00
    sData [2] ='B'
    sData [3] = 0x00
    sData [4] ='C'
    sData [5] = 0x00

但是,当运行第二个代码片段时,会不合需要地导致(wchar_t)字符串的以下内容:
    wsData [0] = L'A'
    wsData [1] = 0x0000
    wsData [2] = L'B'
    wsData [3] = 0x0000
    wsData [4] = L'C'
    wsData [5] = 0x0000

就好像文件仍在逐字节读取,然后只是简单地翻译成单独的wchar_t字符。

我本以为std :: istreambuf_iterator,专门用于wchar_t,应该导致文件一次读取两个字节,不应该吗?如果没有,那么它的目的是什么呢?

我已经追溯到模板(没有简单的壮举;-),并且迭代器确实似乎仍然逐字节地读取文件并将其传递给其内部转换例程,该例程尽职地说明转换是在每个字节之后完成的(不是只有在收到2个字节后)。

我已经搜索了网络上的一些网站(包括这个网站),看似这个看似微不足道的任务,但是没有找到这种行为的解释或者一个不需要更多代码而不是我认为应该是必要的替代方案(例如,Google搜索网络产生的第二个代码片段与可行的代码片段相同。

我发现的唯一有用的是以下内容,我认为这是一个骗子,因为它需要直接访问wstring的内部缓冲区,然后在那里输入-cerces。

std::wstring wsData; 

/*** Open the file for reading, binary mode ***/
std::wifstream ifFile (“MyFile.txt”, std::ios_base::binary); // Open for input, binary mode

wsData.resize (<Size of file in bytes> / sizeof (wchar_t));

ifFile.read ((char *) &wsData [0], <Size of file in bytes>);

哦,并预防不可避免的“为什么在二进制模式下打开文件,为什么不在文本模式下”问题,打开是故意的,好像文件在文本模式下打开(默认),这意味着CR / LF(“\ r \ n“或0x0D0A)序列将被转换为仅LF(”\ n“或0x0A)序列,而文件的纯字节读取将保留它们。无论如何,对于那些顽固分子来说,改变这一点并不令人惊讶,没有任何影响。

所以这里有两个问题,为什么第二种情况不能像人们预期的那样工作(即,那些迭代器会发生什么),以及将最喜欢的“kosher STL-way”加载到wstring中的Unicode字符文件是什么? ?

我在这里想念的是什么;它必须是愚蠢的东西。

克里斯


11961
2018-01-05 01:34


起源

欢迎使用Stack Overflow。感谢您撰写如此精确细致的第一个问题! - Shrey Gupta


答案:


你必须对SO感到失望,因为你之后没有得到第一个问题的答案 4和半个月。这是一个很好的问题,大多数好问题都得到了回答 (好或坏)几分钟之内。忽视你的两个原因可能是:

  • 你没有将它标记为“C ++”,因此许多可能提供帮助的C ++程序员将永远不会 注意到了(我现在已将其标记为“C ++”。)

  • 你的问题是关于unicode流处理,这是一个很酷的编码的想法。

阻碍你调查的误解似乎是这样的:你似乎 相信广泛的人物流, std::wfstream和宽字符串, std::wstring, 分别与“unicode stream”和“unicode string”相同,具体而言 它们分别与UTF-16流和UTF-16字符串相同。这些都不是真的。

一个 std::wifstream (std::basic_ifstream<wchar_t>)是一个转换的输入流 外部序列的字节到内部序列 wchar_t根据指定 或外部序列的默认编码

同样地 std::wofstream (std::basic_ofstream<wchar_t>)是一个输出流 转换内部序列 wchar_t 到外部字节序列, 根据a 外部序列的指定或默认编码

std::wstring (std::basic_string<wchar_t>)是一个简单存储的字符串类型 一系列的 wchar_t,不知道它们产生的编码 - 如果是 - 任何 - 。

统一 是一系列字节序列编码 - UTF-8 / -16 / -32,还有一些比较模糊的其他编码 - 相关的原则是UTF-ñ 使用1或更多的序列对字母进行编码 ñ每个符号的位单位。 UTF-16显然是您尝试阅读的编码 变成一个 std::wstring。你说:

我本以为std :: istreambuf_iterator,专门用于wchar_t,应该导致文件一次读取两个字节,不应该吗?如果没有,那么它的目的是什么呢?

但是一旦你知道了 wchar_t 不一定是2个字节宽(它在微软的C库中, 32位和64位,但在GCC中它是4字节宽),还有一个UTF-16代码点(字符) 不需要适合2个字节(它可能需要4个),你会看到指定一个提取 单位 wchar_t 不可能全部解码UTF-16流。

使用以下内容构造和打开输入流时:

std::wifstream ifFile ("MyFile.txt", std::ios_base::binary);

它准备从“MyFile.txt”中提取(某些字母表)字符到值 类型 wchar_t 它将从字节序列中提取出那些字符 文件根据指定的编码 std::locale当它进行提取时,它在流上有效。

您的代码未指定 std::locale 对于您的流,因此库的默认值生效。 该默认值是全局C ++语言环境,默认情况下是 “C”语言环境;并且“C”语言环境假设 I / O字节序列的“身份编码”,即1字节= 1个字符( 为文本模式I / O留出换行异常。

因此,当你雇用你的 std::istreambuf_iterator<wchar_t> 至 提取字符,通过转换每个字节进行提取 在文件中 wchar_t 它附加到 std::wstring wsData。字节 在文件中,如你所说:

0xFF,0xFE,'A',0x00,'B',0x00,'C',0x00

前两个,你打折为“unicode lead bytes”,确实是一个 UTF-16字节顺序标记(BOM),但在默认编码中它们就是它们。

因此分配给广泛的人物 wsData 正如你所观察到的那样:

0x00FF,0x00FE,L'A',0x0000,L'B',0x0000,L'C',0x0000

就好像文件仍在逐字节读取,然后只是简单地翻译成单独的wchar_t字符。

因为它正是发生了什么。

要阻止这种情况发生,您需要在开始从流中提取字符之前执行某些操作 告诉它它应该解码UTF-16字符序列。这样做的方法 在概念上相当曲折。你需要 imbue  有一个流 std::locale 拥有一个 std::locale::facet 这是一个实例化 std::codecvt<InternT, ExternT, StateT> (或源于此) 这将为流提供从UTF-16解码到的正确方法 wchar_t

但其中的要点是你需要将正确的UTF-16编码器/解码器插入流中 在实践中它是(或应该)足够简单。我猜你的编译器是最近的MS VC ++。 如果那是对的,那么您可以通过以下方式修复代码:

  • 添加 #include <locale> 和 #include <codecvt> 到你的标题
  • 添加行:

    ifFile.imbue(std::locale(ifFile.getloc(),new std::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>));

紧接着:

std::wifstream ifFile ("MyFile.txt", std::ios_base::binary);

这条新线的效果是“灌输” ifFile 使用相同的新区域设置 就像它已经拥有的那样 - ifFile.getloc()  - 但改进了编码器/解码器方面   - std::codecvt_utf16<wchar_t,0x10ffff,std::little_endian>。这个 codecvt 方面是 一个将解码UTF-16字符的最大值为 0x10ffff 进入小端 wchar_t 价值观(0x10ffff 是UTF-16代码点的最大值)。

当您调试到如此修改的代码时,您现在将找到它 wsData 只有4个宽字符长 那些人物是:

0xFEFF, L'A', L'B', L'C'

正如您所期望的那样,第一个是UTF-16小端BOM。

注意订单 FEFF 与申请前的情况相反 的 codecvt facet,向我们展示了little-endian解码是按照要求完成的。 它需要。只需删除即可编辑新行 std::little_endian, 再次调试它,然后你会发现它的第一个元素 wsData 变 0xFFFE  并且其他三个宽字符变成了象形图 IICORE 象形 字符集(如果您的调试器可以显示它们)。 (现在,每当一位同事 他们惊讶地发现他们的代码正在将英文Unicode变成“中文”, 你会知道一个可能的解释。)

你想要填充吗? wsData 没有领先的BOM,你可以做到这一点 再次修改新生产线并进行更换 std::little_endian 同 std::codecvt_mode(std::little_endian|std::consume_header)

最后,你可能已经注意到新代码中的一个错误,即一个2字节的错误 wchar_t 宽度不足以表示0x100000和0x10ffff之间的UTF-16代码点 可以阅读。

只要您必须阅读的所有代码点都存在于此中,您就会侥幸逃脱 UTF-16 基本的多语言平面, 跨越[0,0xffff],您可能知道所有输入都将永远遵守 约束。否则,一个16位 wchar_t 不适合目的。更换:

  • wchar_t 同 char32_t
  • std::wstring 同 std::basic_string<char32_t>
  • std::wifstream 同 std::basic_ifstream<char32_t>

并且代码完全适合将abitrary UTF-16编码文件读入字符串。

(使用GNU C ++库的读者将从v4.7.2开始发现 它尚未提供 <codecvt> 标准标题。标题 <bits/codecvt.h> 存在并且有些人会有时候毕业 <codecvt>,但此时它只是 出口专业 class codecvt<char, char, mbstate_t> 和 class codecvt<wchar_t, char, mbstate_t>,分别是身份 转换和ASCII / UTF-8和。之间的转换 wchar_t。解决OP的问题 你需要子类 std::codecvt<wchar_t,char,std::char_traits<wchar_t>::state_type>  你自己,按照 这个答案


11
2018-05-20 17:10



16位 wchar_t 适用于UTF-16代码,您只需记住每个代码点可能需要一个或多个代码 wchar_t。但是,如果您还记得屏幕上的每个“字形”无论如何都可以由许多代码点组成,那就不是那么大了。我几乎不考虑使用 wchar_t 就这样一个“虫子”。简直不便携。 - Mooing Duck
我已经尝试过这个但仍然不同意。取两单元代码点U + 1D11E(G-clef)。这将在包含的文件中进行UTF-16LE编码 FF FE 34 D8 1E DD。以我的修复方式阅读此文件(使用 std::consume_header,用MS VC ++ 2012构建) 但 保持流和字符串实例化 wchar_t。然后 wsData 最终长度为1和 wsData[0] 是 d11e。第一个代码单元已丢失。更换 wchar_t 同 char32_t 然后我们得到 1d11e,正确的。不知道MS库是否有错误,但无论如何我们都会破坏数据 wchar_t。 - Mike Kinghan
呃,是的,这将是MS库中的一个错误。我很确定它不应该这样做。 - Mooing Duck
等等,据说 这一页 这种行为符合规范! WTF! - Mooing Duck
是的,我们都是Chris Weiner第一个问题的明智者:) - Mike Kinghan