Перейти к содержимому

Что считается одним символом?

Четыре разные системы считают один и тот же текст четырьмя разными способами. Для английского текста они совпадают — поэтому проблема остаётся невидимой, пока однажды не перестанут.

Проверено

Один эмодзи — четыре ответа

Возьмите поднятый большой палец с выбранным оттенком кожи или эмодзи «семья». Посчитайте его четырьмя способами — получите четыре числа, и ни одно не будет ошибочным.

Читатель видит один символ. Одна штука, одно нажатие Backspace, чтобы её удалить.

Регулярное выражение может увидеть семь. Эмодзи собран из нескольких кодовых точек, соединённых невидимыми связками: базовый эмодзи, модификатор, соединитель, ещё один эмодзи и так далее.

JavaScript сообщит одиннадцать. Он хранит текст 16-битными единицами, а всё, что выходит за базовый диапазон, занимает две такие единицы, так что каждая из этих кодовых точек может считаться дважды.

Столбец базы данных видит двадцать пять байт. В кодировке UTF-8 для хранения каждая кодовая точка занимает до четырёх байт.

Для обычного английского текста все четыре числа одинаковы. Именно поэтому проблема не видна, пока кто-нибудь не введёт имя с диакритикой, фразу на японском или один-единственный эмодзи — и поле, которое три года работало без сбоев, начинает обрезать данные.

Какое число означает ваш лимит

Практический вопрос никогда не звучит как «какой длины этот текст». Он звучит как «что именно ограничивает», и распространённых ответов четыре.

Столбец базы данных, объявленный как VARCHAR, в большинстве СУБД считает байты. Поэтому поле, спокойно принимающее 255 английских символов, принимает гораздо меньше японских — по три байта каждый — и поэтому имя с диакритикой иногда не помещается, хотя его вариант без диакритики помещался.

SMS — это 160 символов в собственном семибитном алфавите, и 70 на сообщение, как только хоть один символ выходит за его пределы. Одна «типографская» кавычка, вставленная из текстового редактора, или один эмодзи могут превратить сообщение из одной части в три — и утроить стоимость.

Валидатор формы на JavaScript почти всегда считает единицы UTF-16, поэтому поле с лимитом 280 символов может отвергнуть строку, которая выглядит намного короче, если в ней много эмодзи.

Человек считает то, что видит, — и только это определение имеет значение для заголовка, тега title или всего, у чего есть визуальные рамки.

Где подсчёты расходятся

Знать, какие входные данные вызывают проблемы, полезнее, чем знать теорию, — потому что эти данные предсказуемы.

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

Буквы с диакритикой и комбинируемые символы. «é» можно записать одной кодовой точкой, а можно — буквой «e» и следующим за ней комбинируемым акутом. Выглядят они одинаково, но сортируются по-разному, при сравнении не равны и считаются по-разному — и обе формы встречаются в реальных данных, часто в одном и том же столбце.

Нелатинские письменности. Китайские, японские и корейские иероглифы занимают в UTF-8 по три байта. Хинди, тайский, тамильский и арабский образуют кластеры, которые читатель воспринимает как один знак, а состоят они из нескольких кодовых точек.

Математические и музыкальные символы: они находятся за пределами базового диапазона, поэтому в UTF-16 считаются дважды.

Что с этим делать

Считайте то, что считает ваш лимит. Счётчик, который показывает все четыре числа, отвечает на вопрос сразу — это быстрее, чем рассуждать. В коде используйте подходящую функцию: Intl.Segmenter в JavaScript считает то, что видит читатель, len(s.encode("utf-8")) в Python считает байты, в PHP — mb_strlen, а не strlen.

Исправляйте столбец, а не форму. Если ограничение — поле базы данных, надёжное решение находится выше по цепочке: объявите его как utf8mb4 в MySQL или text в PostgreSQL. Старая кодировка «utf8» в MySQL, как известно, отводит на символ только три байта и вообще не может хранить эмодзи — давняя ловушка, из-за которой незаметно обрезалось огромное количество реальных данных. Аккуратный подсчёт в форме — лишь обходной путь для столбца, который следовало объявить иначе.

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

Родственный случай: base64

Об этом стоит сказать, потому что он преподносит такой же сюрприз. Кодирование в base64 берёт по три байта и записывает их четырьмя символами, поэтому результат примерно на треть больше — это арифметика, а не неэффективность. Вложение в 6 MB превращается примерно в 8 MB текста, поэтому файл, который с запасом укладывается в лимит, может быть отклонён после кодирования для передачи.

Тем же расчётом объясняется, почему письма с вложениями возвращаются на лимитах, которые они вроде бы соблюдают. Если сверяете размер с ограничением, сверяйте закодированный.

Частые вопросы

Почему поле на 255 символов не принимает более короткий текст?

Почти наверняка потому, что оно считает байты, а не символы. Буквы с диакритикой занимают в UTF-8 два байта, иероглифы CJK — три, так что 255 байт могут оказаться 85 японскими иероглифами. Правильное решение — объявить столбец как utf8mb4 или text.

Неужели один эмодзи стоит мне трёх SMS?

Может. Сообщение только из символов алфавита GSM получает 160 символов на часть; один символ за его пределами переводит всё сообщение в кодировку с 70 символами на часть. Так сообщение из 150 символов с одним эмодзи становится тремя частями вместо одной.

Какой подсчёт использовать для тега title?

Точно — ни один: поисковые системы обрезают заголовок по ширине в пикселях, а не по числу символов, поэтому заголовок из широких заглавных обрежется раньше, чем из узких строчных. Обычное эмпирическое правило — около 60 символов, и это ориентир, а не лимит.

Почему две одинаковые на вид строки не совпадают?

Обычно потому, что буква с диакритикой записана двумя разными способами: в одной строке — одной кодовой точкой, в другой — базовой буквой и комбинируемым знаком. Если перед сравнением привести обе к одной форме, они совпадут, — и делать это стоит в момент, когда данные попадают в вашу систему.

Подсчёт слов стабильнее подсчёта символов?

Для английского — в основном да. Но не везде: в китайском и японском слова не разделяются пробелами, в тайском тоже, поэтому подсчёт слов в этих языках требует сегментации, и разные инструменты дают разные ответы.