Внезапно перестал работать простой скрипт. Задача скрипта – получение HTML-страницы (из браузерной игры) и выборка данных с помощью регулярных выражений. У меня как у новичка это событие вызвало недоумение и легкую панику: ведь все еще вчера работало! В чем же дело?
Пришлось досконально разбираться в работе некоторых PHP-функций.
Код был достаточно примитивным:
$pattern = ; $url = "http://www.heroeswm.ru/pl_info.php?id={$id} " ; $html = file_get_contents ($url ) ; preg_match ($pattern , $html , $matches ) ; if (isset ($matches [ 1 ] ) ) echo $matches [ 1 ] ; else echo "не найдено" ; |
$pattern = "#Написать письмо(.*)Боевой уровень#is"; $url = "http://www.heroeswm.ru/pl_info.php?id={$id}"; $html = file_get_contents($url); preg_match($pattern, $html, $matches); if (isset($matches)) echo $matches; else echo "не найдено";
Получение данных и разбор с помощью простого регулярного выражения.
Надо сказать, что этот код – результат легкой модификации. В исходном варианте регулярка искала по HTML-тэгам. Но вот мне понадобилось найти кусочек между двумя словосочетаниями на русском языке. Я добавила в шаблон поиска русские слова, и именно это изменение стало критичным.
А теперь по порядку.
Сайт игры www.heroeswm.ru выдает страницы в кодировке win-1251
. У меня на сервере кодировка UTF-8
, поэтому все скрипты – в UTF-8
без BOM.
Исходный скрипт с поиском по HTML-тэгам работал корректно, несмотря на различие кодировок, но когда в шаблон поиска я добавила символы кириллицы, искать и находить перестал. В моей задаче бы очень просто отмахнуться от проблемы и подобрать другой шаблон – без русских слов, но в большинстве случаев это невозможно. Поэтому я решила разобраться досконально: в чем принципиальная разница кодировок
, почему она вызывает некорректную работу регулярных выражений
, а заодно – какие функции страдают
из-за различия кодировок, и как это обходить.
Для получения данных я использовала функцию file_get_contents() , которая имеет такой синтаксис:
string
file_get_contents(string
$filename) , где $filename – имя читаемого файла.
Возвращает строку или bool(false) – в случае неудачи при получении данных.
Самое очевидное различие кодировок win-1251 и UTF-8 – это количество символов, которые можно с их помощью закодировать. Первой (и всем подобным ей) подвластны лишь 255, так как каждый символ кодируется одним байтом.
С помощью второй можно передать поистине огромный набор символов, в том числе буквы национальных алфавитов, арабские буквы и иероглифы. Достигается такое расширение набора знаков за счет того, что кодируются символы уже не одним, а двумя (для большинства символов) и более (до четырех) байтами. Поэтому кодировку UTF-8 (и подобные ей) называют много- или мультибайтной, в отличие от однобайтных, таких как win-1251 .
Имея такой обширный набор знаков, UTF-8 не только позволит использовать на одном сайте буквы разных алфавитов, но и даст некую гарантию того, что русскоязычный сайт будет корректно отображаться даже там, где о существовании кодировок с поддержкой кириллицы (win-1251 , KOI8-R , CP866 , ISO 8859-5 и др.) даже не подозревают: в Японии, Корее, арабских странах и т.п. Платой за такую универсальность станет несколько больший вес символов при хранении и соответственно большее время их обработки строковыми функциями PHP . Они, кстати, в большинстве случаев будут работать некорректно. Именно с этой проблемой я и столкнулась: регулярка скрипта, записанного в UTF-8 , просто не могла правильно найти нужную мне подстроку, включающую знаки кириллицы, на странице, полученной с сайта в Windows-1251 .
Логично предположить, что сайтам, на которых будут использовать только кириллицу и латиницу, UTF-8 ни к чему, да и рассматриваемому простому парсеру вполне неплохо «жилось» в win-1251 , но бывают ситуации, когда от необходимости подружить эти кодировки и использовать строковые функции PHP просто не отвертеться, например, при разработке проекта в UTF-8 .
Чем вызвано некорректное поведение строковых функций?
Как уже говорила, главное различие кодировок – это длина символов. Поэтому и проблемы возникают при использовании функций, в которых с символами работают, как с байтами, и значения возвращают тоже в байтах (для однобайтной кодировки это справедливо: один символ равен одному байту).
Например, функция
substr ("Проверка" , 0 , 5 ) ; // текст в кодировке UTF-8 |
substr("Проверка", 0, 5); // текст в кодировке UTF-8
вернет «Пр�» вместо ожидаемого «Прове»: в UTF-8 символы кириллицы кодируются двумя байтами, вследствие чего мы и видим «кракозябр» – только первый байт символа «о».
Таким образом, в большинстве случаев для работы со строками в UTF-8 потребуется применение специальных функций (например, из расширения PHP mbstring ), а иногда и использование обеих (например, для передачи размера строки в байтах в заголовок HTTP нужно будет оставить strlen() , а для подсчета количества символов придется добавить mb_strlen()).
Синтаксис часто используемых функций, которым может потребоваться замена функциями из расширения PHP mbString :
int strlen(string $string) – возвращает длину строки или 0, если строка пуста.
int strpos(string $haystack , mixed $needle) – возвращает позицию первого вхождения строки $needle в подстроку $haystack или FALSE , если не найдено.
stripos аналогична предыдущей функции, только поиск регистронезависимый.
string substr(string $string , int $start [, int $length ] ) – позволяет выбрать подстроку, начиная с указанной позиции символа, и при указании третьего параметра – определенной длины.
Для работы в многобайтных кодировках предназначены их аналоги: mb_strlen , mb_strpos , mb_stripos и mb_substr .
Конечно, функций для работы с текстом гораздо больше. Я привела лишь самые популярные.
Особняком стоят функции для работы с регулярными выражениями, предназначенные для поиска подстроки (подстрок), совпадающей с маской, заданной регулярным выражением.
int preg_match(string $pattern , string $subject , array &$matches)
int preg_match_all(string $pattern , string $subject array &$matches) .
В тексте $subject ищутся совпадения с шаблоном $pattern , заданным регулярным выражением. Результат поиска записывается в переменную $matches . Функция возвращает количество найденных совпадений с шаблоном, в случае ошибки вернет FALSE .
Чтобы в многобайтных кодировках для поиска по шаблону использовать регулярные выражения, в них понадобится добавлять модификатор /u , либо использовать группу функций mb_ereg* .
Что делать?
Пришедшее первым решение – перекодировать в UTF-8 получаемые в win-1251 данные – показалось неудобным. Ведь вслед за перекодировкой все обычные функции придется заменять специальными для работы с UTF-8 , или пытаться использовать модификатор /u (забегая вперед, скажу, что он позволяет работать со строками в однобайтных кодировках, «как со строками UTF-8 », но не годится для строк в UTF-8 ). В моем примере лишь одна preg_match() , но на практике так бывает редко.
Поэтому «переворачиваю» задачу: я хочу использовать обычные функции preg_match() , а для этого я буду перекодировать не входящую строку, а шаблон поиска, используя iconv() .
Синтаксис функции:
string
iconv($in_charset, $out_charset, $str) — Конвертирует строку $str из кодировки $in_charset в кодировку $out_charset. Возвращает перекодированный текст, не затрагивая исходную переменную.
Функция возвращает строку в новой кодировке , но не изменяет кодировку самой строки. Поэтому
$pattern = "#Написать письмо(.*)Боевой уровень#is"; iconv("UTF-8", "WINDOWS-1251", $pattern); // $pattern остался в исходной кодировке preg_match($pattern, $html, $matches);
работать не будет – $pattern осталась в исходной кодировке UTF-8 . Необходимо результат работы iconv присвоить переменной:
$pattern = "#Написать письмо(.*)Боевой уровень#is" ; $pattern = iconv ("UTF-8" , "WINDOWS-1251" , $pattern ) ; preg_match ($pattern , $html , $matches ) ; |
$pattern = "#Написать письмо(.*)Боевой уровень#is"; $pattern = iconv("UTF-8", "WINDOWS-1251", $pattern); preg_match($pattern, $html, $matches);
Теперь поиск работает корректно, да только в браузер отдает сплошные кракозябры. Ну, тут уже знаю, что делать: нужно результат перекодировать в рабочую кодировку UTF-8 . И тут всплыл второй момент, который для меня не был очевиден, хотя имей я больше опыта, наверняка бы затруднений не вызвал: почему iconv() одни переменные перекодирует, а другие – нет?
Переменная $matches представляет собой массив, а я попыталась отделаться одной перекодировкой iconv($matches) . Еще раз смотрю описание синтаксиса функций: ну конечно, все параметры должны быть строками, а никак не массивами. То есть необходимо перебрать все значения массива, нуждающиеся в перекодировке, и перевести их в нужную кодировку. В своем примере перебором массива я не занималась, поскольку меня интересовало одно значение, а не весь массив. Его я и указала в качестве параметра функции iconv() .
Вот что у меня получилось в окончательном варианте:
// устанавливаем кодировку по умолчанию setlocale (LC_ALL, "ru_RU.UTF-8" ) ; header ("Content-type: text/html; charset=UTF-8" ) ; $pattern = "#Написать письмо(.*)Боевой уровень#is" ; $pattern = iconv ("UTF-8" , "WINDOWS-1251" , $pattern ) ; $url = "http://www.heroeswm.ru/pl_info.php?id=993353" ; $html = file_get_contents ($url ) ; preg_match ($pattern , $html , $matches ) ; if (isset ($matches [ 1 ] ) ) echo $matches [ 1 ] = iconv ("WINDOWS-1251" , "UTF-8" , $matches [ 1 ] ) ; else echo "не найдено" ; ?> |
Статья написана моей хорошей знакомой. Она занимается написанием и проверкой текстов, программирование на PHP – больше ее хобби. В моем блоге она корректирует все публикации, а эта – стала ее подарком мне к двухлетию блога.
Похожих публикаций не найдено.
Столкнулся с задачей - автоопределение кодировки страницы/текста/чего угодно. Задача не нова, и велосипедов понапридумано уже много. В статье небольшой обзор найденного в сети - плюс предложение своего, как мне кажется, достойного решения.
1. Почему не mb_detect_encoding() ?
Если кратко - он не работает.Давайте смотреть:
// На входе - русский текст в кодировке CP1251 $string = iconv("UTF-8", "Windows-1251", "Он подошел к Анне Павловне, поцеловал ее руку, подставив ей свою надушенную и сияющую лысину, и покойно уселся на диване."); // Посмотрим, что нам выдает md_detect_encoding(). Сначала $strict = FALSE var_dump(mb_detect_encoding($string, array("UTF-8"))); // UTF-8 var_dump(mb_detect_encoding($string, array("UTF-8", "Windows-1251"))); // Windows-1251 var_dump(mb_detect_encoding($string, array("UTF-8", "KOI8-R"))); // KOI8-R var_dump(mb_detect_encoding($string, array("UTF-8", "Windows-1251", "KOI8-R"))); // FALSE var_dump(mb_detect_encoding($string, array("UTF-8", "ISO-8859-5"))); // ISO-8859-5 var_dump(mb_detect_encoding($string, array("UTF-8", "Windows-1251", "KOI8-R", "ISO-8859-5"))); // ISO-8859-5 // Теперь $strict = TRUE var_dump(mb_detect_encoding($string, array("UTF-8"), TRUE)); // FALSE var_dump(mb_detect_encoding($string, array("UTF-8", "Windows-1251"), TRUE)); // FALSE var_dump(mb_detect_encoding($string, array("UTF-8", "KOI8-R"), TRUE)); // FALSE var_dump(mb_detect_encoding($string, array("UTF-8", "Windows-1251", "KOI8-R"), TRUE)); // FALSE var_dump(mb_detect_encoding($string, array("UTF-8", "ISO-8859-5"), TRUE)); // ISO-8859-5 var_dump(mb_detect_encoding($string, array("UTF-8", "Windows-1251", "KOI8-R", "ISO-8859-5"), TRUE)); // ISO-8859-5
Как видим, на выходе - полная каша. Что мы делаем, когда непонятно почему так себя ведет функция? Правильно, гуглим. Нашел замечательный ответ .
Чтобы окончательно развеять все надежды на использование mb_detect_encoding(), надо залезть в исходники расширения mbstring. Итак, закатали рукава, поехали:
// ext/mbstring/mbstring.c:2629
PHP_FUNCTION(mb_detect_encoding)
{
...
// строка 2703
ret = mbfl_identify_encoding_name(&string, elist, size, strict);
...
Ctrl + клик:
// ext/mbstring/libmbfl/mbfl/mbfilter.c:643
const char*
mbfl_identify_encoding_name(mbfl_string *string, enum mbfl_no_encoding *elist, int elistsz, int strict)
{
const mbfl_encoding *encoding;
encoding = mbfl_identify_encoding(string, elist, elistsz, strict);
...
Ctrl + клик:
// ext/mbstring/libmbfl/mbfl/mbfilter.c:557
/*
* identify encoding
*/
const mbfl_encoding *
mbfl_identify_encoding(mbfl_string *string, enum mbfl_no_encoding *elist, int elistsz, int strict)
{
...
Постить полный текст метода не буду, чтобы не засорять статью лишними исходниками. Кому это интересно посмотрят сами. Нас истересует строка под номером 593, где собственно и происходит проверка того, подходит ли символ под кодировку:
// ext/mbstring/libmbfl/mbfl/mbfilter.c:593
(*filter->filter_function)(*p, filter);
if (filter->flag) {
bad++;
}
Вот основные фильтры для однобайтовой кириллицы:
Windows-1251 (оригинальные комментарии сохранены)
// ext/mbstring/libmbfl/filters/mbfilter_cp1251.c:142
/* all of this is so ugly now! */
static int mbfl_filt_ident_cp1251(int c, mbfl_identify_filter *filter)
{
if (c >= 0x80 && c < 0xff)
filter->flag = 0;
else
filter->
KOI8-R
// ext/mbstring/libmbfl/filters/mbfilter_koi8r.c:142
static int mbfl_filt_ident_koi8r(int c, mbfl_identify_filter *filter)
{
if (c >= 0x80 && c < 0xff)
filter->flag = 0;
else
filter->flag = 1; /* not it */
return c;
}
ISO-8859-5 (тут вообще все весело)
// ext/mbstring/libmbfl/mbfl/mbfl_ident.c:248
int mbfl_filt_ident_true(int c, mbfl_identify_filter *filter)
{
return c;
}
Как видим, ISO-8859-5 всегда возвращает TRUE (чтобы вернуть FALSE, нужно выставить filter->flag = 1).
Когда посмотрели фильтры, все встало на свои места. CP1251 от KOI8-R не отличить никак. ISO-8859-5 вообще если есть в списке кодировок - будет всегда детектиться как верная.
В общем, fail. Оно и понятно - только по кодам символов нельзя в общем случае узнать кодировку, так как эти коды пересекаются в разных кодировках.
2. Что выдает гугл
А гугл выдает всякие убожества. Даже не буду постить сюда исходники, сами посмотрите, если захотите (уберите пробел после http://, не знаю я как показать текст не ссылкой):Http:// deer.org.ua/2009/10/06/1/
http:// php.su/forum/topic.php?forum=1&topic=1346
3. Поиск по хабру
1) опять коды символов: habrahabr.ru/blogs/php/27378/#comment_7105322) на мой взгляд, очень интересное решение: habrahabr.ru/blogs/php/27378/#comment_1399654
Минусы и плюсы в комменте по ссылке. Лично я считаю, что только для детекта кодировки это решение избыточно - слишком мощно получается. Определение кодировки в нем - как побочный эффект).
4. Собственно, мое решение
Идея возникла во время просмотра второй ссылки из прошлого раздела. Идея следующая: берем большой русский текст, замеряем частоты разных букв, по этим частотам детектим кодировку. Забегая вперед, сразу скажу - будут проблемы с большими и маленькими буквами. Поэтому выкладываю примеры частот букв (назовем это - «спектр») как с учетом регистра, так и без (во втором случае к маленькой букве добавлял еще большую с такой же частотой, а большие все удалял). В этих «спектрах» вырезаны все буквы, имеющие частоты меньше 0,001 и пробел. Вот, что у меня получилось после обработки «Войны и Мира»:Регистрозависимый «спектр»:
array ("о" => 0.095249209893009,
"е" => 0.06836817536026,
"а" => 0.067481298384992,
"и" => 0.055995027400041,
"н" => 0.052242744063325,
....
"э" => 0.002252892226507,
"Н" => 0.0021318391371162,
"П" => 0.0018574762967903,
"ф" => 0.0015961610948418,
"В" => 0.0014044332975731,
"О" => 0.0013188987793209,
"А" => 0.0012623590130186,
"К" => 0.0011804488387602,
"М" => 0.001061932790165,)
Регистронезависимый:
array ("О" => 0.095249209893009,
"о" => 0.095249209893009,
"Е" => 0.06836817536026,
"е" => 0.06836817536026,
"А" => 0.067481298384992,
"а" => 0.067481298384992,
"И" => 0.055995027400041,
"и" => 0.055995027400041,
....
"Ц" => 0.0029893589260344,
"ц" => 0.0029893589260344,
"щ" => 0.0024649163501406,
"Щ" => 0.0024649163501406,
"Э" => 0.002252892226507,
"э" => 0.002252892226507,
"Ф" => 0.0015961610948418,
"ф" => 0.0015961610948418,)
Спектры в разных кодировках (ключи массива - коды соответствующих символов в соответствующей кодировке):
Далее. Берем текст неизвестной кодировки, для каждой проверяемой кодировки находим частоту текущего символа и прибавляем к «рейтингу» этой кодировки. Кодировка с бОльшим рейтингом и есть, скорее всего, кодировка текста.
$encodings = array("cp1251" => require "specter_cp1251.php",
"koi8r" => require "specter_koi8r.php",
"iso88595" => require "specter_iso88595.php");
$enc_rates = array();
for ($i = 0; $i < len($str); ++$i)
{
foreach ($encodings as $encoding => $char_specter)
{
$enc_rates[$encoding] += $char_specter)];
}
}
var_dump($enc_rates);
Даже не пытайтесь выполнить этот код у себя - он не заработает. Можете считать это псевдокодом - я опустил детали, чтобы не загромождать статью. $char_specter - это как раз те массивы, на которые стоят ссылки на pastebin.
Результаты
Строки таблицы - кодировка текста, столбцы - содержимое массива $enc_rates.1) $str = "Русский текст";
0.441 | 0.020 | 0.085 | Windows-1251
0.049 | 0.441 | 0.166 | KOI8-R
0.133 | 0.092 | 0.441 | ISO-8859-5
Все отлично. Реальная кодировка имеет уже в 4 раза бОльший рейтинг, чем остальные - это на таком коротком тексте. На более длинных текстах соотношение будет примерно таким же.
cp1251 | koi8r | iso88595 |
0.013 | 0.705 | 0.331 | Windows-1251
0.649 | 0.013 | 0.201 | KOI8-R
0.007 | 0.392 | 0.013 | ISO-8859-5
У-упс! Полная каша. А потому что большие буквы в CP1251 обычно соответствуют маленьким в KOI8-R. А маленькие буквы используются в свою очередь намного чаще, чем большие. Вот и определяем строку капсом в CP1251 как KOI8-R.
Пробуем делать без учета регистра («спектры» case insensitive)
1) $str = "Русский текст";
cp1251 | koi8r | iso88595 |
0.477 | 0.342 | 0.085 | Windows-1251
0.315 | 0.477 | 0.207 | KOI8-R
0.216 | 0.321 | 0.477 | ISO-8859-5
2) $str = " СТРОКА КАПСОМ РУССКИЙ ТЕКСТ";
cp1251 | koi8r | iso88595 |
1.074 | 0.705 | 0.465 | Windows-1251
0.649 | 1.074 | 0.201 | KOI8-R
0.331 | 0.392 | 1.074 | ISO-8859-5
Как видим, верная кодировка стабильно лидирует и с регистрозависимыми «спектрами» (если строка содержит небольшое количество заглавных букв), и с регистронезависимыми. Во втором случае, с регистронезависимыми, лидирует не так уверенно, конечно, но вполне стабильно даже на маленьких строках. Можно поиграться еще с весами букв - сделать их нелинейными относительно частоты, например.
5. Заключение
В топике не расмотрена работа с UTF-8 - тут никакий принципиальной разницы нету, разве что получение кодов символов и разбиение строки на символы будет несколько длиннее/сложнее.Эти идеи можно распространить не только на кириллические кодировки, конечно - вопрос только в «спектрах» соответствующих языков/кодировок.
P.S. Если будет очень нужно/интересно - потом выложу второй частью полностью работающую библиотеку на GitHub. Хотя я считаю, что данных в посте вполне достаточно для быстрого написания такой библиотеки и самому под свои нужды - «спектр» для русского языка выложен, его можно без труда перенести на все нужные кодировки.
Вернемся к нашей HTML-странице, которую мы создавали в предыдущих уроках и сейчас выставим кодировку, в которой будет храниться ее текст.
Я хотел бы рассказать о двух способах, как можно поменять кодировку текста. Как правило, я пользуюсь ими на практике и они хорошо себе зарекомендовали.
Самым надежным способом изменить кодировку текста является способ с помощью программы Notepad++. Как правило, этот способ всегда надежно работает и с его помощью можно решить самые тяжелые проблемы.
1 способ. С помощью программы Notepad++
Итак, для того, чтобы сменить кодировку текста нам понадобиться специальный текстовый редактор, который называется notepad++.
Он бесплатный и скачать его можно с этого сайта:
Открываем HTML-страницу с помощью этой программы и переходим в главное меню «Кодировки».
Выбираем ту кодировку, в которую нужно выполнить преобразование и сохраняем файл.
Вот и вся процедура. Программа очень хорошая и в отличии от других альтернатив, меняет кодировку безотказно.
2 способ. С помощью универсального редактора кода Dreamweaver.
Если вы работаете в универсальном редакторе кода Dreamweaver, там тоже есть возможность указать кодировку, в которой будет представлен текст.
Сделать это можно также с помощью главного меню "Изменить - Свойства страницы".
Далее в категории "Название/кодировка" выбираем ту кодировку, которая вам необходима. Чаще всего это будет кодировка Юникод (UTF-8).
При создании нового html-документа, этот способ работает отлично, но если вы меняете кодировку уже существующего файла, то лучше воспользоваться первым способом. В этом случае он работает лучше.
Сделайте эту операцию у себя на компьютере.
Но, указав кодировку текста для html-страницы еще не достаточно. Для нормальной ее работы нужно сделать еще одно действие: подсказать браузеру, в какой кодировке написан текст.