Статьи

Горячие советы PHP UTF-8

В результате всех шумов, связанных с UTF-8 , я получил электронное письмо от Марека Гайера с некоторыми очень умными советами по работе с UTF-8. Далее следует обсуждение, иллюстрирующее, что происходит, когда вы зациклены на производительности и оптимизации (будьте осторожны — может быть скучно, в зависимости от вашей точки зрения).

Превосходящие функции mbstring с нативными реализациями PHP

Нативные функции PHP strtolower / strtoupper не понимают UTF-8 — они могут обрабатывать только символы в диапазоне ASCII плюс (могут) проверять настройку языка вашего сервера для получения дополнительной информации о символах. Последнее поведение фактически делает их « опасными » для использования в строке UTF-8, потому что есть вероятность, что strtolower может ошибочно принять байты в многобайтовых последовательностях UTF-8 за то, что он должен быть преобразован в строчные, нарушая кодировку. Это не должно быть проблемой, если вы пишете код для сервера, которым вы управляете, но это если вы пишете программное обеспечение для использования другими людьми.

Ограничение поведения локали

Оказывается, вы можете отключить это поведение языкового стандарта, ограничив ваш языковой стандарт языковым стандартом POSIX , что означает, что будут учитываться только символы в диапазоне ASCII (переопределяя независимо от настроек языкового стандарта вашего сервера), выполнив следующее;

<?php setlocale(LC_CTYPE, 'C'); 

Это должно работать на любой платформе (конечно, * на основе Nix и Windows) и оказывать влияние не только на strtolower () / strtoupper () — другие функции PHP собирают информацию из локали, такую ​​как метасимвол PCRE / w, strcasecmp () и ucfirst (), все из которых могут привести к неблагоприятным последствиям для UTF-8.

Единственная проблема, на мой взгляд, заключается в том, пишете ли вы распространяемое программное обеспечение; должно быть возиться с setlocale в первую очередь? Смотрите предупреждение в документации здесь — это может быть проблемой для Windows, где у вас только один серверный процесс — вы можете влиять на другие приложения, работающие на сервере.

Быстрое Преобразование Случая

Чтобы сделать возможным преобразование регистра (например, strtolower / upper) вне зависимости от mbstring (потому что, кто знает, установили ли его общие хосты?), Такие приложения, как Mediawiki (как в Википедии) и Dokuwiki, решают эту проблему путем реализации версий PHP на чистом PHP. эти функции и использование таких массивов, как этот или этот (переменная $ UTF8_LOWER_TO_UPPER в конце сценария), которая работает, потому что только ограниченный выбор алфавитов в первую очередь имеет представление о регистре — массив большой, но не настолько большой, что это ужасная производительность накладных расходов. Что интересно отметить в обоих этих массивах поиска, так это то, что они содержат символы в диапазоне ASCII. Они также поддерживают много алфавитов.

Затем Mediawiki (по сути) делает str_to_upper следующим образом (по крайней мере, в выпуске 1.7.1 — см. languages/LanguageUtf8.php — похоже, это изменилось с тех пор в SVN);

// ... bunch of stuff removed return preg_replace( "/$x([az]|[\xc0-\xff][\x80-\xbf]*)/e", "strtr( "$1" , $wikiUpperChars )", $str );
// ... bunch of stuff removed return preg_replace( "/$x([az]|[\xc0-\xff][\x80-\xbf]*)/e", "strtr( "$1" , $wikiUpperChars )", $str ); 

… он находит каждую действительную последовательность символов UTF-8 и выполняет PHP-функцию strtr () с массивом поиска с помощью обратного вызова — модификатора шаблона / e (время звонить другу ?) Для преобразования регистра. Это сохраняет использование памяти минимальным, торгуемым с производительностью (вероятно, не тестируемой) — много обратных вызовов / пробелов.

Dokuwiki (и phputf8 ) использует аналогичный подход, но сначала разбивает входную строку на массив или последовательности UTF-8 и проверяет, совпадают ли они в массиве поиска. Это реализация PHP UTF-8, которая почти такая же ( utf8_to_unicode() преобразует строку UTF-8 в массив последовательностей, представляющих символы, а utf8_from_unicode() выполняет обратное);

function utf8_strtolower($string){ global $UTF8_UPPER_TO_LOWER; $uni = utf8_to_unicode($string); if ( !$uni ) { return FALSE; } $cnt = count($uni); for ($i=0; $i < $cnt; $i++){ if ( isset($UTF8_UPPER_TO_LOWER[$uni[$i]]) ) { $uni[$i] = $UTF8_UPPER_TO_LOWER[$uni[$i]]; } } return utf8_from_unicode($uni); } 

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

В любом случае — введите подход Марека, который можно обобщить как;

function StrToLower ($s) { global $TabToLower; return strtr (strtolower ($s), $TabToLower); }
function StrToLower ($s) { global $TabToLower; return strtr (strtolower ($s), $TabToLower); } 

… Где $TabToLower — таблица поиска (теперь минус поиск символов ASCII, обрабатываемый strtolower ). Обратите внимание, что код, который Марек показал мне, использует классы — это всего лишь упрощение. Он опирается на устанавливаемый языковой стандарт POSIX (в противном случае кодировка UTF-8 может быть нарушена) и использует дизайн аспекта UTF-8, а именно любая полная последовательность в допустимой строке UTF-8 уникальна (ее нельзя принять за часть более длинная последовательность). Вам также нужно внимательно прочитать документацию по strtr ()

strtr () может быть вызван только с двумя аргументами. Если вызывается с двумя аргументами, он ведет себя по-новому: с этого момента должен быть массив, содержащий пары строка -> строка, которые будут заменены в исходной строке. strtr () всегда будет сначала искать самое длинное из возможных совпадений и * NOT * попытается заменить материал, над которым он уже работал.

Я еще не тестировал это, но Марек сказал, что он примерно в 3 раза быстрее, чем эквивалентные функции mbstring, и я могу в это поверить.

Марек также использует некоторые хитрые приемы для обработки массивов поиска. В подходах «Докувики» и «МедиаВики» определены все возможные преобразования случаев, т. Е. Они применяются к нескольким человеческим языкам. Хотя это может быть уместно для пользовательского контента, когда вы делаете такие вещи, как локализация вашего пользовательского интерфейса, есть вероятность, что вы будете использовать только один язык — вам не нужна полная таблица поиска, только те, которые применимы к задействованный язык, если вы знаете, что это такое. Также вы можете подумать о просмотре входящего $_SERVER['HTTP_ACCEPT_LANGUAGE'] из браузера.

В любом случае — когда у меня будет время, я пойму, как использовать идеи Марека в PHP UTF-8 .

Выходной Преобразование

Еще один умный совет от Марека, который я раньше не обсуждал, — как доставлять контент клиентам, которые не могут работать с UTF-8, например старым браузерам, телефонам (?). Его подход прост и эффективен — как только вы закончите создание выходной страницы, запишите ее в выходной буфер , проверьте, что клиент отправил как приемлемые наборы символов ( $_SERVER['HTTP_ACCEPT_CHARSET'] ), и преобразуйте (понизьте) вывод с помощью iconv при необходимости.

Вы должны внимательно изучить содержимое этого заголовка и правильно его обработать. Вы также должны убедиться, что вы повторно объявили кодировку Content-Type плюс любые метасимволы HTML или кодировку в инструкции обработки XML. Но это, безусловно, серьезный / доступный способ решения проблемы в PHP.

Мораль истории…

… Стоит ли говорить с людьми, которым действительно нужен UTF-8, по сравнению с теми, кто в странах самодовольно использует ISO-8859-1 (который изначально не поддерживает символ евро, кстати!).

Учитывая, что Mediawiki «выполнила» Unicode Normalization в PHP ( здесь ), единственным оставшимся кусочком головоломки является Unicode Collation (например, для сортировки) — вот хорошее место для вдохновения. После этого — кому нужен PHP 6;)