Как работают локали в Linux / POSIX и какие преобразования применяются?

Я работаю с огромными файлами (надеюсь) текста UTF-8. Я могу воспроизвести его использую Ubuntu 13.10 (3.11.0-14-generic) и 12.04.

расследуя ошибка, я столкнулся с странной behavoir

$ 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 - он терпит неудачу из-за дубликатов при использовании en_US.UTF-8 locale. Кажется, что C++ не влияет, по крайней мере, на std::string и ввода/вывода.

почему дубликаты найдены при использовании локаля UTF-8 и никакие дубликаты не найдены с локалем C?

какие преобразования делает локаль к тексту, который вызывает это поведение?

Edit:здесь небольшой пример

$ 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=                   

Edit: после нормализации с помощью:

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

я все еще получаю то же результаты

Edit: файл действителен UTF-8 в соответствии с iconv (от здесь)

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

Edit: похоже, что-то похожее на это:http://xahlee.info/comp/unix_uniq_unicode_bug.html и https://lists.gnu.org/archive/html/bug-coreutils/2012-07/msg00072.html

он работает на FreeBSD

3 ответов


я свел проблему к проблеме с 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 (равно) для двух разных символов?


Это может быть из-за нормализации Unicode. В Юникоде есть последовательности кодовых точек, которые различны и все же считаются эквивалентными.

один простой пример этого -сочетания символов. Многие акцентированные символы, такие как "é", могут быть представлены как одна кодовая точка (U+00E9, Латинская маленькая буква E с острым),или как сочетание как неприемлемого символа, так и комбинирующего символа, например двухсимвольного последовательность (Латинская строчная буква 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
é

Edit by n.м. не все системы Linux делают нормализацию Unicode.


чисто гипотеза на данный момент, так как мы не можем видеть фактические данные, но я бы предположил, что что-то вроде этого происходит.

UTF-8 кодирует кодовые точки 0-127 как их репрезентативное значение байта. Значения выше, которые занимают два или более байтов. Существует каноническое определение того, в каких диапазонах значений используется определенное количество байтов, и формат этих байтов. Однако кодовая точка может быть закодирована несколькими способами. Например-32, пространство ASCII, может быть закодировано как 0x20 (его каноническая кодировка), но он также может быть закодирован как 0xc0a0. Это нарушает строгую интерпретацию кодировки, и поэтому хорошо сформированное приложение для написания UTF-8 никогда не будет кодировать его таким образом. Однако декодеры обычно пишутся, чтобы быть более снисходительными, иметь дело с ошибочными кодировками, и поэтому декодер UTF-8 в вашей конкретной ситуации может видеть последовательность, которая не является строго соответствующей кодированной кодовой точкой и интерпретировать ее наиболее разумным способом, который она может, что было бы заставьте его видеть определенные многобайтовые последовательности как эквивалентные другим. Последовательность сопоставления локали также будет иметь дополнительный эффект.

в локали C 0x20, безусловно, будет отсортирован до 0xc0, но в UTF-8, если он захватывает следующий 0xa0, то этот один байт будет считаться равным двум байтам, и поэтому будет сортировать вместе.