对于字符串中的每个字符都给出错误的结果

4

有一个字符串采用UTF-8编码,我可以从文件中读取它并将其写入另一个文件。但是当我尝试逐个加载该字符串中的每个字符时,结果不连贯。我很可能以非常错误的方式进行了操作,那么正确的方法是什么呢?

source.txt文件中的内容为:

afternoon_gb_1          ɑftənun

我写的代码是:

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

文本文件 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命令查看文件。或者你能否上传文件的一部分到某个地方,以便我们查看? - JeffUK
嗨,感谢您的快速回复。我现在已经上传了问题中的文件。 - Znap
2
这是完全可以预料的。你需要阅读关于 utf-8 的资料并理解它的工作原理。 - n. m.
1个回答

4
在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=字符; c.p.: 代码点数; c.p.代码单元在UTF-8中的表示方式; c.u.和c.p.用十六进制表示)

如何将代码点映射到代码单元的确切细节在许多地方有解释;基本上是:

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

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

c9 0a

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


现在,如果你想打印完整的码点(而不是码单元),std::string 就没有帮助了——std::string 对此一无所知,它本质上只是一个高级的 std::vector<char>,完全不了解编码问题;它只是存储/索引代码单元,而不是代码点。
然而,有第三方库可以帮忙处理这个问题;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序列;库版本在这方面可能表现更好)

另一方面,也可以内联只需保持相同代码单元所需的内容:

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.中的“声明计数”,我们只检查高位是否设置;在这种情况下,只要我们得到顶部两个字节设置为10(二进制中称为2),就继续打印 - 因为多个c.u. UTF-8序列的“连续c.u.”都遵循此模式。

1
太棒了,Matteo。虽然这可能不是你在这种情况下的本意,但是在没有引入不必要的废话的情况下,这里有足够的上下文和清晰度,实际上这是完美的“规范”答案。 - Lightness Races in Orbit
谢谢,这确实是一份令人欣慰的反馈。鉴于之前反应不佳,我一直在想是否完全错过了目标或者我的表述过于模糊。 - Matteo Italia
抱歉回复晚了。非常感谢您提供的详细答案,对我帮助很大!我只是想知道应该如何将那个外部的utf-8库链接到我的项目中。 - Znap

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