2013-11-26 1 views
10

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

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

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

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

Edit: 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=     

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

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

Я до сих пор получаю такие же результаты

Edit: Файл действует UTF-8 в соответствии с iconv - (от here)

$ 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

+0

Это может помочь, если вы предоставили небольшой файл с фактическими строками, которые показывают другое поведение. Удалите строки до тех пор, пока вы не получите ровно одну пару строк, которые дублируются в локали en_US, а не в C. – Random832

+0

Это файл в формате 1.1gb. Я изучаю это. –

+0

Ну, во-первых, посмотрите, произойдет ли это в первых 10 000 строк или так (с помощью команды 'head') - или, может быть, grep для одного из символов, который появляется в одной из строк, выводимых uniq. – Random832

ответ

6

У меня проблема с проблемой с функцией 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-8uniq лечит ɢ и ɬ дубликатов, которые не должен быть так. Затем я снова запускал те же команды с 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 (равно) для двух разных символов?

+2

'strcoll()' не просто сравнивает символы: он сравнивает символы для сортировки. Поскольку для используемых вами символов не определен порядок сортировки (сортировки), определенный на английском языке, они 'strcoll()' находятся в одном и том же месте (то есть они находятся в одном и том же месте в порядке, образованном функцией 'strcoll()'). – ninjalj

+0

@ninjalj: Я думаю, что это отвечает на весь вопрос. Для меня имеет смысл не определять порядок для символов, которые не являются частью собственного/используемого языка. Спецификацию можно найти здесь [Unicode Collation Algorithm] (http://www.unicode.org/reports/tr10/). –

+1

В вашем последнем примере '' \ xa2 "' и '' \ xac "' недействительны строки UTF-8, нет смысла их сортировать в соответствии с языковой версией UTF-8. Просто нитпик. –

2

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

Один простой пример: combining characters. Многие акцентированные символы, такие как «é», могут быть представлены как одна кодовая точка (U + 00E9, LATIN SMALL LETTER E WITH ACUTE), или как комбинация как непринятого символа, так и комбинирующего символа, например. двухсимвольная последовательность < U + 0065, U + 0301> (LATIN SMALL LETTER 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 
é 

Редактируйте н.м. Не все системы Linux выполняют нормализацию Юникода.

+0

Не уверен, что я допустил ошибку, но здесь, похоже, не работает: Я нормализую данные, используя: 'cat duplicates | uconv -f utf8 -t utf8 -x nfc> duplicates.nfc' и по-прежнему получать те же результаты для 'sort | uniq -d' в зависимости от локали –

+0

@mt_: Хм, тогда происходит что-то еще. FWIW Я не могу воспроизвести проблему с данными теста, которые вы опубликовали. Независимо от того, что 'LC_COLLATE' и' LC_ALL', вывод 'uniq -d' всегда пуст для меня. –

+0

Ничего себе. Проблема была в машине Ubuntu 12.04. Я только что загрузил файл и могу воспроизвести проблему дубликатов на машине Ubuntu 13.10. –

0

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

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

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

+0

Я добавил данные в сообщение.Я поместил файл через 'uconv' и выполнил нормализацию NFC, но проблема все еще остается. Я пытаюсь проверить правильность UTF-8. –

+0

UTF-8 действует. Добавлено редактирование. –