Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Функции работы со строками в PHP и utf-8

Статья переехала в мой гитхаб: https://github.com/codedokode/pasta/blob/master/php/strings-utf8.md . Ниже идет устаревшая и неточная версия.


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

Если тебе интересно, почему эти функции поддерживают только однобайтные кодировки, а не многобайтные, то причина в том, что они очень старые и написаны в то время (лет 40 назад) когда utf-8 и многобайтных кодировок еще не было.

Давай разберем пример. Допустим, у нас есть строка из русской буквы «щ» в кодирове utf-8. Попытаемся взять первую букву с помощью неправильной функции:

// Внимание! это неправильный код, не пиши так!
$s = "щ";
$x = substr($s, 0, 1);

Буква «щ» кодируется в utf-8 как 2 байта: 209 137 (я взял информацию тут: http://www.utf8-chartable.de/unicode-utf8-table.pl?start=1024&utf8=dec ). substr отрезает от строки не первую букву, а первый байт. Это значит, что в $x он положит 1 байт с кодом 209. В utf-8 это неверная последовательность, она не соответвует никакому символу (так как после 209 обязательно должно идти второе число). Ideone может вообще отказаться что-то отображать, встретив такой код.

То же самое, когда ты обращаешься к строке как к массиву: $s[0]. Эта команда берет не первую букву, а только первый байт строки. Естественно, такая программа не будет работать.

Функция strlen считает число байт (не букв) в строке. То есть в данном случае strlen($s) вернет нам 2.

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

Также, чтобы работать с русскими (и другими нелатинскими) буквами в регулярках, надо ставить в конце флаг u: preg_match("/[абвг]/u", $string). Иначе preg_match будет думать что работает с однобайтной кодировкой и будет видеть не одну букву, а 2 latin1-символа (так как русская буква кодируется как 2 байта). Например, буква л кодирующаяся как 208 187 будет восприниматься как 2 символа с кодами 208 и 187, то есть л (кодировка latin-1: http://en.wikipedia.org/wiki/ISO/IEC_8859-1#Codepage_layout ). Таким образом, регулярка будет работать некорректно и найдет не то.

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

Некоторые строковые функции без префикса mb тем не менее корректно работают с utf-8 и их можно использовать. Вот они: strtr (если передавать массив), str_replace, str_repeat, explode, addslashes, trim.

Не работают с utf-8: strrev, strlen, substr, strpos, ucfirst, wordwrap, str_pad и большинство других строковых функций, для работы которых нужно считать число символов. Не работает задание ширины в функциях вроде sprintf и printf.

mbstring.func_overload

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

@mgrechanik

This comment has been minimized.

Copy link

mgrechanik commented Dec 11, 2017

trim('миша вова', 'м'); неверно обрежет когда они в utf-8, на php 7 проверил.

@ghost

This comment has been minimized.

Copy link

ghost commented Mar 20, 2018

благодарю, полезная статья

@codedokode

This comment has been minimized.

Copy link
Owner Author

codedokode commented Jun 22, 2018

@mgrechanik, совершенно верно. trim() думает, что все символы занимают один байт, а буква м кодируется 2 байтами. trim() воспринимает эти байты как 2 символа и отрезает их с краев строки. При этом она может порезать какой-то другой символ, в котором используется тот же самый байт. Внесу исправления.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.