PHP и UTF-8
Все уже
Увы,
Немного праздника в этот однобайтный склеп привносит тот замечательный факт, что
Печальнее всего с двумя вещами.
Вторая проблема заковыристее. В PHP испокон веков аж три модуля для работы с регулярными выражениями — PCRE, Regular Expression (POSIX Extended) и уже знакомый нам Multibyte String. Первый работает
PCRE в таких помощниках не нуждается, потому что сам умеет работать
if (preg_match('/(\w)/iu', 'ПриветN', $m)) {
var_dump($m);
}Выдаст нам… букву «N». Почему? Находить в строке
Matching characters by Unicode property is not fast, because PCRE has to search a structure that contains data for over fifteen thousand characters. That is why the traditional escape sequences such as \d and \w do not use Unicode properties in PCRE
То есть «\d» (цифры), «\w» (буквы) и, кстати, «\b» (граница слова) и «\s» (пробельные символы) работают только с однобайтовыми кодировками. Ужасно? Не то слово! Всё потеряно? К счастью, нет.
Если внимательно читать документацию по PCRE, то найдётся много интересного, в том числе решение и этой проблемы. В одной из версий этой библиотеки появилась экспериментальная конструкция, называемая «глаголом» (VERB), «глаголы» умеют очень многое, но нас интересует специфический «глагол» «UCP», который (ура) занимается тем, что переключает «\w», «\s», «\d» и «\b» в полностью юникодный режим.
То есть такой код даст нам ожидаемый результат:
if (preg_match('/(*UCP)(\w)/iu', 'ПриветN', $m)) {
var_dump($m);
}Победа? Ничего подобного. Этот глагол работает начиная с PCRE 8.10, в самой последней стабильной версии PHP (5.3.3) содержится PCRE 8.02, там этот глагол ещё не поддерживается, я проверял.
Нужная нам версия 8.10 войдёт в PHP 5.3.4. Но,
Но не стоит опускать руки,
Подведём итоги. Для перевода (большого) проекта
Ну а так же не забыть про внешние источники данных — перевести базу
Милиция → Полиция
Посмотрите чем на самом деле грозит нам «закон» о полиции.
Для комментирования ситуации пригодится большой матерный загиб.
PHP и UTF-8: первый этап
Я решил описать переход проекта
Основная идея — я добавлю в проект статический класс UTF. В этом классе будет переключатель (я назвал его «ON»), если он в состоянии «false», то все статические методы класса вызывают функции, работающие с однобайтовыми кодировками, иначе —
Выглядит он примерно так:
class UTF
{
const ON = false;
static public function strpos($haystack, $needle, $offset = 0)
{
if (self::ON) {
return mb_strpos($haystack, $needle, $offset, 'UTF-8');
}
return strpos($haystack, $needle, $offset);
}
// … и так далее
}Для чего нужен переключатель ON? Он позволяет переключить разом весь проект в состояние
Итак, нам надо посмотреть что же нужно переписывать и заменять в проекте. Я поступил просто — открыл в руководстве по PHP разделы строковых функций, функций PCRE и регулярных выражений в формате POSIX, скопировал их себе в программу и оформил как регулярное выражение:
<?
$funclist = <<<LIST
addcslashes — Quote string with slashes in a C style
chop — Alias of rtrim
chr — Return a specific character
chunk_split — Split a string into smaller chunks
convert_cyr_string — Convert from one Cyrillic character set to another
count_chars — Return information about characters used in a string
hebrev — Convert logical Hebrew text to visual text
hebrevc — Convert logical Hebrew text to visual text with newline conversion
html_entity_decode — Convert all HTML entities to their applicable characters
htmlentities — Convert all applicable characters to HTML entities
htmlspecialchars_decode — Convert special HTML entities back to characters
htmlspecialchars — Convert special characters to HTML entities
lcfirst — Make a string`s first character lowercase
levenshtein — Calculate Levenshtein distance between two strings
localeconv — Get numeric formatting information
ltrim — Strip whitespace (or other characters) from the beginning of a string
metaphone — Calculate the metaphone key of a string
money_format — Formats a number as a currency string
nl_langinfo — Query language and locale information
ord — Return ASCII value of character
rtrim — Strip whitespace (or other characters) from the end of a string
setlocale — Set locale information
similar_text — Calculate the similarity between two strings
soundex — Calculate the soundex key of a string
sscanf — Parses input from a string according to a format
str_getcsv — Parse a CSV string into an array
str_ireplace — Case-insensitive version of str_replace.
str_pad — Pad a string to a certain length with another string
str_rot13 — Perform the rot13 transform on a string
str_shuffle — Randomly shuffles a string
str_split — Convert a string to an array
str_word_count — Return information about words used in a string
strcasecmp — Binary safe case-insensitive string comparison
strcmp — Binary safe string comparison
strcoll — Locale based string comparison
strcspn — Find length of initial segment not matching mask
stripcslashes — Un-quote string quoted with addcslashes
stripos — Find position of first occurrence of a case-insensitive string
stristr — Case-insensitive strstr
strlen — Get string length
strnatcasecmp — Case insensitive string comparisons using a "natural order" algorithm
strnatcmp — String comparisons using a "natural order" algorithm
strncasecmp — Binary safe case-insensitive string comparison of the first n characters
strncmp — Binary safe string comparison of the first n characters
strpbrk — Search a string for any of a set of characters
strpos — Find position of first occurrence of a string
strrchr — Find the last occurrence of a character in a string
strrev — Reverse a string
strripos — Find position of last occurrence of a case-insensitive string in a string
strrpos — Find position of last occurrence of a char in a string
strspn — Finds the length of the first segment of a string consisting entirely of characters contained within a given mask.
strtolower — Make a string lowercase
strtoupper — Make a string uppercase
strtr — Translate certain characters
substr_compare — Binary safe comparison of two strings from an offset, up to length characters
substr_count — Count the number of substring occurrences
substr_replace — Replace text within a portion of a string
substr — Return part of a string
trim — Strip whitespace (or other characters) from the beginning and end of a string
ucfirst — Make a string`s first character uppercase
ucwords — Uppercase the first character of each word in a string
wordwrap — Wraps a string to a given number of characters
preg_filter — Perform a regular expression search and replace
preg_grep — Return array entries that match the pattern
preg_last_error — Returns the error code of the last PCRE regex execution
preg_match_all — Perform a global regular expression match
preg_match — Perform a regular expression match
preg_quote — Quote regular expression characters
preg_replace_callback — Perform a regular expression search and replace using a callback
preg_replace — Perform a regular expression search and replace
preg_split — Split string by a regular expression
ereg_replace — Replace regular expression
ereg — Regular expression match
eregi_replace — Replace regular expression case insensitive
eregi — Case insensitive regular expression match
split — Split string into array by regular expression
spliti — Split string into array by regular expression case insensitive
sql_regcase — Make regular expression for case insensitive match
ctype_alnum — Check for alphanumeric character(s)
ctype_alpha — Check for alphabetic character(s)
ctype_cntrl — Check for control character(s)
ctype_digit — Check for numeric character(s)
ctype_graph — Check for any printable character(s) except space
ctype_lower — Check for lowercase character(s)
ctype_print — Check for printable character(s)
ctype_punct — Check for any printable character which is not whitespace or an alphanumeric character
ctype_space — Check for whitespace character(s)
ctype_upper — Check for uppercase character(s)
array_change_key_case — Changes all keys in an array
array_multisort — Sort multiple or multi-dimensional arrays
arsort — Sort an array in reverse order and maintain index association
asort — Sort an array and maintain index association
krsort — Sort an array by key in reverse order
ksort — Sort an array by key
natcasesort — Sort an array using a case insensitive "natural order" algorithm
natsort — Sort an array using a "natural order" algorithm
rsort — Sort an array in reverse order
sort — Sort an array
LIST;
$funcs = array();
foreach (explode("\n", $funclist) as $line) {
list($line) = explode(" ", $line, 2);
$funcs[] = $line;
}
$regexp = '/(?<!UTF::)\b(' . implode('|', $funcs) . ')\s*\(/si';
set_time_limit(0);
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('.'));
for (;$it->valid(); $it->next()) {
$ext = pathinfo($it->getBaseName(), PATHINFO_EXTENSION);
if ($ext == 'php') {
if (preg_match_all($regexp, file_get_contents($it->key()), $matches)) {
echo $it->key(), "\n";
echo ' ', implode("\n ", array_map('strtolower', $matches[1])), "\n\n";
}
}
}Список функций я проредил — удалил оттуда те, у которых проблем
bolk@dev:~/daproject$ grep ' ' tool.log | sort | uniq -c | sort -rn
225 substr
195 preg_replace
174 trim
132 preg_match
132 htmlspecialchars
127 strpos
113 strtolower
88 strlen
50 preg_replace_callback
40 rtrim
…
bolk@dev:~/daproject$ grep './' tool.log
./tool.php
./htdocs/_file.php
./htdocs/file1.php
./htdocs/dir/file1.php
./htdocs/dir/file2.php
./htdocs/dir/file3.php
./htdocs/dir/file4.php
./htdocs/dir/file5.php
./htdocs/class.php
./htdocs/_ajax.php
…На следующем этапе я заменю эти функции на их аналоги. Кстати, если у вас используются оба алиаса одной функции (например, chop и trim), то подумайте над тем, чтобы привести всё к единому виду.
Отдельно надо будет
Так же очень полезно получить список всех используемых модулей, чтобы решить какие из них могут быть проблемными.
PHP и UTF-8: второй этап (ничего не забыли?)
Итак, на первом этапе перевода нашего
Могли.
Итак, как их найти? Мне кажется, хороший путь — посмотреть где встречается сигнатура „<?“:
bolk@dev:~/daproject$ grep --binary-files=without-match -R '<?' * | cut -d':' -f1 |
> grep -v '.svn' | sed -re 's/.*?/([^/]+)/1/' | awk -F. '/./{print $NF}' | sort -u
conf
csv
example
inc
js
package
php
pm
png
py
svg
tpl
xml
xslОчевидно, что файлы формата XML (у меня это расширения xml, xsl и svg) так же могут содержать сигнатуру „<?“ (там это часть языка), текстовые файлы (conf, csv) так же её могут содержать (они вообще могут содержать что угодно).
Как же отфильтровать такой мусор? У интерпретатора PHP есть специальная опция, для того, чтобы проверить является ли подаваемый на вход файл синтаксически верным. Мы ей можем воспользоваться:
bolk@dev:~/daproject$ grep --binary-files=without-match -R '<?' * | cut -d':' -f1 | egrep -v '.svn|.php' |
> xargs -rn1 -I{} php -r 'echo `php -l {}`;' | grep -F 'No syntax errors detected in' | sort -u
No syntax errors detected in htdocs/_dir/file1.csv
No syntax errors detected in htdocs/_dir/file2.png
No syntax errors detected in htdocs/_dir/file3.csv
No syntax errors detected in htdocs/_dir/file4.png
No syntax errors detected in htdocs/_dir/file5.csv
No syntax errors detected in templates/file1.tpl
…Оказывается, PHP у меня есть даже внутри файлов с расширением png!
К несчастью, у меня таких файлов достаточно прилично — 39, поэтому я собираюсь некоторые из них отбросить сразу:
sed -e 's/^No syntax errors detected in //' tool2.log | xargs -n1 head --verbose | lessОказалось, что рассматривать как файлы PHP нужно файлы с ещё двумя расширениями — inc и tpl, остальные так же содержат
Поэтому я добавлю в программу из предыдущего этапа ещё два расширения и запущу её снова. У меня количество функций не изменилось — 38, а вот файлов добавилось — стало 176 против 173. Ура, теперь файлы
А что по поводу используемых расширений PHP? Их много и с каждым месяцем становится всё больше (я сам написал 3 или 4), вдруг
Для начала неплохо было бы узнать сколько у нас вообще расширений. Я знаю, что расширения в PHP подключаются двумя способами: в конфигурационном файле и при помощи функции dl. Для начала посмотрим, используется ли у нас функция dl:
bolk@dev:~/daproject$ find \( -name '*.php' -or -name '*.inc' -or -name '*.tpl' \) -exec egrep -i 'dl\s*\(' {} \; -print
* @return bool Success or not on the dl() call
// if either returns true dl() will produce a FATAL error, stop that
return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix);
./php/lib/PEAR.php
if(!extension_loaded("mysql")) dl("mysql.so");
./php/db/mysql.phpУ меня dl используется. Берём на заметку, что через dl подгружается модуль MySQL, а заглянув в PEAR, обнаружим, что там есть метод loadExtension, который является мультиплатформенной обёрткой для dl.
Для очистки совести я поискал случаи использования loadExtension в проекте (не забудьте, что регистр нужно игнорировать), но не нашёл.
Итак, мы выяснили какие модули у нас подгружаются через dl — только один (mysql), что же вкомпилированно в сам PHP или подключено из файлов конфигурации? Нет способа лучше, чем узнать что сам PHP думает об этом:
bolk@dev:~/daproject$ php -m
[PHP Modules]
zip
xmlwriter
libxml
xml
wddx
tokenizer
…У меня это 54 модуля. Это очень много. Действительно ли их столько используется в проекте? Вот уж вряд ли. Надо
Отсюда мысль — узнать какие функции/классы содержатся в модуле и поискать их в файлах.
Давайте я это сделаю в следующей части, а то мне сейчас собеседование проводить.
PHP и UTF-8: второй этап (продолжение: ничего не забыли?)
Итак, в прошлый раз мы выяснили какие файлы у нас содержат программы на PHP и какие расширения есть в нашем интерпретаторе. Кстати, не забывайте про внепроектные файлы, те, что лежат в других папках, например, уровнем ниже. Их тоже надо использовать в нашем исследовании.
Выяснить какие же расширения реально используются в проекте нам поможет следующая программа:
<?
$extensions = array_merge(
get_loaded_extensions(true),
get_loaded_extensions()
);
$funcs = array();
foreach ($extensions as $extension) {
if ($extension == 'standard' || $extension == 'session') {
continue;
}
$extfuncs = get_extension_funcs($extension);
if ($extfuncs) {
$funcs = array_merge(
$funcs,
array_combine(
array_map('strtolower', $extfuncs),
array_fill(0, sizeof($extfuncs), $extension)
)
);
};
}
$classes = get_declared_classes();
set_time_limit(0);
$classes_regexp = '/new\s+(?i)(' . implode('|', $classes) . ')/s';
$funcs_regexp = '/(' . implode('|', array_keys($funcs)) . ')/si';
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('.'));
for (;$it->valid(); $it->next()) {
$ext = pathinfo($it->getBaseName(), PATHINFO_EXTENSION);
if ($ext == 'php' || $ext == 'tpl' || $ext == 'inc') {
$content = file_get_contents($it->key());
if (preg_match_all($classes_regexp, $content, $matches)) {
echo $it->key(), "\nClass: ",
implode("\nClass: ", array_map('strtolower', $matches[1])), "\n\n";
}
if (preg_match_all($funcs_regexp, $content, $matches)) {
echo $it->key(), "\n";
foreach ($matches[1] as $func) {
$func = strtolower($func);
if (isset($funcs[$func])) {
echo 'Module: ', $funcs[$func], "\n";
} else {
echo 'Something wrong: ', $func, "\n";
}
}
echo "\n\n";
}
}
}У меня она отработала достаточно неспешно, за несколько минут. Каковы же результаты?
bolk@dev:~/daproject$ awk -F: '/^Module/{print $2}' tool2.log | sort -u
ctype
curl
date
gettext
hash
iconv
…
bolk@dev:~/daproject$ awk -F: '/^Class/{print $2}' tool2.log | sort -u
exception
simplexmlelementНачнём с конца — Exception это класс для работы с исключениями,
А вот остальные 19 модулей, используемых в проекте, просмотреть сложнее. Их тех, что у меня есть я сразу могу исключить SQLITE3, XML,
Как я узнаю какие модули работают
На что ещё надо обратить внимание? На две вещи — нет ли в файлах
С первым просто, я перевожу проект
bolk@dev:~/wiki$ find -type f -name '?*.*' | grep -v '.svn' | xargs -r -n1 egrep -iH 'cp1251|cp\-1251|windows\-1251'
./php/dir1/file1.php: if (strtolower($rss->encoding) != 'windows-1251') {
./php/dir1/file2.php: $c = @iconv($rss->encoding, 'cp1251', $c);
./php/dir2/file3.php: $title = iconv('UTF-8', 'CP1251', $employers[$i]->first_name .' '. $employers[$i]->last_name);
…У меня таких строк — вагон и маленькая тележка, что с ними делать, будем решать дальше, пока же отметим, что они есть, причём в количестве 158 штук.
А что с PEAR? Точнее, с теми файлами PEAR, которые подключаются не из папки проекта, а из папки PEAR, которая скрыта
У меня таких импортов, кстати, не оказалось.
Кажется, информации накоплено достаточно, пора
(И тут я предлагаю сделать перерыв до завтра)
Тест на понимание
Кстати, мой текст про «Ошибки до собеседования» прекрасный тест на восприятие текста. В нём есть две сентенции, которые
Первое — резюме лучше слать как обычный текст, если возможно, вторая — резюме, которое
Неизвестно откуда читателями синтезировано два ложных утверждения: якобы я предлагаю слать резюме в формате DOC (это не так) и якобы я не рассматриваю резюме на английском
Где вы это всё вычитали, м?
Как бороться с туннельным синдромом
В ЖЖ появилась отличная статья «RSI — как бороться с туннельным синдромом», полезна она тем, что вполне тянет на полное исследование вопроса. Крайне, крайне рекомендую к прочтению полностью, а себе я выпишу несколько вещей, которые мне показались особенно примечательными:
- чтобы проверить предрасположенность к RSI, нужно попробовать оттянуть большой палец к запястью, если получается — есть предрасположенность (предрасположенность к RSI — это растянутость связок, растянутость связок побуждает кисть «болтаться» в работе, и приводит к износу сухожильной сумки)
- алкоголь дополнительно повреждает нервы в воспалённом районе кисти и, похоже, смазку сухожилий
- PowerBall нужно использовать без фанатизма. Регулярное превышение
6-8 тысяч оборотов на нём вам сделает RSI, если его даже не было. Для того, чтобы укрепить мышцы, надо систематично крутить его на относительно небольших оборотах: «столько, пока комфортно терпишь, плюс ещё тридцать секунд» - используйте клавиатуру с мягким ходом клавиш, без «дребезга» и щелчков. Шелчки передаются связкам и травмируют их
- обрезайте ногти под ноль. Два миллиметра — уже много. Нажатие на клавиатуру подушечками пальцев щадит связки сильнее, чем нажатие или задевание части поверхности клавиши ногтями
- мышку использовать нельзя. В списках причин RSI мышь стоит на первом месте. В опросниках у врачей по поводу RSI мышь стоит на первом месте: «Используете ли вы компьютерную мышку?» Используйте точпад
ЖЖ, OpenID и мой блог
Good news, everyone!
Я взял примеры, которые идут с библиотекой OpenID от JanRain, которую я использую. И оказалось, что OpenID ЖЖ в примерах работает нормально. Это означает, что это я с библиотекой делаю Оказалось, что не все тесты работают нормально. Значит, надо искать ошибку в библиотеке. Ох.
Добавлено позднее: а пока написал в JanRain и ЖЖ по письму с полным описанием бага.