Преобразование PHP cp1252/windows-1252 в UTF-8

Я пытаюсь преобразовать нашу базу данных с latin1 в UTF-8. К сожалению, я не могу сделать массовое одноключение, так как приложение должно оставаться в сети, и у нас есть 700 ГБ базы данных для конвертирования.

Итак, я пытаюсь использовать небольшой mysql-взлом преобразования таблиц в UTF-8, но не данные. Я бы хотел, чтобы данные были прочитаны, преобразованы и заменены в реальном времени. (Преобразование JIT, если вы это сделаете)

В настоящее время наше php-приложение использует все значения по умолчанию, поэтому он подключается к mysql, используя набор символов latin1, и он переносит данные UTF-8, закодированные в latin1. Когда вы просматриваете данные с latin1, символы UTF-8 отображаются, как ожидалось. Когда вы просматриваете данные с помощью UTF-8, все становится беспорядочным.

Итак, я предлагаю принудительно установить набор символов mysql в UTF-8, а затем при необходимости сделать преобразование данных в нужное время. Теперь, видя, что cp1252/windows-1252 является подмножеством UTF-8, он не настолько прямолинейный (насколько я могу видеть), чтобы обнаружить кодировку cp1252/windows-1252.

Я написал следующий код, который пытается обнаружить кодировку cp1252/windows-1252 и преобразовать по мере необходимости. Он также должен обнаруживать правильно закодированные символы UTF-8 и ничего не делать.

$a = 'Card☃'; //cp1252 encoded
$a_test = '☃'.$a; //add known UTF8 character
$c = mb_convert_encoding($a_test, 'cp1252', 'UTF-8');
// attempt to detect known utf8 character after conversion
if (mb_strpos($c, '☃') === false) {
    // not found, original string was not cp1252 encoded, so print
    var_dump($a);
} else {
    // found, original string was cp1252 encoded, remove test character and print
    // This case runs
    $c = mb_strcut($c, 1);
    var_dump($c);
}

$a = 'COD☃'; //proper UTF8 encoded
$a_test = '☃'.$a; //add known UTF8 character
$c = mb_convert_encoding($a_test, 'cp1252', 'UTF-8');
// attempt to detect known utf8 character after conversion
if (mb_strpos($c, '☃') === false) {
    // not found, original string was not cp1252 encoded, so print
    // This case runs
    var_dump($a);
} else {
    // found, original string was cp1252 encoded, remove test character and print
    $c = mb_strcut($c, 1);
    var_dump($c);
}

Результат запуска этого кода:

string 'Card☃' (length=7)
string 'COD☃' (length=6)

Я понимаю, что выполнение этого на всех строках, выходящих из базы данных, будет иметь влияние на производительность, но еще не измерено, но если я смогу сделать JIT-преобразование, прежде чем переключать все, то это мне того стоит.

Есть ли у кого-нибудь указания относительно того, как оптимизировать это?

Ответ 1

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

Во-вторых, невозможно обрабатывать строки как с символами CP1252, так и с UTF-8 (на самом деле для CP1252 это байт, а для Unicode - кодовая точка). Или вы пытаетесь прочитать его как CP1252, и видите все символы Юникода в виде одиночных байтов, или вы читаете его как UTF-8, и он вырезает любые недопустимые последовательности байтов (или создает случайные символы, если символы CP1252 соответствуют кодовой точке Юникода), Вы не удаляете тестовый символ с помощью $c = mb_strcut($c, 1);, вы удаляете вопросительный знак, созданный mb_convert_encoding, потому что он не может преобразовать этот символ Юникода в символ CP1252.

В-третьих, вы должны никогда преобразовать строку, а затем после того, как попытаться определить кодировку. После преобразования второй тестовой строки это было ?COD?. Нет причин проверять, существует ли в нем символ Unicode, потому что вы преобразовали его в CP1252. В нем не могут быть символы Юникода. Как программист, вы должны знать, что такое выход.

Единственное решение - проверить, является ли строка CP1252, преобразовать символы-нарушители в заполнители и затем преобразовать эту строку в Unicode:

function convert_cp1252_to_utf8($input, $default = '', $replace = array()) {
    if ($input === null || $input == '') {
        return $default;
    }

    // https://en.wikipedia.org/wiki/UTF-8
    // https://en.wikipedia.org/wiki/ISO/IEC_8859-1
    // https://en.wikipedia.org/wiki/Windows-1252
    // http://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT
    $encoding = mb_detect_encoding($input, array('Windows-1252', 'ISO-8859-1'), true);
    if ($encoding == 'ISO-8859-1' || $encoding == 'Windows-1252') {
        /*
         * Use the search/replace arrays if a character needs to be replaced with
         * something other than its Unicode equivalent.
         */ 

        /*$replace = array(
            128 => "€",      // http://www.fileformat.info/info/unicode/char/20AC/index.htm EURO SIGN
            129 => "",              // UNDEFINED
            130 => "‚",      // http://www.fileformat.info/info/unicode/char/201A/index.htm SINGLE LOW-9 QUOTATION MARK
            131 => "ƒ",      // http://www.fileformat.info/info/unicode/char/0192/index.htm LATIN SMALL LETTER F WITH HOOK
            132 => "„",      // http://www.fileformat.info/info/unicode/char/201e/index.htm DOUBLE LOW-9 QUOTATION MARK
            133 => "…",      // http://www.fileformat.info/info/unicode/char/2026/index.htm HORIZONTAL ELLIPSIS
            134 => "†",      // http://www.fileformat.info/info/unicode/char/2020/index.htm DAGGER
            135 => "‡",      // http://www.fileformat.info/info/unicode/char/2021/index.htm DOUBLE DAGGER
            136 => "ˆ",      // http://www.fileformat.info/info/unicode/char/02c6/index.htm MODIFIER LETTER CIRCUMFLEX ACCENT
            137 => "‰",      // http://www.fileformat.info/info/unicode/char/2030/index.htm PER MILLE SIGN
            138 => "Š",      // http://www.fileformat.info/info/unicode/char/0160/index.htm LATIN CAPITAL LETTER S WITH CARON
            139 => "‹",      // http://www.fileformat.info/info/unicode/char/2039/index.htm SINGLE LEFT-POINTING ANGLE QUOTATION MARK
            140 => "Œ",      // http://www.fileformat.info/info/unicode/char/0152/index.htm LATIN CAPITAL LIGATURE OE
            141 => "",              // UNDEFINED
            142 => "Ž",      // http://www.fileformat.info/info/unicode/char/017d/index.htm LATIN CAPITAL LETTER Z WITH CARON 
            143 => "",              // UNDEFINED
            144 => "",              // UNDEFINED
            145 => "‘",      // http://www.fileformat.info/info/unicode/char/2018/index.htm LEFT SINGLE QUOTATION MARK 
            146 => "’",      // http://www.fileformat.info/info/unicode/char/2019/index.htm RIGHT SINGLE QUOTATION MARK
            147 => "“",      // http://www.fileformat.info/info/unicode/char/201c/index.htm LEFT DOUBLE QUOTATION MARK
            148 => "”",      // http://www.fileformat.info/info/unicode/char/201d/index.htm RIGHT DOUBLE QUOTATION MARK
            149 => "•",      // http://www.fileformat.info/info/unicode/char/2022/index.htm BULLET
            150 => "–",      // http://www.fileformat.info/info/unicode/char/2013/index.htm EN DASH
            151 => "—",      // http://www.fileformat.info/info/unicode/char/2014/index.htm EM DASH
            152 => "˜",      // http://www.fileformat.info/info/unicode/char/02DC/index.htm SMALL TILDE
            153 => "™",      // http://www.fileformat.info/info/unicode/char/2122/index.htm TRADE MARK SIGN
            154 => "š",      // http://www.fileformat.info/info/unicode/char/0161/index.htm LATIN SMALL LETTER S WITH CARON
            155 => "›",      // http://www.fileformat.info/info/unicode/char/203A/index.htm SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
            156 => "œ",      // http://www.fileformat.info/info/unicode/char/0153/index.htm LATIN SMALL LIGATURE OE
            157 => "",              // UNDEFINED
            158 => "ž",      // http://www.fileformat.info/info/unicode/char/017E/index.htm LATIN SMALL LETTER Z WITH CARON
            159 => "Ÿ",      // http://www.fileformat.info/info/unicode/char/0178/index.htm LATIN CAPITAL LETTER Y WITH DIAERESIS
        );*/

        if (count($replace) != 0) {
            $find = array();
            foreach (array_keys($replace) as $key) {
                $find[] = chr($key);
            }
            $input = str_replace($find, array_values($replace), $input);
        }
        /*
         * Because ISO-8859-1 and CP1252 are identical except for 0x80 through 0x9F
         * and control characters, always convert from Windows-1252 to UTF-8.
         */
        $input = iconv('Windows-1252', 'UTF-8//IGNORE', $input);
        if (count($replace) != 0) {
            $input = html_entity_decode($input);
        }
    }
    return $input;
}

Фокус в том, что вы должны проверить как ISO-8859-1, так и CP1252, потому что они настолько похожи. Я нашел это трудным путем после нескольких часов игры с этой функцией, только чтобы этот ответ сохранить меня. Если вы нашли эту функцию полезной, откройте ответ +1.

В принципе, эта функция заменяет все эти плохие байты CP1252 объектами HTML, представляющими символы Unicode. Затем мы преобразуем строку из ISO-8859-1/CP1252 в UTF-8, в то время как ни один из наших новых символов Юникода не искажен, потому что они простые символы ASCII. Наконец, мы декодируем объекты HTML и, наконец, имеем 100% -ную строку Unicode.