我正在处理(希望)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
运行使用
C ++似乎至少对std::stringstream
读取文件的自定义C ++程序时也会出现重复项 - 使用en_US.UTF-8
语言环境时由于重复而失败。std::string
和输入/输出没有影响。
为什么在使用UTF-8语言环境时找到重复项,并且在C语言环境中找不到重复项?
导致此行为的文本区域设置有哪些转换?
编辑:Here是一个小例子
$ 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
我仍然得到相同的结果
编辑:根据iconv
- (来自here)
$ 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
答案 0 :(得分:7)
我已将问题归结为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
那么,这里有什么问题?为什么strcoll()
会为两个不同的字符返回0
(相等)?
答案 1 :(得分:3)
可能是由于Unicode normalization。 Unicode中的代码点序列是不同的,但被认为是等价的。
一个简单的例子是combining characters。像“é”这样的许多重音字符可以表示为单个代码点(U + 00E9,LATIN SMALL LETTER E WITH ACUTE),或作为未接受字符和组合字符的组合,例如两个字符序列&lt; U + 0065,U + 0301&gt; (拉丁文小写字母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
é
由n.m编辑并非所有Linux系统都执行Unicode规范化。
答案 2 :(得分:0)
此时纯粹猜想,因为我们无法看到实际数据,但我猜这样的事情正在发生。
UTF-8将代码点0-127编码为其代表字节值。上面的值需要两个或更多字节。有一个规范定义,哪个值范围使用一定数量的字节,以及这些字节的格式。但是,代码点可以以多种方式编码。例如-32,ASCII空间,可以编码为0x20(其规范编码),但是,它也可以编码为0xc0a0。这违反了对编码的严格解释,因此格式良好的UTF-8编写应用程序永远不会以这种方式编码。然而,解码器通常被编写为更宽容,以处理错误编码,因此在您的特定情况下UTF-8解码器可能会看到一个不是严格符合编码的代码点的序列并以最合理的方式解释它它可以的方式,这将导致它看到某些多字节序列等同于其他序列。区域设置整理顺序也会产生进一步的影响。
在C语言环境中,0x20肯定会在0xc0之前排序,但在UTF-8中,如果它抓取以下0xa0,那么该单个字节将被视为等于两个字节,因此将排序在一起。