在Linux / POSIX中语言环境如何工作以及应用了哪些转换?

我正在处理大量的(我希望)UTF-8文本文件。 我可以使用Ubuntu 13.10(3.11.0-14-generic)和12.04来重现它。

在调查一个bug时,我遇到了奇怪的行为

$ export LC_ALL=en_US.UTF-8 $ sort part-r-00000 | uniq -d ɥ ɨ ɞ ɧ 251 ɨ ɡ ɞ ɭ ɯ 291 ɢ ɫ ɬ ɜ 301 ɪ ɳ 475 ʈ ʂ 565 $ export LC_ALL=C $ sort part-r-00000 | uniq -d $ # no duplicates found 

运行使用std::stringstream读取文件的自定义C ++程序时,也会出现重复项 – 在使用en_US.UTF-8语言环境时,重复项会失败。 至less对于std::string和input / output,C ++似乎不受影响。

为什么在使用UTF-8语言环境时find重复项,并且在C语言环境中找不到重复项?

导致这种行为的文本的语言环境有哪些变化?

编辑: 这是一个小例子

 $ uniq -D duplicates.small.nfc ɢ ɦ ɟ ɧ ɹ 224 ɬ ɨ ɜ ɪ ɟ 224 ɥ ɨ ɞ ɧ 251 ɯ ɭ ɱ ɪ 251 ɨ ɡ ɞ ɭ ɯ 291 ɬ ɨ ɢ ɦ ɟ 291 ɢ ɫ ɬ ɜ 301 ɧ ɤ ɭ ɪ 301 ɹ ɣ ɫ ɬ 301 ɪ ɳ 475 ͳ ͽ 475 ʈ ʂ 565 ˈ ϡ 565 

出现问题时的locale输出:

 $ locale LANG=en_US.UTF-8 LC_CTYPE="en_US.UTF-8" LC_NUMERIC=de_DE.UTF-8 LC_TIME=de_DE.UTF-8 LC_COLLATE="en_US.UTF-8" LC_MONETARY=de_DE.UTF-8 LC_MESSAGES="en_US.UTF-8" LC_PAPER=de_DE.UTF-8 LC_NAME=de_DE.UTF-8 LC_ADDRESS=de_DE.UTF-8 LC_TELEPHONE=de_DE.UTF-8 LC_MEASUREMENT=de_DE.UTF-8 LC_IDENTIFICATION=de_DE.UTF-8 LC_ALL= 

编辑:正常化后使用:

 cat duplicates | uconv -f utf8 -t utf8 -x nfc > duplicates.nfc 

我仍然得到相同的结果

编辑:该文件是有效的UTF-8根据iconv – (从这里 )

 $ iconv -f UTF-8 duplicates -o /dev/null $ echo $? 0 

编辑:看起来像是类似于这个: http : //xahlee.info/comp/unix_uniq_unicode_bug.html和https://lists.gnu.org/archive/html/bug-coreutils/2012-07/msg00072.html

它正在使用FreeBSD

我已经解决了问题与strcoll()函数,这是不相关的Unicode规范化问题。 回顾一下:我最小的例子演示了取决于当前语言环境的uniq的不同行为:

 $ echo -e "\xc9\xa2\n\xc9\xac" > test.txt $ cat test.txt ɢ ɬ $ LC_COLLATE=C uniq -D test.txt $ LC_COLLATE=en_US.UTF-8 uniq -D test.txt ɢ ɬ 

显然,如果语言环境是en_US.UTF-8 uniq会将ɢɬ作为重复项,这不应该是这种情况。 然后我用valgrind再次运行相同的命令,并用kcachegrind调查两个调用图。

 $ LC_COLLATE=C valgrind --tool=callgrind uniq -D test.txt $ LC_COLLATE=en_US.UTF-8 valgrind --tool=callgrind uniq -D test.txt $ kcachegrind callgrind.out.5754 & $ kcachegrind callgrind.out.5763 & 

唯一的区别是, LC_COLLATE=en_US.UTF-8的版本叫做strcoll()LC_COLLATE=C没有。 所以我strcoll()的以下最简单的例子:

 #include <iostream> #include <cstring> #include <clocale> int main() { const char* s1 = "\xc9\xa2"; const char* s2 = "\xc9\xac"; std::cout << s1 << std::endl; std::cout << s2 << std::endl; std::setlocale(LC_COLLATE, "en_US.UTF-8"); std::cout << std::strcoll(s1, s2) << std::endl; std::cout << std::strcmp(s1, s2) << std::endl; std::setlocale(LC_COLLATE, "C"); std::cout << std::strcoll(s1, s2) << std::endl; std::cout << std::strcmp(s1, s2) << std::endl; std::cout << std::endl; s1 = "\xa2"; s2 = "\xac"; std::cout << s1 << std::endl; std::cout << s2 << std::endl; std::setlocale(LC_COLLATE, "en_US.UTF-8"); std::cout << std::strcoll(s1, s2) << std::endl; std::cout << std::strcmp(s1, s2) << std::endl; std::setlocale(LC_COLLATE, "C"); std::cout << std::strcoll(s1, s2) << std::endl; std::cout << std::strcmp(s1, s2) << std::endl; } 

输出:

 ɢ ɬ 0 -1 -10 -1     0 -1 -10 -1  ɢ ɬ 0 -1 -10 -1     0 -1 -10 -1  ɢ ɬ 0 -1 -10 -1     0 -1 -10 -1 

那么,这里有什么问题? 为什么strcoll()为两个不同的字符返回0 (等于)?

这可能是由于Unicode规范化 。 Unicode中的代码点序列是不同的,但被认为是等价的。

一个简单的例子是组合字符 。 许多重音字符(如“é”)可以表示为单个代码点(U + 00E9,LATIN小字母E WITH ACUTE),也可以表示为不接受字符和组合字符的组合,例如双字符序列< U + 0065,U + 0301>(拉丁文小写字母E,组合急性病)。

这两个字节序列明显不同,所以在C语言环境中,它们的比较是不同的。 但在UTF-8语言环境中,由于Unicode规范化,它们被视为相同。

这个例子是一个简单的双行文件:

 $ echo -e '\xc3\xa9\ne\xcc\x81' > test.txt $ cat test.txt é é $ hexdump -C test.txt 00000000 c3 a9 0a 65 cc 81 0a |...e...| 00000007 $ LC_ALL=C uniq -d test.txt # No output $ LC_ALL=en_US.UTF-8 uniq -d test.txt é 

nm编辑不是所有的Linux系统都做Unicode规格化。

纯粹猜测在这一点上,因为我们不能看到实际的数据,但我猜这样的事情正在发生。

UTF-8将代码点0-127编码为其代表字节值。 高于两个或多个字节的值。 有一个规范的定义,哪些值的范围使用一定数量的字节,以及这些字节的格式。 但是,代码点可以通过多种方式进行编码。 例如,32位的ASCII空间可以编码为0x20(它的规范编码),但也可以编码为0xc0a0。 这违反了对编码的严格解释,所以一个结构良好的UTF-8编写应用程序永远不会这样编码。 然而,解码器通常被写为更宽容,以处理错误的编码,因此UTF-8解码器在您的特定情况下可能会看到一个不是严格符合编码的编码点的序列,并将其解释为最合理的它可以,这会导致它看到某些多字节序列相当于其他。 语言环境排序顺序也会有进一步的影响。

在C语言环境中,0x20肯定会在0xc0之前被排序,但是在UTF-8中,如果它抓取了一个跟随的0xa0,那么这个单字节将被认为等于两个字节,所以会一起排序。