Рассылка закрыта
При закрытии подписчики были переданы в рассылку "Килограмм килобайтов" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
Клуб профессиональных программистов :: Выпуск #13
Кодировки символов и как с ними бороться в PHP и JavaScript.
Поговорим о кодировках символов, используемых в браузерах, и работе с ними в серверных и клиентских скриптах. Для начала - небольшой обзор кодировок, немного теории, позже перейдем к проблемам и их решению. Какие кодировки бывают?
Кодировки символов делятся на два типа: универсальные (единые для всех языков) и узкого назначения. Первая группа называется unicode (utf, ucs). Кодировки бывают 8-и, 16-и и 32-х битные. Бывают еще 7-и битные, но на сегодня это экзотика.
Кодировок море и как тут можно не запутаться... Тем более, что большинство сайтов используют 8-и битные кодировки (но не utf-8), и они не всегда совпадают с кодировкой по умолчанию в браузере пользователя. Как сервер может узнать, какую кодировку предпочитает браузер?
Очень просто: в протоколе http предусмотрен параметр заголовка запроса Accept-charset. В нем браузер может передать желаемые и допустимые кодировки. Дело сервера - подстроиться (если сможет), либо сообщить свою кодировку. Если браузер ее не сообщает (этим грешит IE, когда выполняется ввод ссылки вручную в адресную строку браузера), сервер, как правило, использует свою кодировку по умолчанию или ту, что сообщит браузеру серверное приложение. Как браузер может узнать кодировку полученного документа?
Под документом тут я понимаю html, xml, а также документы, которые можно отнести к plain text (чистый текст). Способов несколько. Прежде всего, сервер может сообщить об этом в поле для дополнительной информации в параметре заголовка ответа Content-type. Например: Content-type: text/html; charset=windows-1251 Для plain text это - единственный способ. Для html и xml кодировку еще можно сообщить внутри документа. Для xml - в атрибуте encoding тега , для html - в теге meta. Примеры: <?xml version="1.0" encoding="windows-1251"?> <meta http-equiv="content-type" content="text/html; charset=windows-1251"/> Если кодировка указана в двух местах - в http-заголовке и в самом документе, то браузер должен использовать указанную в документе. Если данных о кодировке нет, то браузер либо выбирает кодировку, установленную в нем по умолчанию, либо пытается определить ее автоматически, по содержимому. Какие проблемы бывают у серверных скриптов?
Прежде всего - понять, в какой кодировке прислал данные браузер. Вообще то, об этом можно не думать - пусть браузер думает, прежде чем посылать, да и пользователю проще переключить кодировку при ощущении несовпадения желаемого и наблюдаемого. Потому что php-скрипту не передается никакой информации о кодировках. Например, если запрос сделан по методу POST, то в заголовке должен быть параметр Content-type, в котором может быть указана кодировка посланных данных, но в массиве $_SERVER она никак не отображается. При запросе GET информации о кодировках вообще не предусмотрено. Единственное, что видит скрипт - желаемые кодировки, переданные в параметре http-заголовка Accept-encoding, скопированные в переменную $_SERVER['HTTP_ACCEPT_CHARSET']. Можно попытаться определить кодировку автоматически, но в этой статье я не ставил такой задачи. Обычно процесс происходит так:
Кодировку для отсылаемых браузеру данных обычно выбирают одну - которая используется в серверном скрипте. Реже, она соответствует кодировке принятых от браузера данных или пользователь ранее перешел по специально предложенным ссылкам, по которым сервер понимает желаемую кодировку. Для поддержки нескольких кодировок можно держать на сервере несколько копий скриптов в разных кодировках, но это неудобно в поддержке, т.к. при внесении изменений в один файл придется создать его копии в других кодировках. Да и данные БД придется перекодировать. Есть решение лучше. Скрипт и БД работают только с одной кодировкой. Входные данные придется перекодировать, перебрав массивы $_GET и $_POST и пропустив каждый элемент массива через iconv(). Для перекодирования выходных данных придется их пропустить через iconv(). Это можно сделать следующими способами:
Следующая проблема - кодированные символы, полученные сервером по методу GET и POST. В строке запроса (query string - часть url после знака "?") могут быть символы: в 8-и битной кодировке (очень не желательно), в виде шестнадцатеричной записи "%XX" (php это автоматически декодирует в 8-и битную и, если может, разбивает на части и помещает в массив $_GET) и в виде шестнадцатеричной unicode-записи "%uXXXX" (такого вида запись выдает функция escape() в JavaScript). Последнюю запись php (по крайней мере 4.3.x) не обрабатывает и ее придется декодировать в скрипте. <?php function unicodeUrlDecode($url, $encoding = "") { if ($encoding == '') { if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) { preg_match('/^\s*([-\w]+?)([,;\s]|$)/', $_SERVER['HTTP_ACCEPT_CHARSET'], $a); $encoding = strtoupper($a[1]); } else $encoding = 'CP1251'; // default } preg_match_all('/%u([[:xdigit:]]{4})/', $url, $a); foreach ($a[1] as $unicode) { $num = hexdec($unicode); $str = ''; // UTF-16(32) number to UTF-8 string if ($num < 0x80) $str = chr($num); else if ($num < 0x800) $str = chr(0xc0 | (($num & 0x7c0) >> 6)) . chr(0x80 | ($num & 0x3f)); else if ($num < 0x10000) $str = chr(0xe0 | (($num & 0xf000) >> 12)) . chr(0x80 | (($num & 0xfc0) >> 6)) . chr(0x80 | ($num & 0x3f)); else $str = chr(0xf0 | (($num & 0x1c0000) >> 18)) . chr(0x80 | (($num & 0x3f000) >> 12)) . chr(0x80 | (($num & 0xfc0) >> 6)) . chr(0x80 | ($num & 0x3f)); $str = iconv("UTF-8", "$encoding//IGNORE", $str); $url = str_replace('%u'.$unicode, $str, $url); } return urldecode($url); } ?> Дополнительный параметр в IGNORE позволяет удалять символы, которые нет возможности перевести в указанную кодировку. Также сделана попытка определить желаемую кодировку по $_SERVER['HTTP_ACCEPT_CHARSET']. При передаче данных на сервер методом POST браузер должен использовать кодировку, используемую на странице. Т.е., теоретически, кодировки браузера и сервера должны совпадать. Иногда браузер кодирует посылаемые данные в виде записей html entity - "&#N;", где N - от 1 до 6 десятичных цифр, либо в шестнадцатеричном формате - "&#xXXXX;". Когда и при каких условиях это происходит, я пока не понял, но способ борьбы с такими записями есть - html_entity_decode(). Правда, у него есть один существенный недостаток – в данных, полученных от браузера, php уже заменил некоторые entity (& > < " '). Может сложиться ситуация, когда в этом, уже декодированном тексте, встретится такая же последовательность символов и перекодировка произойдет еще раз. На этот случай я написал программу, которая декодирует только entity, кодирующие unicode-символы. <?php $GLOBALS['_HTMLEntitiesDecode_encoding'] = ''; $GLOBALS['_HTMLEntitiesDecode_replacements'] = array( 160 => '"', 171 => '"', 187 => '"', 8220 => '"', 8221 => '"', 8211 => '-', 8212 => '-', 9552 => '-' ); function _HTMLEntitiesDecode_iconv($num) { $encoding = $GLOBALS['_HTMLEntitiesDecode_encoding']; // UTF-16(32) number to UTF-8 string if ($num < 0x80) $str = chr($num); else if ($num < 0x800) $str = chr(0xc0 | (($num & 0x7c0) >> 6)) . chr(0x80 | ($num & 0x3f)); else if ($num < 0x10000) $str = chr(0xe0 | (($num & 0xf000) >> 12)) . chr(0x80 | (($num & 0xfc0) >> 6)) . chr(0x80 | ($num & 0x3f)); else $str = chr(0xf0 | (($num & 0x1c0000) >> 18)) . chr(0x80 | (($num & 0x3f000) >> 12)) . chr(0x80 | (($num & 0xfc0) >> 6)) . chr(0x80 | ($num & 0x3f)); $tmp = @iconv("UTF-8", "$encoding//IGNORE", $str); if (strlen($tmp) != '') // если перекодировка прошла успешно return $tmp; // если такого символа в нашей кодировке не оказалось // но он широко распространен, то его можно заменить руками if (isset($GLOBALS['_HTMLEntitiesDecode_replacements'][$num])) return $GLOBALS['_HTMLEntitiesDecode_replacements'][$num]; return "&#$num;"; // в крайнем случае, оставляем все как было } function _HTMLEntitiesDecode_cb($matches) { $str = $matches[1]; if ($str{0} == 'x') $str = hexdec(substr($str, 1)); else $str = 0 + preg_replace('/^0+(?=\d)/', '', $str); return _HTMLEntitiesDecode_iconv($str); } function HTMLEntitiesDecode($str, $encoding = '') { if ($encoding == '' && $GLOBALS['_HTMLEntitiesDecode_encoding'] == '') { if (isset($_SERVER['HTTP_ACCEPT_CHARSET'])) { preg_match('/^\s*([-\w]+?)([,;\s]|$)/', $_SERVER['HTTP_ACCEPT_CHARSET'], $a); $encoding = strtoupper($a[1]); } else $encoding = 'CP1251'; // default. // Обязательно в верхнем регистре - так требует iconv() } $GLOBALS['_HTMLEntitiesDecode_encoding'] = $encoding; return preg_replace_callback('/&#(x[[:xdigit:]]{4}|\d{1,5});/', "_HTMLEntitiesDecode_cb", $str); } ?> У таких замен тоже есть один маленький недостаток. Как я уже говорил, связан он с тем, что php самостоятельно декодирует данные запроса, но не декодирует unicode-вставки. Если пользователь намеренно ввел в форме, к примеру, А (кириллическая буква "А" в юникоде), то php-скрипт не будет об этом знать. То же самое, если намеренно ввести %u0410 (та же буква "А") в строке адреса браузера. Выход? Переходить на unicode-кодировки! Для этого нужно, чтобы utf-8 или utf-16 поддерживалась системой и php, а иначе функции работы со строками будут работать совсем не так, как хотелось. Какие проблемы могут быть у клиентского скрипта на JavaScript?
Прежде всего, если в нем есть не-ascii символы, он должен быть в той же кодировке, что и страница, на которой он запущен. Также, если скрипт динамически создает элементы, имеющие внешние ссылки, или скрипт использует AJAX с методом GET, то компоненты строки запроса, перед составлением их в url, нужно закодировать функцией escape(). Напоминаю, что при этом на сервере следует ожидать конструкций "%uXXXX". Этой информации достаточно для понимания возможных проблем и их решений. Если же что-то показалось вам непонятным или у вас есть иные не менее интересные проблемы - прошу писать на наш форум: http://forum.shelek.com. Роман Чернышов (RXL) 25.06.2006.
А теперь прощаемся с Вами до следующего выпуска. С уважением, команда Клуба. |
В избранное | ||