PHP: конвертировать любую строку в UTF-8 без знания исходного набора символов или, по крайней мере, попробовать

У меня есть приложение, которое касается клиентов со всего мира, и, естественно, я хочу, чтобы все, что попадало в мои базы данных, было кодировано в кодировке UTF-8.

Основная проблема для меня в том, что я не знаю, какая кодировка источника какой-либо строки будет - она ​​может быть из текстового поля (использование <form accept-charset="utf-8"> полезно, только если пользователь действительно отправил форму), или это может быть из загруженного текстового файла, поэтому я действительно не могу контролировать вход.

Мне нужна функция или класс, который гарантирует, что материал, поступающий в мою базу данных, является, насколько это возможно, кодировкой UTF-8. Я пробовал iconv(mb_detect_encoding($text), "UTF-8", $text); но у этого есть проблемы (если вход "fiancée", он возвращает "fianc" ). Я пробовал много вещей =/

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

Я прочитал другие SO-вопросы по этому вопросу, но они, похоже, имеют тонкие различия, такие как "Мне нужно разобрать RSS-каналы" или "Я удаляю данные с веб-сайтов" (или, действительно, "Вы не можете" ).

Но должно быть что-то, что, по крайней мере, имеет хорошую попытку!

Ответ 1

То, о чем вы просите, чрезвычайно сложно. Если возможно, то лучше всего задать пользователю кодировку. Предотвращение атаки не должно быть намного проще или сложнее.

Однако вы можете попробовать:

iconv(mb_detect_encoding($text, mb_detect_order(), true), "UTF-8", $text);

Установка строгого значения может помочь вам получить лучший результат.

Ответ 2

В родине России у нас есть 4 популярных кодировки, поэтому ваш вопрос здесь очень востребован.

Только кодами символов char вы не можете обнаружить кодировку, потому что страницы кода пересекаются. Некоторые кодовые страницы на разных языках имеют даже полное пересечение. Итак, нам нужен другой подход.

Единственный способ работы с неизвестными кодировками - работать с вероятностями. Итак, мы не хотим отвечать на вопрос "что такое кодирование этого текста?", Мы пытаемся понять ", что, скорее всего, кодирование этого текста?".

Один парень из популярного российского технологического блога придумал такой подход:

Создайте диапазон вероятности кодов char в каждой кодировке, которую вы хотите поддерживать. Вы можете построить его, используя некоторые большие тексты на вашем языке (например, какую-то фикцию, используйте Шекспир для английского и Толстого для русского, lol). Вы получите что-то вроде этого:

    encoding_1:
    190 => 0.095249209893009,
    222 => 0.095249209893009,
    ...
    encoding_2:
    239 => 0.095249209893009,
    207 => 0.095249209893009,
    ...
    encoding_N:
    charcode => probabilty

Далее. Вы берете текст в неизвестной кодировке и для каждой кодировки в вашем "вероятностном словаре" вы ищете частоту каждого символа в тексте с неизвестным кодированием. Суммарная вероятность символов. Кодирование с большим рейтингом, вероятно, является победителем. Лучшие результаты для больших текстов.

Если вам интересно, я могу с радостью помочь вам в решении этой задачи. Мы можем значительно повысить точность, создав список вероятностей с двумя символами.

Btw. mb_detect_encoding certanly не работает. Да, вообще. Пожалуйста, посмотрите исходный код mb_detect_encoding в "ext/mbstring/libmbfl/mbfl/mbfl_ident.c".

Ответ 3

Вероятно, вы пробовали это, но почему бы просто не использовать функцию mb_convert_encoding? Он попытается автоматически определить char набор предоставленного текста или вы можете передать ему список.

Кроме того, я попытался запустить:

$text = "fiancée";
echo mb_convert_encoding($text, "UTF-8");
echo "<br/><br/>";
echo iconv(mb_detect_encoding($text), "UTF-8", $text);

и результаты одинаковы для обоих. Как вы видите, что ваш текст усечен до 'fianc'? это в БД или в браузере?

Ответ 4

Невозможно идентифицировать кодировку строки, которая является полностью точной. Есть способы попытаться угадать кодировку. Один из этих способов и, вероятно,/в настоящее время лучший в PHP, это mb_detect_encoding(). Это сканирует вашу строку и ищет вхождения вещей, уникальных для определенных кодировок. В зависимости от вашей строки не может быть таких различимых случаев.

Возьмите кодировку ISO-8859-1 и ISO-8859-15 (http://en.wikipedia.org/wiki/ISO/IEC_8859-15#Changes_from_ISO-8859-1)

Есть только несколько разных персонажей, и чтобы они ухудшались, они представлялись одними и теми же байтами. Невозможно обнаружить, получив строку, не зная ее кодировки, должен ли байт 0xA4 обозначать ¤ или € в вашей строке, поэтому нет способа узнать его точную кодировку.

(Примечание: вы могли бы добавить человеческий фактор или еще более совершенный метод сканирования (например, что предлагает Oroboros102), чтобы попытаться выяснить, основываясь на окружающем контексте, если персонаж должен быть ¤ или €, хотя это кажется как мост слишком далеко)

Существуют более различимые различия между, например, UTF-8 и ISO-8859-1, поэтому по-прежнему стоит попытаться понять это, когда вы не уверены, хотя вы можете и не должны полагаться на то, что это правильно.

Интересное чтение: http://kore-nordmann.de/blog/php_charset_encoding_FAQ.html#how-do-i-determine-the-charset-encoding-of-a-string

Существуют и другие способы обеспечения правильной кодировки. Что касается форм, попробуйте максимально усилить UTF-8 (проверьте снеговика, чтобы убедиться, что вы будете представлять UTF-8 в каждом браузере: http://intertwingly.net/blog/2010/07/29/Rails-and-Snowmen) Это делается, по крайней мере, вы можете быть уверены, что каждый текст, представленный через ваши формы, - utf_8. В отношении загруженных файлов попробуйте запустить команду unix 'file -i' на ней, например. exec() (если возможно, на вашем сервере), чтобы помочь обнаружению (используя спецификацию документа). Что касается скребущих данных, вы можете прочитать заголовки HTTP, которые обычно определяют кодировку. При анализе XML файлов см., Если метаданные XML содержат определение набора символов.

Вместо того, чтобы пытаться автоматически угадать кодировку, вы должны сначала попытаться обеспечить определенную кодировку самостоятельно, если это возможно, или попытаться получить определение из источника, из которого вы его получаете (если применимо), прежде чем прибегать к обнаружению.

Ответ 5

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

Я не думаю, что это проблема. Приложение знает источник ввода. Если это из формы, используйте кодировку UTF-8 в вашем случае. Это работает. Просто убедитесь, что предоставленные данные правильно закодированы (проверка). Имейте в виду, что не все базы данных поддерживают UTF-8 в полном объеме.

Если это файл, вы не сохраните его в кодировке UTF-8 в базе данных, а в двоичной форме. Когда вы снова выводите файл, используйте также двоичный вывод, тогда это полностью прозрачно.

Ваша идея хорошая, что пользователь может сказать кодировку, если он/она может все равно сказать после загрузки файла, так как он является двоичным.

Поэтому я должен признать, что я не вижу конкретной проблемы, которую вы поднимаете с вашим вопросом. Но, возможно, вы можете добавить несколько подробностей о вашей проблеме.

Ответ 6

Вы можете настроить набор показателей, чтобы попытаться угадать, какая кодировка используется. Опять же, не идеально, но может уловить некоторые промахи от mb_detect_encoding().

Ответ 7

Если вы хотите "взять это на консоль", я бы рекомендовал enca. В отличие от довольно упрощенного mb_detect_encoding, он использует "смесь анализа, статистического анализа, угадывания и черной магии для определения их кодировок" (lol - см. справочная страница). Тем не менее, вы обычно должны передавать язык входного файла, если вы хотите определить такие кодировки для конкретной страны. (Тем не менее, mb_detect_encoding по существу имеет то же требование, что и кодирование должно появиться "в нужном месте" в списке переданных кодировок, чтобы он мог быть обнаружен вообще.)

enca также появился здесь: Как найти кодировку файла в Unix через script (ы)

Ответ 8

Грим, есть действительно хорошие ответы и попытки ответить на ваш вопрос здесь. Я хотел бы поблагодарить всех за их ответы. Они великолепны. Я не являюсь мастером кодирования, но я понимаю ваше желание иметь чистый стек UTF-8 в вашей базе данных. Я использую MySQL utf8mb4 для таблиц, полей и подключений.

Моя ситуация сводилась к "Я просто хочу, чтобы мои дезинфицирующие средства, валидаторы, бизнес-логика и подготовленные заявления обрабатывали UTF-8, когда данные поступают из форм HTML или ссылок на регистрацию по электронной почте". Итак, по-моему, я начал с этой идеи:

  • Попытка обнаружения кодировки: $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];
  • Если кодировка не может быть обнаружена, throw new RuntimeException
  • Если ввод UTF-8, продолжайте.
  • Иначе, если это ISO-8859-1 или ASCII

    а. Попытка преобразования в UTF-8 (ожидание, не завершено)

    б. Определить кодировку преобразованного значения

    с. Если сообщенное кодированное и преобразованное значение равно UTF-8, продолжайте.

    д. Else, throw new RuntimeException

Из моего абстрактного класса Sanitizer

Sanitizer

    private function isUTF8($encoding, $value)
    {
        return (($encoding === 'UTF-8') && (utf8_encode(utf8_decode($value)) === $value));
    }

    private function utf8tify(&$value)
    {
        $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];

        mb_internal_encoding('UTF-8');
        mb_substitute_character(0xfffd); //REPLACEMENT CHARACTER
        mb_detect_order($encodings);

        $stringEncoding = mb_detect_encoding($value, $encodings, true);

        if (!$stringEncoding) {
            $value = null;
            throw new \RuntimeException("Unable to identify character encoding in sanitizer.");
        }

        if ($this->isUTF8($stringEncoding, $value)) {
            return;
        } else {
            $value = mb_convert_encoding($value, 'UTF-8', $stringEncoding);
            $stringEncoding = mb_detect_encoding($value, $encodings, true);

            if ($this->isUTF8($stringEncoding, $value)) {
                return;
            } else {
                $value = null;
                throw new \RuntimeException("Unable to convert character encoding from ISO-8859-1, or ASCII, to UTF-8 in sanitizer.");
            }
        }

        return;
    }

Можно было бы аргументировать, что я должен отделить проблемы кодирования от моего абстрактного класса Sanitizer и просто вставить объект Encoder в конкретный дочерний экземпляр Sanitizer. Однако основная проблема с моим подходом заключается в том, что я без каких-либо знаний отвергаю типы кодирования, которые мне не нужны (и я полагаюсь на функции PHP mb_ *). Без дальнейшего изучения я не могу знать, причиняет ли боль некоторым людям или нет (или, если я теряю важную информацию). Поэтому мне нужно больше узнать. Я нашел эту статью.

Что каждый программист абсолютно, должен знать о кодировании и наборах символов для работы с текстом

Кроме того, что происходит, когда зашифрованные данные добавляются к моим ссылкам регистрации электронной почты (используя OpenSSL или mcrypt)? Может ли это помешать расшифровке? Что относительно Windows-1252? Как насчет последствий для безопасности? Использование utf8_decode() и utf8_encode() в Sanitizer::isUTF8 сомнительно.

Люди указали на короткое замыкание в функциях PHP mb_ *. Я никогда не занимался исследованием iconv, но если он работает лучше, чем функции mb_ *, дайте мне знать.

Ответ 9

public function convertToUtf8($text) {
    if(!$this->html)
        $this->html = cURL('http://'.$this->url, array('timeout' => 15));

    $html = $this->html;
    preg_match('/<meta.*?charset=(|\")(.*?)("|\")/i', $html, $matches);

    $charset = $matches[2];

    if($charset)
        return mb_convert_encoding($text, 'UTF-8', $charset);
    else
        return $text;
}

Параметры по умолчанию cURL:

curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);

Я пробовал что-то вроде этого. Это помогло мне. Если вы найдете в мета-кодировке информацию, я конвертирую, иначе ничего не делаю.