Вопрос или проблема
Контекст
Сегодня я хотел сохранить уникальные строки в файле, содержащем китайские символы. Я решил использовать утилиту sort
, потому что мне известен этот инструмент, и удалить повторяющиеся строки в файле так же просто, как использовать флаг -u
. Я узнал, что мне нужно изменить локаль, чтобы sort
работал корректно с китайскими символами. Я заметил, что использование различных локалей заставляло sort
вести себя по-разному. В этом посте я показал, что я нашел.
Я осознаю, что задача по удалению повторяющихся строк в файле может быть выполнена с использованием нескольких инструментов/языков программирования. Хотя я благодарен любому, кто предложит инструмент для выполнения этой задачи, меня больше интересует возможность узнать больше о локалях и о том, как они влияют на утилиты Unix.
Обнаруженные проблемы
Проблема № 1
Следующая информация о локали моей системы.
locale
LANG=en_US.UTF-8
LC_CTYPE="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=
Рассмотрим следующий файл с именем main.txt
䔍
䏝
Если я попытаюсь отсортировать его, используя en_US.UTF-8
в качестве моего $LANG
, я потеряю строку № 2.
sort -u main.txt
䔍
Я решил эту проблему, установив LANG
на zh_CN.UTF-8
.
export LANG=zh_CN.UTF-8
sort -u main.txt
䔍
䏝
Я создал вопрос об этой проблеме на Stack Overflow. Кто-то предложил мне изменить локаль, и это, похоже, сработало; я думал, что я решил проблему, но затем я нашел Проблему № 2 и Проблему № 3 (описанные ниже).
Проблема № 2
Теперь предположим, что мы добавляем две строки в наш файл main.txt
.
䔍
䏝
𠵇
𠳐
Если я выполню sort -u
на нем, имея LANG
, равный zh_CN.UTF-8
, я потеряю строку № 4.
export LANG=zh_CN.UTF-8
sort -u main.txt
䔍
𠵇
䏝
Я пробовал разные локали, которые начинались с zh.CN
, поскольку символы являются китайскими. Некомментированные локали – это те, которые я пытался использовать.
grep '^#\?zh' /etc/locale.gen
zh_CN.GB18030 GB18030
zh_CN.GBK GBK
zh_CN.UTF-8 UTF-8
zh_CN GB2312
#zh_HK.UTF-8 UTF-8
#zh_HK BIG5-HKSCS
#zh_SG.UTF-8 UTF-8
#zh_SG.GBK GBK
#zh_SG GB2312
#zh_TW.EUC-TW EUC-TW
#zh_TW.UTF-8 UTF-8
#zh_TW BIG5
Я заметил, что sort -u
работает так, как ожидается, при установке LANG
на zh_CN.GBK
или zh_CN.GB18030
. Я решил эту проблему, используя либо zh_CN.GBK
, либо zh_CN.GB18030
.
Вот доказательство того, что это работает так, как ожидается, при установке LANG
на zh_CN.GBK
.
export LANG=zh_CN.GBK
sort -u main.txt
䔍
𠵇
𠳐
䏝
Вот доказательство того, что это работает так, как ожидается, при установке LANG
на zh_CN.GB18030
.
export LANG=zh_CN.GB18030
sort -u main.txt
䔍
𠵇
𠳐
䏝
Проблема № 3
Предположим, что мы добавим еще две строки в наш файл main.txt
.
䔍
䏝
𠵇
𠳐
书
乙
Если я использую zh_CN.GB18030
или zh_CN.GBK
, я потеряю строку № 6.
export LANG=zh_CN.GB18030
sort -u main.txt
书
䔍
𠵇
𠳐
䏝
export LANG=zh_CN.GBK
sort -u main.txt
书
䔍
𠵇
𠳐
䏝
Вопрос
Какое значение для $LANG
я должен использовать, чтобы сделать так, чтобы sort
действительно сохранял уникальные строки в следующем файле?
䔍
䏝
𠵇
𠳐
书
乙
Я ранее показал, что использование zh_CN.GB18030
, zh_CN.GBK
, zh_CN.UTF-8
или zh_CN
не похоже на то, что работает.
sort -u
выводит по одному из каждой набора строк, которые коллируют одинаково (имеют идентичный порядок сортировки) в локали.
На системах GNU (системах, использующих GNU libc) во многих локалях многие символы¹ имеют неопределенный порядок сортировки, что означает, что они в конечном итоге сортируются одинаково, также есть символы, которые явно определены как имеющие одинаковый порядок сортировки.
Когда байты не могут быть декодированы в текст, они часто игнорируются или считаются имеющими одинаковый порядок сортировки².
Таким образом, для того чтобы sort -u
дал вам уникальные строки на основе сравнения байт с байтом независимо от кодировки текста, в которой он написан, вам нужна локаль, где каждый байт может быть декодирован в символ и локаль с полным порядком.
Самый простой способ – использовать локаль C
, где есть соответствие один байт == один символ (некоторые байты могут вести к неопределенным символам), а сортировка основана на значении байта (по крайней мере, на системах с ASCII) и это единственная локаль, которую вы гарантированно найдете на любой данной системе.
В вашем случае, чтобы понять, почему zh_CN
дает вам несколько лучшее поведение, вы можете взглянуть на определения локалей обычно в /usr/share/i18n/locale
на системах GNU, где для zh_CN
вы найдете:
LC_COLLATE
copy "iso14651_t1_pinyin"
END LC_COLLATE
Где iso14651_t1_pinyin
имеет:
copy "iso14651_t1_common"
И затем указывает порядок для многих дополнительных китайских символов, в то время как en_US
использует только iso14651_t1_common
.
Если вы установите LC_CTYPE
(через LANG
) на zh_CN.GB18030 или zh_CN.GBK и попытаетесь обработать файл, закодированный в UTF-8, sort
обычно не сможет декодировать строки в текст, поскольку байты текста, закодированного в UTF-8, обычно не формируют допустимый текст, закодированный в GB18030, и вы значительно усугубите проблему.
Здесь, если вы хотите получить уникальные (в соответствии с сравнением байт с байтом) строки, но также хотите получить “правильный” отсортированный вывод в вашей локали, вы можете сделать:
LC_ALL=C sort -u your-file | sort
Где первый sort
удаляет дубликаты, а второй сортирует оставшиеся строки в соответствии с порядком колляции вашей локали.
Мы используем LC_ALL
вместо LANG
или LC_CTYPE
+LC_COLLATE
, потому что это переменная, которая переопределяет все (LC_ALL
имеет приоритет над LC_individual_setting
, который имеет приоритет над LANG
), так что это все еще работает, если LANG
или LC_COLLATE
также установлены в окружении.
Теоретически, вы также могли бы сделать:
LC_ALL= LC_CTYPE=C LC_COLLATE=C sort
чтобы установить только настройки “типа символа” и “колляции”, оставляя другие категории локали без изменений (которые охватываются LANG
или LC_xxx
), но оставшаяся, используемая sort
, это LC_MESSAGES
, используемая, например, в сообщениях об ошибках, и если вы установите LC_CTYPE=C
, только сообщения на английском США могут быть отображены корректно, поскольку в них содержатся только символы ASCII. Например:
$ LC_MESSAGES=zh_CN LC_CTYPE=C sort --version
sort (GNU coreutils) 9.1
Copyright (C) 2022 Free Software Foundation, Inc.
??? GPLv3+:GNU ???????? 3 ?????? <https://gnu.org/licenses/gpl.html>?
????????:??????????????
?????????,????????
? Mike Haertel ? Paul Eggert ???
Вы найдете больше деталей на:
- Что делает “LC_ALL=C”?
- Использование uniq на юникодном тексте
- Какова разница между “sort -u” и “sort | uniq”?
¹ Строго говоря, основной единицей является не символ, а коллирующий элемент, который может состоять из более чем одного символа, как ch
, который сортируется между h
и i
в некоторых чешских локалях.
² Имейте в виду, что некоторые реализации sort
(не GNU sort
) также могут зависнуть на символах NUL или чрезмерно длинных строках.
Ответ или решение
Чтобы корректно обрабатывать китайские символы с помощью команды sort -u
, необходимо установить переменную окружения LANG
в значение, которое поддерживает полное кодирование и определенный порядок сортировки для китайских иероглифов. Как вы уже заметили, использование LANG=zh_CN.UTF-8
может не всегда давать ожидаемые результаты из-за неопределенного порядка сортировки в некоторых локалях, в то время как zh_CN.GBK
и zh_CN.GB18030
подчас ведут себя лучше.
Первое Решение: Использование LANG=zh_CN.GBK
или LANG=zh_CN.GB18030
Оба эти варианта обеспечивают корректное отображение и сортировку китайских символов. Тем не менее, следует помнить, что при использовании GBK
или GB18030
важно убедиться, что текстовые файлы закодированы в соответствующей кодировке. Ваша начальная кодировка — это UTF-8, и попытка декодировать UTF-8 символы в GBK
или GB18030
может привести к ошибкам.
Второе Решение: Использование LC_ALL=C
Для достижения стабильных результатов с уникальными строками, рекомендуется использовать LC_ALL=C
. Эта локаль обеспечивает порядок сортировки на основе значений байтов, что подходит для задач, где требуется сохранить уникальные строки независимо от их коллаторационной обработки:
LC_ALL=C sort -u main.txt | sort
Здесь первый вызов команды sort -u
удаляет дубли, а второй sort
сортирует результаты в соответствии с локализацией вашей системы, которая может быть настроена на поддержку китайских символов.
Обзор
В контексте выполнения сравнения, использование LC_ALL=C
предоставит вам более предсказуемые результаты при обработке уникальности строк. В случае необходимости работы с конкретными локалями, такими как zh_CN.GBK
или zh_CN.GB18030
, убедитесь, что ваш файл кодируется в нужной системе, чтобы избежать неправильного отображения или ошибок сортировки.
Для работы с текстами на китайском языке, установление правильной локали имеет важное значение, и использование комбинации LC_ALL
может быть наиболее универсальным решением, гарантируя соответствие как в задачах сортировки, так и в удалении дубликатов. Однако для специфических задач и при наличии информации о кодировке всегда стоит учитывать возможности конкретной локали.