Конвертировать CESU-8 в UTF-8 с высокой производительностью

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

Есть ли быстро способ определить, кодируется ли строка с помощью CESU-8 или UTF-8? Я думаю, я всегда мог слепо конвертировать UTF-8 в UTF-16LE, а затем в UTF-8, используя iconv() и я, вероятно, получал бы правильный результат каждый раз, потому что CESU-8 достаточно близко к UTF-8, чтобы это работало. Можете ли вы предложить что-нибудь быстрее? (Я ожидаю, что входная строка будет CESU-8 вместо действительного UTF-8 в пределах 0,01-0,1% от всех вхождений строки.)

(CESU-8 — это нестандартный формат строк, который содержит 16-битные суррогатные пары, закодированные в UTF-8. Технически строки UTF-8 должны содержать символы, представленные этими суррогатными парами, а не сами суррогатные пары.)

2

Решение

Вот более эффективная версия вашей функции преобразования:

$regex = '@(\xED[\xA0-\xAF][\x80-\xBF]\xED[\xB0-\xBF][\x80-\xBF])@';
$s = preg_replace_callback($regex, function($m) {
$in = unpack("C*", $m[0]);
$in[2] += 1; // Effectively adds 0x10000 to the codepoint.
return pack("C*",
0xF0 | (($in[2] & 0x1C) >> 2),
0x80 | (($in[2] & 0x03) << 4) | (($in[3] & 0x3C) >> 2),
0x80 | (($in[3] & 0x03) << 4) | ($in[5] & 0x0F),
$in[6]
);
}, $s);

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

ED       A0-AF    80-BF    ED       B0-BF    80-BF
11101101 1010aaaa 10bbbbbb 11101101 1011cccc 10dddddd

в

F0-F4    80-BF    80-BF    80-BF
11110oaa 10aabbbb 10bbcccc 10dddddd    // o is "overflow" bit

Вот онлайн пример.

2

Другие решения

Строки CESU-8 будут кодировать суррогатные пары, используя последовательность байтов:

ED [A0..BF] [80..BF]

То есть: 0xEDс последующим байтом между 0xA0 а также 0xBF (включительно), за которым следует любой байт между 0x80 а также 0xBF (Включительно).

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

3

Вот реализация, которую я сейчас использую:

/**
* @param string $s raw input with UTF-8 or CESU-8 encoding
* @return string input with UTF-8 encoding
* @license MIT
*/
protected function verifyValidUtf8($s)
{
$s = preg_replace_callback('@(?:\xED[\xA0-\xBF][\x80-\xBF]){2}@', function ($m)
{
$bytes = unpack("C*", $m[0]); # always 6 bytes

# create UCS-4 character from CESU-8 encoded surrogate pair in $bytes

# 3 bytes CESU-8 to UNICODE high surrogate:
$high = (($bytes[1] & 0x0F) << 12) + (($bytes[2] & 0x3F) << 6) + ($bytes[3] & 0x3F);
# 3 bytes CESU-8 to UNICODE low surrogate:
$low = (($bytes[4] & 0x0F) << 12) + (($bytes[5] & 0x3F) << 6) + ($bytes[6] & 0x3F);

$codepoint = ($high & 0x03FF) << 10 | ($low & 0x03FF);
$codepoint += 0x10000;
return mb_convert_encoding(pack("N", $codepoint), "UTF-8", "UTF-32");
}, $s);

# replace unmatched surrogate pairs with U+FFFD REPLACEMENT CHARACTER
return preg_replace('@\xED[\xA0-\xBF][\x80-\xBF]@', "\xEF\xBF\xBD", $s);
}

(Вам может понадобиться pack("V", ...) выше, если у вас процессор с прямым порядком байтов …)

0
По вопросам рекламы [email protected]