Столкнулся с задачей — автоопределение кодировки страницы/текста/чего угодно. Задача не нова, и велосипедов понапридумано уже много. В статье небольшой обзор найденного в сети — плюс предложение своего, как мне кажется, достойного решения.
Если кратко — он не работает.
Давайте смотреть:
Как видим, на выходе — полная каша. Что мы делаем, когда непонятно почему так себя ведет функция? Правильно, гуглим. Нашел замечательный ответ.
Чтобы окончательно развеять все надежды на использование mb_detect_encoding(), надо залезть в исходники расширения mbstring. Итак, закатали рукава, поехали:
Ctrl + клик:
Ctrl + клик:
Постить полный текст метода не буду, чтобы не засорять статью лишними исходниками. Кому это интересно посмотрят сами. Нас истересует строка под номером 593, где собственно и происходит проверка того, подходит ли символ под кодировку:
Вот основные фильтры для однобайтовой кириллицы:
Windows-1251 (оригинальные комментарии сохранены)
KOI8-R
ISO-8859-5 (тут вообще все весело)
Как видим, ISO-8859-5 всегда возвращает TRUE (чтобы вернуть FALSE, нужно выставить filter->flag = 1).
Когда посмотрели фильтры, все встало на свои места. CP1251 от KOI8-R не отличить никак. ISO-8859-5 вообще если есть в списке кодировок — будет всегда детектиться как верная.
В общем, fail. Оно и понятно — только по кодам символов нельзя в общем случае узнать кодировку, так как эти коды пересекаются в разных кодировках.
А гугл выдает всякие убожества. Даже не буду постить сюда исходники, сами посмотрите, если захотите (уберите пробел после http://, не знаю я как показать текст не ссылкой):
http:// deer.org.ua/2009/10/06/1/
http:// php.su/forum/topic.php?forum=1&topic=1346
1) опять коды символов: habrahabr.ru/blogs/php/27378/#comment_710532
2) на мой взгляд, очень интересное решение: habrahabr.ru/blogs/php/27378/#comment_1399654
Минусы и плюсы в комменте по ссылке. Лично я считаю, что только для детекта кодировки это решение избыточно — слишком мощно получается. Определение кодировки в нем — как побочный эффект ).
Идея возникла во время просмотра второй ссылки из прошлого раздела. Идея следующая: берем большой русский текст, замеряем частоты разных букв, по этим частотам детектим кодировку. Забегая вперед, сразу скажу — будут проблемы с большими и маленькими буквами. Поэтому выкладываю примеры частот букв (назовем это — «спектр») как с учетом регистра, так и без (во втором случае к маленькой букве добавлял еще большую с такой же частотой, а большие все удалял). В этих «спектрах» вырезаны все буквы, имеющие частоты меньше 0,001 и пробел. Вот, что у меня получилось после обработки «Войны и Мира»:
Регистрозависимый «спектр»:
Регистронезависимый:
Спектры в разных кодировках (ключи массива — коды соответствующих символов в соответствующей кодировке):
Windows-1251: case sensitive, case insensitive
KOI8-R: case sensitive, case insensitive
ISO-8859-5: case sensitive, case insensitive
Далее. Берем текст неизвестной кодировки, для каждой проверяемой кодировки находим частоту текущего с��мвола и прибавляем к «рейтингу» этой кодировки. Кодировка с бОльшим рейтингом и есть, скорее всего, кодировка текста.
Даже не пытайтесь выполнить этот код у себя — он не заработает. Можете считать это псевдокодом — я опустил детали, чтобы не загромождать статью. $char_specter — это как раз те массивы, на которые стоят ссылки на pastebin.
Строки таблицы — кодировка текста, столбцы — содержимое массива $enc_rates.
1) $str = 'Русский текст';
Все отлично. Реальная кодировка имеет уже в 4 раза бОльший рейтинг, чем остальные — это на таком коротком тексте. На более длинных текстах соотношение будет примерно таким же.
2) $str = ' СТРОКА КАПСОМ РУССКИЙ ТЕКСТ';
У-упс! Полная каша. А потому что большие буквы в CP1251 обычно соответствуют маленьким в KOI8-R. А маленькие буквы используются в свою очередь намного чаще, чем большие. Вот и определяем строку капсом в CP1251 как KOI8-R.
Пробуем делать без учета регистра («спектры» case insensitive)
1) $str = 'Русский текст';
2) $str = ' СТРОКА КАПСОМ РУ��СКИЙ ТЕКСТ';
Как видим, верная кодировка стабильно лидирует и с регистрозависимыми «спектрами» (если строка содержит небольшое количество заглавных букв), и с регистронезависимыми. Во втором случае, с регистронезависимыми, лидирует не так уверенно, конечно, но вполне стабильно даже на маленьких строках. Можно поиграться еще с весами букв — сделать их нелинейными относительно частоты, например.
В топике не расмотрена работа с UTF-8 — тут никакий принципиальной разницы нету, разве что получение кодов символов и разбиение строки на символы будет несколько длиннее/сложнее.
Эти идеи можно распространить не только на кириллические кодировки, конечно — вопрос только в «спектрах» соответствующих языков/кодировок.
P.S. Если будет очень нужно/интересно — потом выложу второй частью полностью работающую библиотеку на GitHub. Хотя я считаю, что данных в посте вполне достаточно для быстрого написания такой библиотеки и самому под свои нужды — «спектр» для русского языка выложен, его можно без труда перенести на все нужные кодировки.
UPDATED
В комментариях проскочила замечательная функция, ссылку на которую я опубликовал под графом «убожество». Может быть погорячился со словами, но уж как опубликовал, так опубликовал — редактировать такие вещи не привык. Чтобы не быть голословным, давайте разберемся, работает ли она на 100%, как об этом говорит предполагаемый автор.
1) будут ли ошибки при «нормальной» работе этой функции? Предположим, что контент у нас на 100% валидный.
ответ: да, будут.
2) определит ли она что-нибудь кроме UTF-8 и не-UTF-8?
ответ: нет, не определит.
Вот код:
что на выходе:
Что мы видим? Однобайтовая кириллица после iconv($encoding, $encodigng) не изменится. Так можно отличить только UTF-8 от не-UTF-8. И то — ценой ворнинга.
ИМХО именно вот из-за таких кусков кода и считают 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->flag = 1; /* not it */ return c; }
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_710532
2) на мой взгляд, очень интересное решение: 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, )
Спектры в разных кодировках (ключи массива — коды соответствующих символов в соответствующей кодировке):
Windows-1251: case sensitive, case insensitive
KOI8-R: case sensitive, case insensitive
ISO-8859-5: case sensitive, case insensitive
Далее. Берем текст неизвестной кодировки, для каждой проверяемой кодировки находим частоту текущего с��мвола и прибавляем к «рейтингу» этой кодировки. Кодировка с бОльшим рейтингом и есть, скорее всего, кодировка текста.
$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[ord($str[$i])]; } } var_dump($enc_rates);
Даже не пытайтесь выполнить этот код у себя — он не заработает. Можете считать это псевдокодом — я опустил детали, чтобы не загромождать статью. $char_specter — это как раз те массивы, на которые стоят ссылки на pastebin.
Результаты
Строки таблицы — кодировка текста, столбцы — содержимое массива $enc_rates.
1) $str = 'Русский текст';
cp1251 | koi8r | iso88595 |
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 раза бОльший рейтинг, чем остальные — это на таком коротком тексте. На более длинных текстах соотношение будет примерно таким же.
2) $str = ' СТРОКА КАПСОМ РУССКИЙ ТЕКСТ';
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. Хотя я считаю, что данных в посте вполне достаточно для быстрого написания такой библиотеки и самому под свои нужды — «спектр» для русского языка выложен, его можно без труда перенести на все нужные кодировки.
UPDATED
В комментариях проскочила замечательная функция, ссылку на которую я опубликовал под графом «убожество». Может быть погорячился со словами, но уж как опубликовал, так опубликовал — редактировать такие вещи не привык. Чтобы не быть голословным, давайте разберемся, работает ли она на 100%, как об этом говорит предполагаемый автор.
1) будут ли ошибки при «нормальной» работе этой функции? Предположим, что контент у нас на 100% валидный.
ответ: да, будут.
2) определит ли она что-нибудь кроме UTF-8 и не-UTF-8?
ответ: нет, не определит.
Вот код:
$str_cp1251 = iconv('UTF-8', 'Windows-1251', 'Русский текст'); var_dump(md5($str_cp1251)); var_dump(md5(iconv('Windows-1251', 'Windows-1251', $str_cp1251))); var_dump(md5(iconv('KOI8-R', 'KOI8-R', $str_cp1251))); var_dump(md5(iconv('ISO-8859-5', 'ISO-8859-5', $str_cp1251))); var_dump(md5(iconv('UTF-8', 'UTF-8', $str_cp1251)));
что на выходе:
m00t@m00t:~/workspace/test$ php detect_encoding.php string(32) "96e14d7add82668414ffbc498fcf2a4e" string(32) "96e14d7add82668414ffbc498fcf2a4e" string(32) "96e14d7add82668414ffbc498fcf2a4e" string(32) "96e14d7add82668414ffbc498fcf2a4e" PHP Notice: iconv(): Detected an illegal character in input string in /home/m00t/workspace/test/detect_encoding.php on line 36 PHP Stack trace: PHP 1. {main}() /home/m00t/workspace/test/detect_encoding.php:0 PHP 2. iconv() /home/m00t/workspace/test/detect_encoding.php:36 string(32) "d41d8cd98f00b204e9800998ecf8427e"
Что мы видим? Однобайтовая кириллица после iconv($encoding, $encodigng) не изменится. Так можно отличить только UTF-8 от не-UTF-8. И то — ценой ворнинга.
ИМХО именно вот из-за таких кусков кода и считают PHP «языком для дураков» (с) — как не переминут написать тролли в любом топике про этот язык.