проверка кодировки строки php
Определение кодировки текста в PHP — обзор существующих решений плюс еще один велосипед
Столкнулся с задачей — автоопределение кодировки страницы/текста/чего угодно. Задача не нова, и велосипедов понапридумано уже много. В статье небольшой обзор найденного в сети — плюс предложение своего, как мне кажется, достойного решения.
Если кратко — он не работает.
Давайте смотреть:
Как видим, на выходе — полная каша. Что мы делаем, когда непонятно почему так себя ведет функция? Правильно, гуглим. Нашел замечательный ответ.
Чтобы окончательно развеять все надежды на использование mb_detect_encoding(), надо залезть в исходники расширения mbstring. Итак, закатали рукава, поехали:
Постить полный текст метода не буду, чтобы не засорять статью лишними исходниками. Кому это интересно посмотрят сами. Нас истересует строка под номером 593, где собственно и происходит проверка того, подходит ли символ под кодировку:
Вот основные фильтры для однобайтовой кириллицы:
Windows-1251 (оригинальные комментарии сохранены)
ISO-8859-5 (тут вообще все весело)
Как видим, 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. Поиск по хабру
2) на мой взгляд, очень интересное решение: habrahabr.ru/blogs/php/27378/#comment_1399654
Минусы и плюсы в комменте по ссылке. Лично я считаю, что только для детекта кодировки это решение избыточно — слишком мощно получается. Определение кодировки в нем — как побочный эффект ).
4. Собственно, мое решение
Идея возникла во время просмотра второй ссылки из прошлого раздела. Идея следующая: берем большой русский текст, замеряем частоты разных букв, по этим частотам детектим кодировку. Забегая вперед, сразу скажу — будут проблемы с большими и маленькими буквами. Поэтому выкладываю примеры частот букв (назовем это — «спектр») как с учетом регистра, так и без (во втором случае к маленькой букве добавлял еще большую с такой же частотой, а большие все удалял). В этих «спектрах» вырезаны все буквы, имеющие частоты меньше 0,001 и пробел. Вот, что у меня получилось после обработки «Войны и Мира»:
Спектры в разных кодировках (ключи массива — коды соответствующих символов в соответствующей кодировке):
Далее. Берем текст неизвестной кодировки, для каждой проверяемой кодировки находим частоту текущего символа и прибавляем к «рейтингу» этой кодировки. Кодировка с бОльшим рейтингом и есть, скорее всего, кодировка текста.
Результаты
У-упс! Полная каша. А потому что большие буквы в CP1251 обычно соответствуют маленьким в KOI8-R. А маленькие буквы используются в свою очередь намного чаще, чем большие. Вот и определяем строку капсом в CP1251 как KOI8-R.
Пробуем делать без учета регистра («спектры» case insensitive)
Как видим, верная кодировка стабильно лидирует и с регистрозависимыми «спектрами» (если строка содержит небольшое количество заглавных букв), и с регистронезависимыми. Во втором случае, с регистронезависимыми, лидирует не так уверенно, конечно, но вполне стабильно даже на маленьких строках. Можно поиграться еще с весами букв — сделать их нелинейными относительно частоты, например.
5. Заключение
В топике не расмотрена работа с UTF-8 — тут никакий принципиальной разницы нету, разве что получение кодов символов и разбиение строки на символы будет несколько длиннее/сложнее.
Эти идеи можно распространить не только на кириллические кодировки, конечно — вопрос только в «спектрах» соответствующих языков/кодировок.
P.S. Если будет очень нужно/интересно — потом выложу второй частью полностью работающую библиотеку на GitHub. Хотя я считаю, что данных в посте вполне достаточно для быстрого написания такой библиотеки и самому под свои нужды — «спектр» для русского языка выложен, его можно без труда перенести на все нужные кодировки.
UPDATED
В комментариях проскочила замечательная функция, ссылку на которую я опубликовал под графом «убожество». Может быть погорячился со словами, но уж как опубликовал, так опубликовал — редактировать такие вещи не привык. Чтобы не быть голословным, давайте разберемся, работает ли она на 100%, как об этом говорит предполагаемый автор.
1) будут ли ошибки при «нормальной» работе этой функции? Предположим, что контент у нас на 100% валидный.
ответ: да, будут.
2) определит ли она что-нибудь кроме UTF-8 и не-UTF-8?
ответ: нет, не определит.
Функции для работы с многобайтовыми строками
Схемы многобайтного кодирования символов и их реализации достаточно сложны, и их описание находится за пределами этой документации. Более исчерпывающую информацию о кодировках и их устройстве можно почерпнуть из нижеприведённых источников.
Материалы по Юникоду
Информация о символах японской/корейской/китайской кодировок
Содержание
User Contributed Notes 35 notes
Please note that all the discussion about mb_str_replace in the comments is pretty pointless. str_replace works just fine with multibyte strings:
= ‘漢字はユニコード’ ;
$needle = ‘は’ ;
$replace = ‘Foo’ ;
?>
The usual problem is that the string is evaluated as binary string, meaning PHP is not aware of encodings at all. Problems arise if you are getting a value «from outside» somewhere (database, POST request) and the encoding of the needle and the haystack is not the same. That typically means the source code is not saved in the same encoding as you are receiving «from outside». Therefore the binary representations don’t match and nothing happens.
PHP can input and output Unicode, but a little different from what Microsoft means: when Microsoft says «Unicode», it unexplicitly means little-endian UTF-16 with BOM(FF FE = chr(255).chr(254)), whereas PHP’s «UTF-16» means big-endian with BOM. For this reason, PHP does not seem to be able to output Unicode CSV file for Microsoft Excel. Solving this problem is quite simple: just put BOM infront of UTF-16LE string.
SOME multibyte encodings can safely be used in str_replace() and the like, others cannot. It’s not enough to ensure that all the strings involved use the same encoding: obviously they have to, but it’s not enough. It has to be the right sort of encoding.
UTF-8 is one of the safe ones, because it was designed to be unambiguous about where each encoded character begins and ends in the string of bytes that makes up the encoded text. Some encodings are not safe: the last bytes of one character in a text followed by the first bytes of the next character may together make a valid character. str_replace() knows nothing about «characters», «character encodings» or «encoded text». It only knows about the string of bytes. To str_replace(), two adjacent characters with two-byte encodings just looks like a sequence of four bytes and it’s not going to know it shouldn’t try to match the middle two bytes.
While real-world examples can be found of str_replace() mangling text, it can be illustrated by using the HTML-ENTITIES encoding. It’s not one of the safe ones. All of the strings being passed to str_replace() are valid HTML-ENTITIES-encoded text so the «all inputs use the same encoding» rule is satisfied.
The text is «x = ‘x ;
mb_internal_encoding ( ‘HTML-ENTITIES’ );
?>
Even though neither ‘l’ nor ‘;’ appear in the text «x y» and in the other it broke the encoding completely.
One more reason to use UTF-8 if you can, I guess.
Yet another single-line mb_trim() function
PHP5 has no mb_trim(), so here’s one I made. It work just as trim(), but with the added bonus of PCRE character classes (including, of course, all the useful Unicode ones such as \pZ).
mb_detect_encoding
(PHP 4 >= 4.0.6, PHP 5, PHP 7, PHP 8)
mb_detect_encoding — Определение кодировки символов
Описание
Определяет наиболее вероятную кодировку символов для строки ( string ) string из упорядоченного списка кандидатов.
Автоматическое определение предполагаемой кодировки символов не может быть полностью надёжным; без дополнительной информации это похоже на расшифровку зашифрованной строки без ключа. Всегда предпочтительно использовать индикацию кодировки символов, хранящуюся или передаваемую с данными, такую как HTTP-заголовок «Content-Type».
Функция наиболее полезна с многобайтовыми кодировками, когда не все последовательности байтов образуют допустимую строку. Если входная строка содержит такую последовательность, эта кодировка будет отклонена, и будет проверена следующая кодировка.
Список параметров
Проверяемая строка ( string ).
Упорядоченный список кодировок символов. Список может быть указан как массив строк или как строка кодировок, разделённых запятыми.
Значение по умолчанию для strict можно установить с помощью параметра конфигурации mbstring.strict_detection.
Возвращаемые значения
Примеры
Пример #1 Пример использования mb_detect_encoding()
Пример #2 Действие параметра strict
// ‘áéóú’ закодирована в ISO-8859-1
$str = «\xE1\xE9\xF3\xFA» ;
Результат выполнения данного примера:
В некоторых случаях одна и та же последовательность байтов может образовывать допустимую строку в нескольких кодировках символов, и невозможно узнать, какая интерпретация предназначалась. Например, среди многих других байтовая последовательность «\xC4\xA2» может быть:
Пример #3 Использование порядка при совпадении нескольких кодировок
Результат выполнения данного примера:
Смотрите также
User Contributed Notes 23 notes
If you try to use mb_detect_encoding to detect whether a string is valid UTF-8, use the strict mode, it is pretty worthless otherwise.
If you need to distinguish between UTF-8 and ISO-8859-1 encoding, list UTF-8 first in your encoding_list:
mb_detect_encoding($string, ‘UTF-8, ISO-8859-1’);
if you list ISO-8859-1 first, mb_detect_encoding() will always return ISO-8859-1.
I used Chris’s function «detectUTF8» to detect the need from conversion from utf8 to 8859-1, which works fine. I did have a problem with the following iconv-conversion.
The problem is that the iconv-conversion to 8859-1 (with //TRANSLIT) replaces the euro-sign with EUR, although it is common practice that \x80 is used as the euro-sign in the 8859-1 charset.
I could not use 8859-15 since that mangled some other characters, so I added 2 str_replace’s:
if(detectUTF8($str)) <
$str=str_replace(«\xE2\x82\xAC»,»€»,$str);
$str=iconv(«UTF-8″,»ISO-8859-1//TRANSLIT»,$str);
$str=str_replace(«€»,»\x80″,$str);
>
If html-output is needed the last line is not necessary (and even unwanted).
Based upon that snippet below using preg_match() I needed something faster and less specific. That function works and is brilliant but it scans the entire strings and checks that it conforms to UTF-8. I wanted something purely to check if a string contains UTF-8 characters so that I could switch character encoding from iso-8859-1 to utf-8.
I modified the pattern to only look for non-ascii multibyte sequences in the UTF-8 range and also to stop once it finds at least one multibytes string. This is quite a lot faster.
A simple way to detect UTF-8/16/32 of file by its BOM (not work with string or file without BOM)
Function to detect UTF-8, when mb_detect_encoding is not available it may be useful.
Much simpler UTF-8-ness checker using a regular expression created by the W3C:
Just a note: Instead of using the often recommended (rather complex) regular expression by W3C (http://www.w3.org/International/questions/qa-forms-utf-8.en.php), you can simply use the ‘u’ modifier to test a string for UTF-8 validity:
For detect UTF-8, you can use:
In my environment (PHP 7.1.12),
«mb_detect_encoding()» doesn’t work
where «mb_detect_order()» is not set appropriately.
To enable «mb_detect_encoding()» to work in such a case,
simply put «mb_detect_order(‘. ‘)»
before «mb_detect_encoding()» in your script file.
Both
«ini_set(‘mbstring.language’, ‘. ‘);»
and
«ini_set(‘mbstring.detect_order’, ‘. ‘);»
DON’T work in script files for this purpose
whereas setting them in PHP.INI file may work.
beware : even if you need to distinguish between UTF-8 and ISO-8859-1, and you the following detection order (as chrigu suggests)
returns ISO-8859-1, while
bottom line : an ending ‘�’ (and probably other accentuated chars) mislead mb_detect_encoding
a) if the FUNCTION mb_detect_encoding is not available:
Sometimes mb_detect_string is not what you need. When using pdflib for example you want to VERIFY the correctness of utf-8. mb_detect_encoding reports some iso-8859-1 encoded text as utf-8.
To verify utf 8 use the following:
//
// utf8 encoding validation developed based on Wikipedia entry at:
// http://en.wikipedia.org/wiki/UTF-8
//
// Implemented as a recursive descent parser based on a simple state machine
// copyright 2005 Maarten Meijer
//
// This cries out for a C-implementation to be included in PHP core
//
function valid_1byte($char) <
if(!is_int($char)) return false;
return ($char & 0x80) == 0x00;
>
function valid_2byte($char) <
if(!is_int($char)) return false;
return ($char & 0xE0) == 0xC0;
>
function valid_3byte($char) <
if(!is_int($char)) return false;
return ($char & 0xF0) == 0xE0;
>
function valid_4byte($char) <
if(!is_int($char)) return false;
return ($char & 0xF8) == 0xF0;
>
function valid_nextbyte($char) <
if(!is_int($char)) return false;
return ($char & 0xC0) == 0x80;
>
This might be of interest when trying to convert utf-8 strings into ASCII suitable for URL’s, and such. this was never obvious for me since I’ve used locales for us and nl.
Last example for verifying UTF-8 has one little bug. If 10xxxxxx byte occurs alone i.e. not in multibyte char, then it is accepted although it is against UTF-8 rules. Make following replacement to repair it.
Replace
> // goto next char
with
> else <
return false; // 10xxxxxx occuring alone
> // goto next char
function isUTF8($str) <
if ($str === mb_convert_encoding(mb_convert_encoding($str, «UTF-32», «UTF-8»), «UTF-8», «UTF-32»)) <
return true;
> else <
return false;
>
>
Определение кодировки текста в PHP вместо mb_detect_encoding
Существует несколько кодировок символов кириллицы.
При создании сайтов в Интернете обычно используют:
Иногда появляется необходимость определить кодировку текста. И в PHP даже функция для этого есть:
но как писал m00t
в статье Определение кодировки текста в PHP — обзор существующих решений плюс еще один велосипед
Я протестировал функцию определения кодировки по кодам символов, результат меня удовлетворил и я использовал эту функцию пару лет.
Недавно решил переписать проект где использовал эту функцию, нашел готовый пакет на packagist.org cnpait/detect_encoding, в котором кодировка определяется методом m00t
При этом указанный пакет был установлен более 1200 раз, значит не у меня одного периодически возникает задача определения кодировки текста.
Мне бы установить этот пакет и успокоиться, но я решил «заморочиться».
В общем, сделал свой пакет: onnov/detect-encoding.
Как его использовать написано в README.md
А о его тестировании и сравнении с пакетом cnpait/detect_encoding напишу.
Методика тестирования
Берем большой текст: Tolstoy — Anna Karenina
Всего — 1’701’480 знаков
Убираем все лишнее, оставляем только кириллицу:
Осталось 1’336’252 кирилистических знаков.
В цикле берем часть текста (5, 15, 30,… символов) преобразуем в известную кодировку и пытаемся определить кодировку скриптом. Затем сравниваем правильно или нет.
Вот таблица в которой слева кодировки, сверху количество символов по которому определяем кодировку, в таблице результат достоверности в %%
К счастью, кодировки mac-cyrillic и ibm866 не используются для кодирования веб-страниц.
Точность определения высока даже в коротких предложениях от 5 до 10 букв. А для фраз из 60 букв точность определения достигает 100%. А еще, определение кодировки выполняется очень быстро, например, текст длиной более 1 300 000 символов кириллицы проверяется за 0 00096 секунд. (на моем компьютере)
А какие результаты покажет статистический способ описанный m00t:
Как видим результаты определения кодировки хорошие. Скорость работы скрипта высокая, особенно на коротких текстах, на огромных текстах скорость значительно уступает. Текст длиной более 1 300 000 символов кириллицы проверяется за 0 32 секунд. (на моем компьютере).
Определение кодировки текста в PHP вместо mb_detect_encoding
Существует несколько кодировок символов кириллицы.
При создании сайтов в Интернете обычно используют:
Иногда появляется необходимость определить кодировку текста. И в PHP даже функция для этого есть:
Я протестировал функцию определения кодировки по кодам символов, результат меня удовлетворил и я использовал эту функцию пару лет.
Недавно решил переписать проект где использовал эту функцию, нашел готовый пакет на packagist.org cnpait/detect_encoding, в котором кодировка определяется методом m00t
При этом указанный пакет был установлен более 1200 раз, значит не у меня одного периодически возникает задача определения кодировки текста.
Мне бы установить этот пакет и успокоиться, но я решил «заморочиться».
В общем, сделал свой пакет: onnov/detect-encoding.
Как его использовать написано в README.md
А о его тестировании и сравнении с пакетом cnpait/detect_encoding напишу.
Методика тестирования
Берем большой текст: Tolstoy — Anna Karenina
Всего — 1’701’480 знаков
Убираем все лишнее, оставляем только кириллицу:
Осталось 1’336’252 кирилистических знаков.
В цикле берем часть текста (5, 15, 30,… символов) преобразуем в известную кодировку и пытаемся определить кодировку скриптом. Затем сравниваем правильно или нет.
Вот таблица в которой слева кодировки, сверху количество символов по которому определяем кодировку, в таблице результат достоверности в %%
К счастью, кодировки mac-cyrillic и ibm866 не используются для кодирования веб-страниц.
Точность определения высока даже в коротких предложениях от 5 до 10 букв. А для фраз из 60 букв точность определения достигает 100%. А еще, определение кодировки выполняется очень быстро, например, текст длиной более 1 300 000 символов кириллицы проверяется за 0.00096 секунд. (на моем компьютере)
А какие результаты покажет статистический способ описанный m00t:
Как видим результаты определения кодировки хорошие. Скорость работы скрипта высокая, особенно на коротких текстах, на огромных текстах скорость значительно уступает. Текст длиной более 1 300 000 символов кириллицы проверяется за 0.32 секунд. (на моем компьютере).