Одинаковые строки Python не равны: как проверить Unicode и нормализацию NFC

Диагностика визуально одинаковых строк: кодовые точки, канонические формы NFC и NFD, проверка коллизий и сохранение исходного текста перед сопоставлением данных.

Обновлено 14 мин чтенияУчёба и навыки
Клавиатура ноутбука с символами двух алфавитов

На экране совпадает, а сравнение возвращает False

На практикуме по обработке текста нужно сопоставить названия из двух небольших наборов. Одно название выглядит совершенно одинаково в обоих окнах, однако выражение left == right возвращает False. Поиск в словаре тоже не находит запись. Перепечатать слово вручную удаётся, но такая починка ничего не объясняет и не подходит для остальных строк. Возможная причина — разные последовательности кодовых точек Unicode, представляющие одну букву с диакритическим знаком. Проверить эту гипотезу можно на двух строках без установки дополнительных библиотек.

Разберём условную работу студентки Алины: она сравнивает подписи образцов, среди которых встречается французское слово café. Из одного источника буква é пришла готовым символом, из другого — сочетанием e и отдельного знака ударения. Нам нужен проверяемый способ сопоставления, который учитывает такое различие и сохраняет исходные записи. Прежде чем применять преобразование ко всему набору, Алина должна показать причину несовпадения, выбрать правило равенства и проверить, не объединит ли оно разные объекты в один ключ.

Отделите нормализацию от чтения файла

Начните с уже прочитанных строк Python типа str. Если вместо ожидаемых букв видны искажённые последовательности или символ замены, сначала выясните кодировку исходного файла. Нормализация не восстанавливает текст, который неверно декодировали или уже сохранили с потерями. Для учебной проверки полезно прямо записать границу: файл прочитан по подтверждённым правилам, нужные слова узнаваемы, подозрение касается представления отдельных букв. Так вы не будете одновременно менять кодировку, регистр, пробелы и форму Unicode, теряя возможность назвать причину результата.

Если данные поступили через CSV, убедитесь также, что сравниваете нужные поля одной записи. Ошибка разделителя и сдвиг колонок могут дать совсем другую строку под правильным заголовком. Однако после подтверждения структуры не нужно заново проходить весь импорт ради одного несовпадения. Сохраните пару исходных значений, их источники и номера записей. Диагностика начинается с маленькой воспроизводимой пары. Скриншот подойдёт как иллюстрация симптома, но для проверки нужны сами строки, поскольку изображение не содержит их внутреннюю последовательность.

Создайте пример с явно заданными кодовыми точками

В новом файле Python задайте left = 'caf\u00e9' и right = 'cafe\u0301'. В этих литералах запись с обратной косой чертой задаёт конкретную кодовую точку, поэтому пример не зависит от того, как клавиатура вводит акцент. Выполните print(left == right): ожидается False. Затем выполните print(len(left), len(right)): ожидаются 4 и 5. Если просто напечатать оба слова, при корректном отображении они будут выглядеть одинаково. Не копируйте одну напечатанную строку вместо обеих исходных: так легко случайно получить одинаковое представление.

Длина здесь считает кодовые точки строки Python, а не видимые пользователю буквы. В первом слове окончание представлено U+00E9, во втором — U+0065 и U+0301. Последняя точка обозначает комбинируемый акут, который отображается вместе с предшествующей буквой. Поэтому различие длин согласуется с одинаковым внешним видом. Из этого примера не следует, что любую видимую букву всегда можно измерить одним или двумя элементами: устройство письменностей сложнее. Для нашей ошибки достаточно понять, что число элементов str не обязано совпадать с числом видимых знаков.

Посмотрите последовательность, которую действительно сравнивает Python

Получите список числовых значений выражением list(map(ord, left)), затем таким же способом проверьте right. Для первой строки результатом будет [99, 97, 102, 233], для второй — [99, 97, 102, 101, 769]. Первые три значения совпадают, окончания различаются. Это более точное свидетельство, чем увеличение масштаба редактора или подбор другого шрифта. Шрифт влияет на изображение, но не превращает две разные последовательности в одну. Сохраните вывод рядом с исходными литералами, чтобы напарник мог повторить опыт.

Для понятных имён импортируйте модуль командой import unicodedata. Выражение unicodedata.name(left[-1]) вернёт LATIN SMALL LETTER E WITH ACUTE. У right последним окажется COMBINING ACUTE ACCENT, а предпоследним — LATIN SMALL LETTER E. Если анализируете произвольный символ, можно передать второй аргумент по умолчанию: unicodedata.name(character, 'без имени'). Это позволит диагностической печати продолжиться даже при отсутствии имени. Для основной пары уже достаточно кодов: названия помогают читать отчёт, но не устанавливают самостоятельное правило сопоставления данных.

Приведите обе стороны к одной канонической форме

После импорта unicodedata выполните left_key = unicodedata.normalize('NFC', left) и right_key = unicodedata.normalize('NFC', right). Теперь print(left_key == right_key) должен показать True. Исходные left и right при этом остаются прежними строками: результат преобразования вы сохранили в других переменных. Для нашей пары NFC приводит окончания к одинаковому представлению с é. Важно преобразовать обе стороны. Наличие NFC только у одной строки ничего не говорит о форме другой строки, полученной из независимого источника.

Форма NFD тоже позволяет сравнить эту пару: замените NFC на NFD в обоих вызовах, и равенство снова окажется истинным. Результат будет разложенным представлением с e и комбинируемым знаком. Канонические формы описаны стандартом Unicode; NFC использует каноническое разложение и последующую композицию, NFD оставляет каноническое разложение. Выберите одну форму для конкретного процесса сравнения. Фраза «обе строки нормализованы» неполна, если одна получена в NFC, а другая в NFD: между собой такие результаты всё ещё могут различаться.

Не пытайтесь склеивать все видимые знаки вручную

Удобный результат для é не означает, что NFC всегда превращает сочетание в единственную кодовую точку. Некоторые сочетания остаются последовательностями. Поэтому проверка успешности должна сравнивать выбранные нормализованные строки и смысловые ожидания, а не требовать длину в один элемент для каждой буквы. Не составляйте собственную таблицу замен акцентов. Даже небольшой учебный набор может содержать порядок комбинируемых знаков, который не учтёт такая таблица. Стандартная функция помогает применить установленное правило одинаково ко всем выбранным полям.

Алине достаточно короткого преобразования в месте подготовки ключа. Не нужно проходить по каждому символу и условием искать только U+0301. Такой код исправит демонстрационный акут, но будет выдавать частное решение за общее. С другой стороны, не стоит добавлять несколько форм нормализации подряд на всякий случай. В отчёте должно быть одно ясное правило и конкретный пример, который оно объясняет. Чем больше несвязанных преобразований добавлено одновременно, тем труднее проверить, какое из них создало новое совпадение.

Уточните, что считается одним объектом в задании

Равенство нормализованных подписей ещё не доказывает, что записи описывают один образец. Названия вообще могут повторяться у разных объектов. Если в наборе есть отдельный устойчивый идентификатор, выясните, почему соединение выполняется по названию. Нормализация помогает сравнивать текст по выбранному правилу, но не назначает идентичность реальным объектам. Для учебного задания допустимо использовать подпись как ключ, когда это прямо установлено условиями и её уникальность проверена. В остальных случаях сохраните результат как найденного кандидата, требующего дополнительного основания.

Запишите договорённость простым предложением: «Для сопоставления подписей считаем канонически эквивалентные последовательности одним написанием, регистр сохраняем». Тогда напарник понимает предел операции и может оценить спорную пару. Отдельно обозначьте, какие колонки затронуты. Не нормализуйте весь файл побайтно и не меняйте каждое текстовое поле автоматически. Исходная цитата, комментарий участника и технический идентификатор могут иметь разные правила обработки. В нашем примере ключ создаётся только для колонки подписи, а остальные данные сохраняются без дополнительного решения.

Проверьте коллизии до создания словаря

Представьте список labels = [left, right, 'cafe']. До преобразования он содержит три разных строковых значения. После применения NFC в обычном цикле получатся три рабочих значения, среди которых первые два совпадут. Сначала сохраните все результаты и исходные позиции, а не превращайте их сразу в словарь с единственной записью на ключ. Иначе совпадение скроет одну строку, прежде чем вы успеете проверить её происхождение. Для небольшого набора достаточно явно вывести исходное значение, его позицию и полученный ключ.

Практический аудит можно сделать с пустым словарём groups = {}. Для каждой пары position, original из enumerate(labels) вычислите key = unicodedata.normalize('NFC', original), затем выполните groups.setdefault(key, []).append((position, original)). После цикла выведите группы, где len(records) > 1. Каждая такая группа содержит несколько исходных записей на один рабочий ключ. Это кандидаты для разбора, а не команда удалить лишнее. В нашем наборе под ключом café будут позиции 0 и 1, под cafe останется позиция 2.

Различайте обычный повтор одинаковой исходной строки и схождение разных написаний после преобразования. Оба случая могут нарушить требование уникального ключа, но объясняются по-разному. Для каждого совпадения посмотрите идентификаторы образцов и значения остальных полей. Если первые две записи относятся к одному образцу, определите правило сохранения их сведений. Если к разным, выберите другой ключ. Молчаливое сохранение последней записи в словаре способно потерять измерение, даже когда сама нормализация выполнена совершенно правильно.

Сохраните оригинал и рабочий ключ рядом

В рабочей таблице оставьте original_label и добавьте match_key. В первой колонке хранится полученное написание, во второй результат NFC. Для воспроизводимости сохраните также имя источника и идентификатор записи, если он есть. Необязательно создавать сложную систему журналирования: для небольшого практикума достаточно явных колонок и короткого описания преобразования. Так спорную связь можно проверить по исходнику, а изменение выбранного правила не потребует угадывать утраченную последовательность. Оригинальный файл тоже оставьте отдельно от подготовленной копии.

При сопоставлении двух источников проведите аудит ключа на каждой стороне. Уникальность слева не гарантирует уникальность справа. После соединения проверьте число полученных строк и несколько конкретных связей. Отдельно посмотрите записи без соответствия и ключи с несколькими кандидатами. Рост доли совпадений сам по себе не подтверждает качество: он мог сопровождаться объединением разных объектов. Для Алины удачный результат означает объяснимое совпадение нужной пары и отсутствие незамеченной потери записей, а не максимально большой процент найденных названий.

Проверьте передачу подготовленного текста

После сохранения рабочей таблицы прочитайте её в той среде, где будет выполняться дальнейшее сравнение. Выберите исходную пару, проверьте сохранённые ключи и повторите равенство. Это помогает обнаружить, что в расчёт случайно попала первоначальная колонка или старая версия файла. Не оценивайте результат только по тому, что оба слова красиво отображаются на листе. Для передачи напарнику укажите точное имя колонки, используемой в связи, и приложите короткий пример ожидаемого результата. Иначе он может снова построить ключ из исходной подписи другим способом.

Если ключ составляется из нескольких текстовых частей, проверяйте итоговую собранную строку. Нормализованные отдельно фрагменты после соединения не обязаны оставаться нормализованными: граница может поставить комбинируемый знак рядом с предыдущей буквой. В учебном примере возьмите первую часть 'e', вторую — отдельный акут U+0301. Для выбранного процесса проще применить NFC к уже собранному ключу, а затем провести обычный аудит. Если разделитель между частями является частью формата, сохраняйте и его по договорённости. Не убирайте разделители ради совпадения названий: так разные пары частей способны дать одинаковый итог.

Зафиксируйте версию Python командой python --version в вашей среде, если результат включается в воспроизводимую лабораторную. Модуль также сообщает unicodedata.unidata_version. Не нужно менять установленную версию только ради показанного примера с é; запись окружения полезна для повторения работы и обсуждения отличий на необычных символах. Если сдаёте несколько файлов, проверьте, что пояснение относится к последнему сохранённому варианту. Пара корректных литералов в отдельном черновике не подтверждает, что основная программа действительно использует тот же ключ при поиске.

NFC не заменяет регистр и не исправляет похожие алфавиты

Выполните unicodedata.normalize('NFC', 'A') == unicodedata.normalize('NFC', 'a'): результат False. Прописная и строчная буквы остаются разными. Если задача требует сравнения без регистра, это дополнительное правило; в Python для него предусмотрен метод casefold. Не добавляйте его к нашей проверке автоматически. Сначала выясните, допустимо ли считать регистр незначимым для конкретных подписей или кодов, затем отдельно проверьте возникающие совпадения. Наличие нормализации не сообщает пользователю, что программа также перестала учитывать регистр.

Проверьте другую пару: латинская 'a' и кириллическая 'а'. Для воспроизводимости используйте '\u0061' и '\u0430'. После NFC они по-прежнему не равны. Это разные буквы, а их визуальное сходство не является канонической эквивалентностью. Если образец случайно назвали буквой из другой раскладки, исправление требует отдельного решения по правилам набора. Массовая замена всех похожих букв может испортить нормальные многоязычные названия. Диагностический вывод кодовых точек полезен в обоих случаях, но причины несовпадения и допустимые действия различаются.

Не переходите на NFKC ради большего числа совпадений

Кроме канонических форм существуют NFKC и NFKD, использующие совместимое разложение. Наглядная проверка: unicodedata.normalize('NFKC', '①') == '1' даст True, тогда как NFC сохранит обведённую цифру. Здесь преобразование убирает различие, которое в конкретном тексте могло быть значимым. Стандарт предупреждает о безусловном применении совместимых форм к произвольному тексту. В задании с обозначениями, математическими символами или исходными цитатами такое схождение нужно обсуждать отдельно, даже если результат визуально кажется удобнее для поиска.

Удаление всех диакритических знаков — ещё одно самостоятельное преобразование. В нашей паре café и cafe после NFC остаются разными словами: акцент не исчезает. Не разлагайте строку и не отбрасывайте комбинируемые символы только ради увеличения числа совпадений. Так можно объединить различающиеся названия и фамилии. Если преподаватель действительно поставил задачу поиска без диакритики, сформулируйте отдельную политику и её ограничения. Для устранения канонического различия между готовой é и последовательностью e с акутом удаление акцента не требуется.

Завершите проверку примерами, которые должны остаться разными

Для сдачи практикума подготовьте маленький набор ожидаемых результатов. Исходные café в двух представлениях различаются при прямом сравнении и совпадают после одинаковой NFC. Слово cafe без акцента остаётся отдельным. Прописная буква отличается от строчной. Латинская a отличается от кириллической. Наконец, две записи с совпавшим рабочим ключом сохраняются до разбора коллизии. Эти примеры проверяют как появление нужного равенства, так и сохранение границ. Проверка только одного положительного совпадения не обнаружит слишком широкую замену текста.

Запустите пример заново из сохранённого файла. В пояснении назовите исходные кодовые точки, выбранную форму, область преобразования и результат аудита ключей. Если после этого отдельные строки всё ещё не совпадают, снова посмотрите их последовательности: возможно, различаются пробелы, регистр или сами буквы. Не добавляйте очередную очистку без наблюдаемой причины. Готовая работа позволяет повторить преобразование, найти исходник каждой записи и объяснить, почему конкретная пара признана равной. Именно это делает исправление полезным за пределами одного удачно перепечатанного слова.

Если хотите сохранить пояснение рядом с другими материалами курса, зарегистрируйтесь в Prepodoff и добавьте файл с разбором в личные материалы. Ему можно указать предмет и выбрать свою папку. В пояснении оставьте исходную пару, ожидаемые результаты и правило подготовки ключа. Сам Python запускайте в своей учебной среде: материал в библиотеке помогает вернуться к объяснению, а проверку программы выполняйте там, где сохранён рабочий пример.

Была полезна статья?

Читайте также

Пригодится для подготовки