Php использовать utf 8

UTF-8 в PHP. Часть 1

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

1. Вступление

Для понимания дальнейшего текста начинающим нужно знать некоторые детали по кодировкам в целом. Подачу материала я постараюсь максимально упростить. Для незнающих ничего о побитовых операциях необходимо предварительно ознакомиться с материалами на википедии.

Начать нужно с понимания того, что компьютер работает с числами и хранить строку (и символ, как её часть) приходиться тоже в числовом виде. Для этих целей существуют кодировки. По сути это таблицы, в которых указано соответствие между числами и символами. Исторически сложилось, что основная кодировка ASCII содержит лишь контрольные коды и латинские символы, всего их 128 (127 – максимальное число, которое можно хранить в 7 битах).

Для того чтобы хранить и другие тексты на основе ASCII было создано много других кодировок, в которых добавили 8-ой бит. Они могут хранить уже до 256 символов, первые 128 с которых традиционно соответствовали ASCII, а вот в остальную часть каждый пихал всё, что ему хотелось. Так и получилось, что у каждого производителя операционных систем свои наборы кодировок, причём каждая удовлетворяла потребности лишь относительно узкого круга людей. Ситуацию ещё сильнее усложнили отсутствием общих стандартов, различать их алгоритмически стало невозможно и теперь это больше похоже на угадывание (об этом в следующих частях).

Читайте также:  Java reference source code

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

  • полная совместимость с ASCII;
  • её можно с высокой точностью отличить от других кодировок;
  • каждый символ может занимать от 1 до 4 байт (в стандарте байты называют октетами; внимание, я могу заменять эти термины друг другом!) в зависимости от числового значения, которое нужно хранить.

Хотелось бы подробнее остановиться на последнем пункте. Это значит, что если раньше можно было выполнять простое преобразование по таблице и записывать результат, то сейчас определён и метод сохранения этого результата, в зависимости от разрядности, которая требуется для его хранения. На примере принцип хранения вы можете увидеть в таблице (x – хранимые биты данных):

Бит Максимальное хранимое значение 1 октет 2 октет 3 октет 4 октет
Начальный октет Продолжающие октеты
7 U+007F 0xxxxxxx
11 U+07FF 110xxxxx 10xxxxxx
16 U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
21 U+10FFFF (по стандарту, но реально U+1FFFFF) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Легко заметить, что в старших битах начального октета всегда находится счётчик, указывающий на количество байт в последовательности – это количество ведущих единиц, после которых идёт ноль. Обратите внимание: если октет лишь один, то ведущая единица не указывается, благодаря чему начальные октеты легко отличить от продолжающих.

Для примера давайте посмотрим как строка «Привет Hi» будет выглядеть в кодировке UTF-8.

Шаг первый. Перевести каждый символ в его числовое представление (я буду использовать шестнадцатеричную систему исчисления) по таблице.

Привет Hi = 0x041F 0x0440 0x0438 0x0432 0x044D 0x0442 0x0020 0x0048 0x0069
Не забываем, что пробел – тоже символ.

Шаг второй. Конвертировать числа из шестнадцатеричной в двоичную систему. Используем калькулятор Windows 7 (в режиме программиста).

0x041F = 0000 0100 0001 1111
0x0440 = 0000 0100 0100 0000
0x0438 = 0000 0100 0011 1000
0x0432 = 0000 0100 0011 0010
0x0435 = 0000 0100 0011 0101
0x0442 = 0000 0100 0100 0010
0x0020 = 0010 0000
0x0048 = 0100 1000
0x0069 = 0110 1001
Для наглядности я добавил нули в старшие разряды. Обратите внимание: символы могут занимать разное количество байт.

Шаг третий. Перевести числовые представления в последовательности октетов UTF-8.

0x041F = 100 0001 1111 = 110xxxxx 10xxxxxx = 11010000 10011111
0x0440 = 100 0100 0000 = 110xxxxx 10xxxxxx = 11010001 10000000
0x0438 = 100 0011 1000 = 110xxxxx 10xxxxxx = 11010000 10111000
0x0432 = 100 0011 0010 = 110xxxxx 10xxxxxx = 11010000 10110010
0x0435 = 100 0011 0101 = 110xxxxx 10xxxxxx = 11010000 10110101
0x0442 = 100 0100 0010 = 110xxxxx 10xxxxxx = 11010001 10000010
0x0020 = 010 0000 = 0xxxxxx = 00100000
0x0048 = 100 1000 = 0xxxxxx = 01001000
0x0069 = 110 1001 = 0xxxxxx = 01101001
Счётчики выделены жирным. Обратите внимание: символы с кодами до 0x0080 сохраняются без изменений, это и есть совместимость с ASCII. Ещё следует понимать, что UTF-8 будет занимать в 2 раза больше места (2 байта) для русскоязычного текста, чем Windows-1251, которая использует лишь 1 байт.

В качестве решения можно записать всю последовательность подряд (надеюсь без ошибок): «11010000 10011111 11010001 10000000 11010000 10111000 11010000 10110010 11010000 10110101 11010001 10000010 00100000 01001000 01101001».

Проверить решение можно кодом:

$tmp = » ;
foreach ( explode ( ‘ ‘ , ‘11010000 10011111 11010001 10000000 11010000 10111000 11010000 10110010 11010000 10110101 11010001 10000010 00100000 01001000 01101001’ ) as $octet ) <
$tmp .= chr ( bindec ( $octet ) ) ;
>
echo $tmp ;

  1. Определить количество октетов в 1-ом символе и сохранить это значение;
  2. От первого байта отбросить счётчик октетов, остаток сохранить;
  3. Если в последовательности более 1 октета сдвигать остаток после операции 2 на 6 бит влево и записывать в них информацию с младших 6 бит последующего октета;
  4. Повторять с 1 пункта до удовлетворения :).

Оптимизированный PHP код, который позволяет получать числовое представление символов и обратную операцию (полную версию опубликую в конце цикла):

  1. class String_Multibyte
  2. /**
    * Возвращает десятеричное значение UTF-8 символа, первый октет которого находится на позиции $index в строке $char.
    * Суррогатные коды, символы с приватных зон, BOM и 0x10FFFE-0x10FFFF вернут FALSE.
    *
    * [. ] Функция была оптимизирована, потому содержит избыточный код.
    *
    * @author Andrew Dryga , .
    * @param string $char Строка с символом (символами).
    * @param int &$index Аргумент указывает на октет, в котором необходимо начать вычисление значение для символа. После вызова будет хранить позицию последнего октета, принадлежащего указанному символу.
    * @return int|false Десятерчиное значение символа или FALSE в случае обнаружения символа или байта, которые нужно проигнорировать.
    */
  3. public function getCodePoint( $char , & $index = 0 )
  4. // Получаем значение первого октета
  5. $octet1 = ord( $char [ $index ]);
  6. // Если оно попадает в диапазон ASCII кодов (имеет вид 0bbb bbbb), то возвращаем результат.
  7. if ( $octet1 >> 7 == 0x00 )
  8. return $octet1 ;
  9. > elseif ( $octet1 >> 6 != 0x02 )
  10. // Проверяем существование следующего октета
  11. if (! isset ( $char [++ $index ]))
  12. return false ;
  13. >
  14. // Получаем его значение
  15. $octet2 = ord( $char [ $index ]);
  16. // Проверяем его на валидность (должен иметь вид 10bb bbbb)
  17. if ( $octet2 >> 6 != 0x02 )
  18. — $index ;
  19. return false ;
  20. >
  21. // Оставляем только его нижние 6 бит
  22. $octet2 &= 0x3F ;
  23. // Проверяем счётчик и если октетов должно быть всего два, то формируем результат
  24. if ( $octet1 >> 5 == 0x06 )
  25. $result = ( $octet1 & 0x1F )
  26. // Результат должен быть в максимально сокращённой форме
  27. if ( 0x80 < $result )
  28. return $result ;
  29. >
  30. > else
  31. if (! isset ( $char [++ $index ]))
  32. return false ;
  33. >
  34. $octet3 = ord( $char [ $index ]);
  35. if ( $octet3 >> 6 != 0x02 )
  36. — $index ;
  37. return false ;
  38. >
  39. $octet3 &= 0x3F ;
  40. if ( $octet1 >> 4 == 0x0E )
  41. $result = ( $octet1 & 0x0F )
  42. // Проверяем минимальное значение; удаляем суррогаты, приватную зону и BOM
  43. if ( 0x800 < $result && !( 0xD7FF < $result && $result < 0xF900 ) && $result != 0xFEFF )
  44. return $result ;
  45. >
  46. > else
  47. if (! isset ( $char [++ $index ]))
  48. return false ;
  49. >
  50. $octet4 = ord( $char [ $index ]);
  51. if ( $octet4 >> 6 != 0x02 )
  52. — $index ;
  53. return false ;
  54. >
  55. $octet4 &= 0x3F ;
  56. if ( $octet1 >> 3 == 0x1E )
  57. $result = ( $octet1 & 0x07 )
  58. // Проверяем минимальное значение; Удаляем приватную зону и некоторые другие символы;
  59. // Удостовериваемся, что полученое значение не выходит за рамки зоны Unicode 10FFFF
  60. if ( 0x10000 < $result && $result < 0xF0000 )
  61. return $result ;
  62. >
  63. >
  64. >
  65. >
  66. return false ;
  67. >
  68. >
  69. /**
    * Возвращает UTF-8 символ по его коду.
    * [. ]
    * @author ur001 , .
    * @param string $codePoint Unicode character ordinal.
    * @return string|FALSE UTF-8 символ или FALSE в случае ошибки.
    */
  70. public function getChar( $codePoint )
  71. if ( $codePoint < 0x80 )
  72. return chr( $codePoint );
  73. > elseif ( $codePoint < 0x800 )
  74. return chr( 0xC0 | $codePoint >> 6 ) . chr( 0x80 | $codePoint & 0x3F );
  75. > elseif ( $codePoint < 0x10000 )
  76. return chr( 0xE0 | $codePoint >> 12 ) . chr(
  77. 0x80 | $codePoint >> 6 & 0x3F ) . chr( 0x80 | $codePoint & 0x3F );
  78. > elseif ( $codePoint < 0x110000 )
  79. return chr( 0xF0 | $codePoint >> 18 ) . chr(
  80. 0x80 | $codePoint >> 12 & 0x3F ) . chr( 0x80 | $codePoint >> 6 & 0x3F ) . chr(
  81. 0x80 | $codePoint & 0x3F );
  82. > else
  83. return false ;
  84. >
  85. >
  86. >

Метод getChar() был взят с библиотеки Jevix, я всё-равно уже видел этот код, хорошо его запомнил и даже при его реализации по памяти было бы нечестно не упомянуть автора.

Вы же можете протестировать получившийся класс при помощи кода:

  1. // Создадим экземляр объекта
  2. $obj = new String_Multibyte ();
  3. // Сформируем строку наиболее удобным для теста способом
  4. $tmp = » ;
  5. foreach ( explode ( ‘ ‘ , ‘11010000 10011111 11010001 10000000 11010000 10111000 11010000 10110010 11010000 10110101 11010001 10000010 00100000 01001000 01101001’ ) as $octet )
  6. $tmp .= chr ( bindec ( $octet ) );
  7. >
  8. // Строим карту кодов символов
  9. $map = array ();
  10. $len = strlen ( $tmp );
  11. for ( $i = 0 ; $i < $len ; $i ++)
  12. if ( true == ( $result = $obj ->getCodePoint ( $tmp , $i )))
  13. $map [] = $result ;
  14. >
  15. >
  16. // Очищаем строку и восстанавливаем её с карты
  17. $tmp = » ;
  18. $count = count ( $map );
  19. for ( $i = 0 ; $i < $count ; $i ++)
  20. $tmp .= $obj ->getChar ( $map [ $i ] );
  21. >
  22. // Выводим восстановленную строку
  23. echo $tmp , ‘
    ‘ .EOL;
  24. // Проверяем её на валидность (это самый простой способ)
  25. echo preg_match ( ‘#.#u’ , $tmp ) ? ‘Valid Unicode’ : ‘Unknown’ , ‘
    ‘ .EOL;

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

Чтобы быть уверенным, что текст не содержит ничего лишнего нужно удалить с него ненужные (непечатные, нарушающие разметку, неопределённые, суррогатные и т.п.) символы и провести нормализацию, об этом в следующей части.

P.S.:

Дальше будет про нормализацию, безопасность, определение кодировок и работу с UTF-8 в PHP.

Ссылки:

Источник

Установка локали UTF-8 в PHP

В любом PHP приложении нужно настраивать локаль и кодировку вне зависимости от настроек сервера. Это предотвратит неверное отображение и работу сайта при переезде на другой хостинг и других ситуаций.

Setlocale

Основная функция, в случаи успеха возвращает устанавливаемое значение или FALSE . Влияет на строковые функции, даты и т.д.

setlocale(LC_ALL, 'ru_RU.utf8');

Возможен вариант:

Вместо LC_ALL можно указать отдельную категорию функций, на которые будет влиять локаль:

  • LC_COLLATE – функции сравнения строк,
  • LC_CTYPE – функции преобразования и классификации строк,
  • C_MONETARYL – для функции localeconv(),
  • LC_NUMERIC – задает символ десятичного разделения,
  • LC_TIME – форматирование даты/времени,
  • LC_MESSAGES – для системных сообщений.

MB_string

Настройка функций для работы с многобайтовыми строками.

mb_internal_encoding('UTF-8'); mb_regex_encoding('UTF-8'); mb_http_output('UTF-8'); mb_language('uni');

Часовой пояс

От него зависит результат работы функций с датами, подробнее о настройке временной зоны.

date_default_timezone_set('Europe/Moscow');

Кодировка контента

Ещё можно явно указать в какой кодировке передается контент, отправив заголовок:

header('Content-type: text/html; charset=utf-8');

Код целиком

// Локаль. setlocale(LC_ALL, 'ru_RU.utf8'); mb_internal_encoding('UTF-8'); mb_regex_encoding('UTF-8'); mb_http_output('UTF-8'); mb_language('uni'); header('Content-type: text/html; charset=utf-8'); date_default_timezone_set('Europe/Moscow');

Источник

Оцените статью