【问题标题】:For each char in string gives wrong result对于字符串中的每个字符给出错误的结果
【发布时间】:2017-07-09 09:41:18
【问题描述】:

有一个 UTF-8 编码的字符串,我可以从一个文件中读取它并将其写入另一个文件就可以了。但是当我尝试一个一个地加载该字符串中的每个字符时,结果并不一致。我很可能以非常错误的方式这样做,但正确的做法是什么?

source.txt中的内容是

afternoon_gb_1          ɑftənun

我写的代码是

while (source >> word >> word_ipa) { 
for (char& c : word_ipa)
 myfile <<word<<" is " << c<< endl;}

txt文件myfile中的内容写成

afternoon_gb_1 is �
afternoon_gb_1 is �
afternoon_gb_1 is f
afternoon_gb_1 is t
afternoon_gb_1 is �
afternoon_gb_1 is �
afternoon_gb_1 is n
afternoon_gb_1 is u
afternoon_gb_1 is n

【问题讨论】:

  • 我认为你有一个编码问题,它可能是一个 unicode 文件,所以需要这样处理它。你能在十六进制编辑器中加载文件并显示结果吗? Linux 上的 xxd 文件.. 或者您可以将文件的一部分上传到某个地方以便我们看到它吗?
  • 您好,感谢您这么快回复。我现在已经上传了问题中的文件。
  • 这是完全可以预料的。您需要阅读 utf-8 并了解其工作原理。

标签: c++ utf-8


【解决方案1】:

在 UTF-8 中,每个代码点(=逻辑字符)由多个代码单元(=char)表示; ɑftənun,尤其是:

ch| c.p. | c.u.
--+------+-------
ɑ | 0251 | c9 91
f | 0066 | 66
t | 0074 | 74
ə | 0259 | c9 99
n | 006e | 6e
u | 0075 | 75
n | 006e | 6e

(ch=character;c.p.:码位编号;c.p. UTF-8 编码单元表示;c.u. 和 c.p. 以十六进制表示)

解释了代码点如何映射到代码单元的确切细节in many places;最基本的是:

  • 小于 0x7f 的代码点直接映射到单个代码单元;对于这些,永远不会设置高位;
  • 从 0x80 开始的代码点被映射到多个代码单元;多代码单元序列中的所有代码单元都设置了高位;
  • 如果设置了高位,则高位具有特定含义;在多字节序列的第一个字节中,它们告诉预期有多少个连续字节,在其他字节中,它们被明确标记为连续字节。

如果您单独打印出每个代码单元,您将破坏需要表示多个代码单元的代码点的 UTF-8 编码。您的终端应用程序在第一行看到

c9 0a

(第一个代码单元后跟换行符),并立即检测到这是一个损坏的 UTF-8 序列,因为 c9 设置了高位但下一个 c.u.没有;因此是 � 字符。第二个字符以及 c.u. 也是如此。表示ə的部分序列。


现在,如果你想打印出完整的代码点(不是代码单元),std::string 不会有任何帮助 - std::string 对这些东西一无所知,它本质上是一个美化的std::vector&lt;char&gt;,完全忽略了编码问题;它所做的只是存储/索引代码单元,而不是代码点。

但是有第三方库可以帮助解决这个问题; utf8-cpp 是一个小而完整的;在您的情况下,utf8::next 函数会特别有用:

while (source >> word >> word_ipa) {
    auto cur = word_ipa.begin();
    auto end = word_ipa.end();
    auto next = cur;
    for(;cur!=end; cur=next) {
        utf8::next(next, end);
        myfile << word << "is ";
        for(; cur!=next; ++cur) myfile<<*cur;
        myfile << "\n";
    }
}

utf8::next 这里只是增加给定的迭代器,使其指向开始下一个代码单元的代码点;此代码确保我们将构成单个代码点的所有代码单元打印在一起。

请注意,我们可以非常简单地重现其准系统行为,只需阅读 UTF-8 规范(请参阅上面维基百科链接中的第一个表格):

template<typename ItT>
void safe_advance(ItT &it, size_t n, ItT end) {
    size_t d = std::distance(it, end);
    if(n>d) throw std::logic_error("Truncated UTF-8 sequence");
    std::advance(it, n);
}


template<typename ItT>
void my_next(ItT &it, ItT end) {
    uint8_t b = *it;
    if(b>>7 == 0) safe_advance(it, 1, end);
    else if(b>>5 == 6) safe_advance(it, 2, end);
    else if(b>>4 == 14) safe_advance(it, 3, end);
    else if(b>>3 == 30) safe_advance(it, 4, end);
    else throw std::logic_error("Invalid UTF-8 sequence");
}

这里我们利用了这样一个事实,即序列的第一个字节声明了要完成多少个额外的代码点来完成代码单元。

(请注意,这需要有效的 UTF-8,并且不会尝试重新同步损坏的 UTF-8 序列;库版本在这方面可能要好得多)

OTOH,也可以内联将相同代码单元保持在一起所必需的内容:

while (source >> word >> word_ipa) {
    auto cur = word_ipa.begin();
    auto end = word_ipa.end();
    for(;cur!=end;) {
        myfile << word << "is "<<*cur;
        if(uint8_t(*cur++)>>7 != 0) {
            for(; cur!=end && (uint8_t(*cur)>>6)==2; ++cur) myfile<<*cur;
        }
        myfile << "\n";
    }
}

在这里,我们完全忽略了第一个 c.u. 中的“声明计数”,我们只检查高位是否设置;在这种情况下,只要我们得到 c.u.,我们就会继续打印。前两个字节设置为 10(二进制,AKA 十进制 2) - 因为“延续 c.u.”多 c.u. UTF-8 序列都遵循这种模式。

【讨论】:

  • 很棒的答案,马特奥。虽然在这种特殊情况下可能不是您的意图,但这里有足够的上下文和清晰性(没有引入不必要的华夫饼),这实际上是完美的“规范”答案。
  • 谢谢你,这肯定是一个令人欣慰的反馈,因为反应不佳,我想知道我是否完全错过了目标,或者我在我的说明中过于手足无措。
  • 抱歉回复晚了。非常感谢您的详细回答,它帮助了很多!我只是想知道如何将外部 utf-8 库链接到我的项目
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-03-15
  • 2015-12-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多