оригинальная версия версия для слабовидящих контрастная версия выключить изображения включить изображения тёмная версия светлая версия RSS FEED K2 NEWS
☰☰
Среда, 04 Ноябрь 2026 00:28

Почему на конце русского слова появляется ромбик

Знакомая картина: в русском тексте последняя буква слова вдруг превращается в чёрный ромбик с вопросительным знаком. Файл сохранён в UTF-8, кодировка страницы указана верно, а ошибка при этом упорно повторяется в одних заголовках и обходит другие. Причина почти всегда одна: программа где-то обращается с текстом как с последовательностью байтов, хотя на самом деле это последовательность символов. Разберёмся, как так получается.

Почему на конце русского слова появляется ромбик

Начнём с устройства UTF-8. Латиница занимает в нём один байт, поэтому программисты десятилетиями привыкали считать, что символ и байт — одно и то же. Кириллица устроена иначе: каждая русская буква занимает два байта. Например, строчная «х» записывается парой D1 и 85, и по отдельности эти байты ничего не значат. Если от пары оторвать вторую половину, первая превратится в некорректную последовательность, а браузер честно покажет символ замены — тот самый ромбик. Так «слабовидящих» превращается в «слабовидящи» с ромбиком на конце.

Кто же отрывает половину буквы? Нередко виновником оказывается регулярное выражение с последовательностью \R. В документации она описана как «любой вариант перевода строки», и разработчики охотно берут её вместо ручного перечисления \n и \r\n: так короче и кажется надёжнее. Но среди вариантов, которые понимает \R, есть символ NEL с кодом 0x85. В режиме символов это один редкий управляющий символ, и всё в порядке. Однако без флага, включающего режим UTF-8, движок видит текст как набор отдельных байтов и принимает байт 0x85 внутри буквы «х» за настоящий перевод строки. Именно поэтому ошибка годами живёт в чужом коде: на латинице она ни разу себя не проявит.

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

Коварство в том, что под подозрение первыми попадают совсем другие вещи. Начинают проверять кодировку исходных файлов, перекодировать их, искать в них битые байты, подбирать настройки вывода. Часть этих мер даже маскирует симптом: например, флаги, заменяющие некорректные последовательности символом замены, лишь прячут повреждение. Исходные данные при этом совершенно здоровы, и сколько ни чини файлы, результат не меняется, потому что текст портится не при хранении, а при разборе.

Лечится это просто. Во-первых, все регулярные выражения, работающие с русским текстом, стоит запускать с флагом /u: тогда движок оперирует символами и пополам их уже не разорвёт. Во-вторых, там, где важна точность, лучше отказаться от \R и \s и явно написать, какие переводы строки вы ожидаете: обычно достаточно \r\n, \n и \r. В-третьих, перед разбором полезно один раз проверить текст на корректность UTF-8 и очистить его, чтобы действительно битые данные не шли дальше. И наконец, осторожнее с функциями, которые работают по байтовым позициям, вроде substr, а также с trim, если в наборе символов лежат многобайтовые буквы: результат будет тем же.

Урок шире одной функции. Любая операция, которая считает, режет, сравнивает или разделяет текст по байтам, потенциально опасна для не латинских алфавитов, и это касается любого языка программирования. Так же ломаются обрезка строк до фиксированной длины, превью новостей и поиск по подстроке. Увидев ромбик на месте последней буквы, не спешите винить файлы и кодировки: сначала найдите место, где текст режется или чистится, и убедитесь, что оно понимает, что имеет дело с символами, а не с числами от нуля до двухсот пятидесяти пяти. Хорошая привычка — прогонять каждую новую функцию разбора текста через тестовую строку со всем русским алфавитом, включая редкие буквы вроде «ъ», «ё» и «щ»: проверка занимает минуту и ловит большинство подобных ошибок до того, как их увидят читатели.

Спонсоры: