Skip to content

Latest commit

 

History

History
executable file
·
103 lines (67 loc) · 16.1 KB

strings-utf8.md

File metadata and controls

executable file
·
103 lines (67 loc) · 16.1 KB

Функции работы с UTF-8 строками в PHP

Краткое содержание: используйте mb_* функции. Не используйте доступ к строке по индексу ($str[0]). В регулярных выражениях используйте флаг u (он говорит, что используется utf-8, а не однобайтовая кодировка).

Некоторые функции PHP (вроде strlen, substr, а также обращение к строке как к массиву: $str[0]) не работают с текстами в многобайтовых кодировках (вроде utf-8). Такие функции нормально работают со строкой из латинских букв, но если мы попытаемся передать строку с кирилицей, то буквы превращаются в непонятные символы или теряются.

Вот пример неправильно написанного кода:

var_dump(strlen("азъ")); // выводит int(6) вместо 3

var_dump(strrev("hello")); // выводит string(5) "olleh"
var_dump(strrev("аякс")); // выводит string(8) "�ѺЏѰ�" вместо "скяа"

$str = "хор";
var_dump($str[0]); // выводит string(1) "�" вместо первой буквы "х"

Чтобы понять, почему так происходит, придется вспомнить историю. В компьютерах данные в памяти хранятся в виде байтов, где байт можно представить целым числом от 0 до 255. Соответственно, текст тоже хранится в памяти в виде последовательности байтов. Первоначально для этого использовались однобайтовые кодировки вроде ASCII, в которых символы кодировались числами от 0 до 255, и таким образом один символ занимал в памяти ровно один байт. Написанный в то время код опирался на это предположение. Например, функция для определения длины строки просто смотрела, сколько байтов памяти она занимает и возвращала это число, не глядя на содержимое этих байтов.

К примеру, строка "hello" кодируется в ASCII как последовательность из 5 байтов 104, 101, 108, 108, 111.

Когда компьютеры стали распространяться по всему миру, 256 кодов стало недостаточно, чтобы представить десятки тысяч символов из различных языков и в 90-е годы пришлось переходить на Юникод (кодировка, которая пытается присвоить коды всем существующим символам из любых языков) и многобайтовые кодировки вроде utf-8. В utf-8 символ может кодироваться последовательностью длиной от 1 до 4 байтов. Латинские символы в utf-8 кодируются одним байтом с точно таким же кодом, как и в ASCII, а символы кириллицы - двумя.

Вот как кодируется в utf-8 слово "азъ": 208, 176, 208, 183, 209, 138. Подробно про кодировки и их историю я написал в отдельной статье про способ кодирования строк в памяти.

Старый код, рассчитанный на однобайтовые кодировки, не работает с utf-8. Например, функция определения длины строки, которая просто считает число байт в ней, вернет 6 вместо 3 для строки "азъ". Функция, которая отрезает первый символ строки, вернет один байт 208 вместо пары байт 208, 176, которые представляют букву "а". Функция, которая переворачивает строку, перепутает байты местами.

В PHP, к сожалению, функции вроде strlen используют такой устаревший код, и потому дают некорретные результаты. Вместо них надо использовать функции, которые "знают" о многобайтовых кодировках и корректно обрабатывают тексты в них. В PHP такие функции содержатся в расширении mbstring и имеют имена, начинающиеся с mb_, например, mb_strlen, mb_substr.

Для корректной работы этих функций надо сообщить им о том, какая кодировка используется. Это делается либо опцией default_charset в файле php.ini, либо функцией mb_internal_encoding в начале программы:

mb_internal_encoding('utf-8');

В большинстве случаев utf-8 уже задана как кодировка по умолчанию, и делать ничего не требуется, но описанные выше советы позволяют гарантировать, что кодировка задана правильно.

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

Не поддерживает utf-8 Поддерживает utf-8 Примечания
Взятие символа по индексу: $str[0] mb_substr($str, 0, 1)
chr mb_chr
lcfirst нету аналога Можно отрезать первую букву с помощью mb_substr(), перевести ее в нижний регистр mb_strtolower() и приклеить остаток строки
ord mb_ord ord() возвращает значение первого байта в строке от 0 до 255, а mb_ord() - код первого символа
str_pad нету аналога
str_shuffle нету аналога Можно разбить строку на массив символов, использовать shuffle() и собрать обратно в строку
strcasecmp нету аналога Можно привести обе строки в нижний регистр с помощью mb_strtolower() и сравнить с помощью класса Collator из расширения intl
strcmp, strcoll нету аналога Можно использовать класс Collator из расширения intl для сравнения и сортировки строк с учетом правил нужного языка, смотрите урок про сравнение строк
strlen mb_strlen
strpos mb_strpos
strrev нету аналога Можно разбить строку на массив символов, перевернуть массив с помощью array_reverse и склеить обратно
strtolower mb_strtolower
strtoupper mb_strtoupper
substr mb_substr
ucfirst нету аналога Можно отрезать первый символ с помощью mb_substr(), перевести в верхний регистр с помощью mb_strtoupper() и приклеить остаток строки
ucwords mb_convert_case с опцией MB_CASE_TITLE

Вот список менее популярных функций, которые тоже не поддерживают utf-8 и которые не стоит использовать: chunk_split, count_chars, levenshtein, similar_text, str_ireplace, stripos, str_split, str_word_count, strchr, strcspn, stristr, strnatcasecmp, strnatcmp, strncasecmp, strncmp, strpbrk, strrchr, strripos, strspn, strstr, strtok, substr_compare, substr_count, substr_replace, wordwrap.

Латиница и цифры кодируются в utf-8 одним байтом, с ними устаревшие функции работают, но все равно, не стоит использовать эти функции — это слишком ненадежно и легко сделать ошибку.

Эти функции работают корректно с utf-8: addslashes(), stripslashes(), explode(), implode(), htmlspecialchars(), nl2br(), number_format(), str_repeat(), strtr() при использовании с 2 аргументами (с массивом, а не со строками).

Функция trim()ltrim(), rtrim()) работает корректно с utf-8 только если мы отрезаем символы из ASCII, кодирующиеся одним байтом (например, перенос строки, пробел или латинскую букву). В других случаях, если, например, написать trim('миша вова', 'м'), она воспринимает букву м, закодированную двумя байтами, как два символа, и корежит исходную строку.

Функции из семейства printf/scanf (fprintf, vprintf, sprint, sscanf и тд) ошибочно считают длины строк в байтах, а не в символах. Например, printf("%10s", "азъ") посчитает, что "азъ" состоит из 6 символов и добавит 4 пробела для выравнивания, а не 7.

Чтобы работать с кирилицей (и другими нелатинскими) буквами в регулярках, надо ставить в конце флаг u: preg_match("/[абвг]/u", $string). Иначе preg_match будет думать, что работает с однобайтной кодировкой latin-1 и будет видеть не одну русскую букву, а 2 символа (так как русская буква кодируется как 2 байта). Например, буква л, кодирующаяся как 208 187, будет восприниматься как 2 символа с кодами 208 и 187, то есть л (в кодировке latin-1). Регулярка будет работать некорректно.

Разбиение строки на символы

Строку в utf-8 можно разбить на массив символов таким кодом:

$str = "Тест";
$chars = preg_split("//u", $str, null, PREG_SPLIT_NO_EMPTY);
var_dump($chars); // Массив ["Т", "е", "с", "т"]

Пустое регулярное выражение "срабатывает" на границах между буквами, а опция PREG_SPLIT_NO_EMPTY удаляет из массива два пустых элемента в начале и конце. Это позволяет использовать функции работы с массивами вроде array_reverse(). Собрать строку обратно из массива можно с помощью $str = implode("", $chars);.

Сравнение строк по алфавиту

Обычное сравнение строк в PHP (if ($s1 > $s2)) просто сравнивает байты, из которых они состоят, и не учитывает правила сортировки строк, которые зависят от языка. В этом уроке про корректную сортировку строк описано, как можно для этого использовать класс Collator из расширения intl.

Графемы

В Юникоде, кроме обычных, есть комбинирующие символы, которые позволяют добавлять какой-нибудь диакритический знак идущему перед ними символу. Например, буква m̊ составлена из символов m и знака кружочка над буквой. В utf-8 это кодируется как 109 (буква m), 204, 138 (знак кружочка). При печати эти 2 "символа" (которые правильно называть code points) комбинируются в одну графему (сгенерировать такие символы можно на сайте https://symbols.typeit.org/ ).

Если мы попробуем использовать функцию strlen("m̊ "), она вернет 3 - число байт, mb_strlen() вернет 2 - число использованных code points. Это может приводить к проблемам, например, если мы попробуем отрезать первый символ с помощью mb_substr(), то мы получим лишь букву m, а символ кружочка останется отдельно.

Для решения этой проблемы в PHP в расширении intl есть функции работы с графемами. Вот пример их использования:

var_dump(grapheme_strlen('')); // корректно выводит int(1)
var_dump(grapheme_substr('m̊x̂', 0, 1)); // string(3) "m̊"

mbstring.func_overload

В некоторых (неграмотных) учебниках можно увидеть совет включить опцию mbstring.func_overload (подробнее про нее: http://php.net/manual/ru/mbstring.overload.php ). Она подменяет часть строковых функций вроде strlen на их mb-аналоги. Не стоит так делать, так как это изначально неправильно спроектированная опция. Она не решает проблему, для которой задумывалась (включить в старом приложении, использующем функции вроде strlen, поддержку utf-8), а лишь создает путаницу. Например, при ее включении strlen заменяется на поддерживающую utf-8 mb_strlen, но ucfirst, trim или sprintf ни на что не заменяется и не работает.