将CESU-8转换为UTF-8,具有高性能

时间:2015-12-08 08:26:58

标签: php performance unicode utf-8 cesu-8

我有一些原始文本通常是有效的UTF-8字符串。然而,事实证明输入实际上是一个CESU-8字符串。从技术上来说可以检测到并转换为UTF-8,但由于这种情况很少发生,我宁愿不花费大量的CPU时间来做这件事。

是否有快速方法来检测字符串是否使用CESU-8或UTF-8进行编码?我想我总是可以盲目地转换" UTF-8"使用iconv()到UTF-16LE再到UTF-8,我可能每次都得到正确的结果,因为CESU-8足够接近UTF-8才能工作。 你能建议更快吗?(我希望输入字符串是CESU-8而不是有效的UTF-8,大约是所有字符串出现的0.01-0.1%。)

(CESU-8是一种非标准字符串格式,包含以UTF-8编码的16位代理项对。技术上,UTF-8字符串应包含由这些代理对表示的字符,而不是代理对本身。)

3 个答案:

答案 0 :(得分:3)

CESU-8字符串将使用字节序列编码代理项对:

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

即:0xED,后跟0xA00xBF(包括)之间的任意字节,后跟0x800xBF之间的任何字节(包括)。

这样的字节序列不能出现在任何有效的UTF-8字符串中,并且是唯一允许在CESU-8中出现超过UTF-8的字节。检查这样的字节序列应该可靠地检测CESU-8,并且可能比解码整个字符串更快。

答案 1 :(得分: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

这是一个online example

答案 2 :(得分:0)

以下是我目前正在使用的实现:

/**
 * @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);
}

(如果你有一个大端CPU,你可能需要pack("V", ...)以上......)