6

無効な UTF-8 文字を引用符に置き換えたい (PHP 5.3.5)。

これまでのところ、この解決策がありますが、無効な文字は「?」に置き換えられるのではなく削除されます。

function replace_invalid_utf8($str)
{
  return mb_convert_encoding($str, 'UTF-8', 'UTF-8');
}

echo mb_substitute_character()."\n";

echo replace_invalid_utf8('éééaaaàààeeé')."\n";
echo replace_invalid_utf8('eeeaaaaaaeeé')."\n";

出力する必要があります:

63 // ASCII code for '?' character
???aaa???eé // or ??aa??eé
eeeaaaaaaeeé

しかし、現在は次のように出力されます:

63
aaaee // removed invalid characters
eeeaaaaaaeeé

何かアドバイス?

別の方法でそれを行いますか (preg_replace()たとえば、を使用しますか?)

ありがとう。

4

1 に答える 1

35

PHP 5.4 以降では、 mb_convert_encoding()またはhtmlspecialchars()ENT_SUBSTITUTEオプションを使用できます。もちろんpreg_match()も使えます。intl を使用する場合は、PHP 5.5 以降でUConverterを使用できます。

無効なバイト シーケンスの推奨代替文字はU+FFFDです。詳細については、UTR #36: Unicode セキュリティに関する考慮事項の「 3.1.2 不適切な形式のサブシーケンスの置換」を参照してください。

mb_convert_encoding()を使用する場合、Unicode コード ポイントをmb_substitute_character()またはmbstring.substitute_characterディレクティブに渡すことで、代替文字を指定できます。置換のデフォルト文字は ? です。(疑問符 - U+003F)。

// REPLACEMENT CHARACTER (U+FFFD)
mb_substitute_character(0xFFFD);

function replace_invalid_byte_sequence($str)
{
    return mb_convert_encoding($str, 'UTF-8', 'UTF-8');
}

function replace_invalid_byte_sequence2($str)
{
    return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE, 'UTF-8'));
}

UConverterは、手続き型とオブジェクト指向の両方の API を提供します。

function replace_invalid_byte_sequence3($str)
{
    return UConverter::transcode($str, 'UTF-8', 'UTF-8');
}

function replace_invalid_byte_sequence4($str)
{
    return (new UConverter('UTF-8', 'UTF-8'))->convert($str);
}

preg_match()を使用する場合、UTF-8 非最短形式の脆弱性を回避するために、バイトの範囲に注意する必要があります。トレール バイトの範囲は、リード バイトの範囲に応じて変化します。

lead byte: 0x00 - 0x7F, 0xC2 - 0xF4
trail byte: 0x80(or 0x90 or 0xA0) - 0xBF(or 0x8F)

バイト範囲の確認については、次のリソースを参照できます。

  1. RFC 3629 の「UTF-8 バイト シーケンスの構文
  2. Unicode 標準 6.1 の「表 3-7. 整形式の UTF-8 バイト シーケンス」
  3. 「W3C 国際化における「多言語フォーム エンコーディング」」

バイト範囲表は以下です。

      Code Points    First Byte Second Byte Third Byte Fourth Byte
  U+0000 -   U+007F   00 - 7F
  U+0080 -   U+07FF   C2 - DF    80 - BF
  U+0800 -   U+0FFF   E0         A0 - BF     80 - BF
  U+1000 -   U+CFFF   E1 - EC    80 - BF     80 - BF
  U+D000 -   U+D7FF   ED         80 - 9F     80 - BF
  U+E000 -   U+FFFF   EE - EF    80 - BF     80 - BF
 U+10000 -  U+3FFFF   F0         90 - BF     80 - BF    80 - BF
 U+40000 -  U+FFFFF   F1 - F3    80 - BF     80 - BF    80 - BF
U+100000 - U+10FFFF   F4         80 - 8F     80 - BF    80 - BF

有効な文字を壊さずに無効なバイト シーケンスを置き換える方法については、UTR #36: Unicode Security Considerationsの「 3.1.1 Ill-Formed Subsequences 」および「 Table 3-8. Use of U+FFFD in UTF-8 Conversion」に示していますユニコード標準。

Unicode 標準は例を示しています。

before: <61    F1 80 80  E1 80  C2    62    80    63    80    BF    64  >
after:  <0061  FFFD      FFFD   FFFD  0062  FFFD  0063  FFFD  FFFD  0064>

上記のルールに従って、 preg_replace_callback()による実装を次に示します。

function replace_invalid_byte_sequence5($str)
{
    // REPLACEMENT CHARACTER (U+FFFD)
    $substitute = "\xEF\xBF\xBD";
    $regex = '/
      ([\x00-\x7F]                       #   U+0000 -   U+007F
      |[\xC2-\xDF][\x80-\xBF]            #   U+0080 -   U+07FF
      | \xE0[\xA0-\xBF][\x80-\xBF]       #   U+0800 -   U+0FFF
      |[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} #   U+1000 -   U+CFFF
      | \xED[\x80-\x9F][\x80-\xBF]       #   U+D000 -   U+D7FF
      | \xF0[\x90-\xBF][\x80-\xBF]{2}    #  U+10000 -  U+3FFFF
      |[\xF1-\xF3][\x80-\xBF]{3}         #  U+40000 -  U+FFFFF
      | \xF4[\x80-\x8F][\x80-\xBF]{2})   # U+100000 - U+10FFFF
      |(\xE0[\xA0-\xBF]                  #   U+0800 -   U+0FFF (invalid)
      |[\xE1-\xEC\xEE\xEF][\x80-\xBF]    #   U+1000 -   U+CFFF (invalid)
      | \xED[\x80-\x9F]                  #   U+D000 -   U+D7FF (invalid)
      | \xF0[\x90-\xBF][\x80-\xBF]?      #  U+10000 -  U+3FFFF (invalid)
      |[\xF1-\xF3][\x80-\xBF]{1,2}       #  U+40000 -  U+FFFFF (invalid)
      | \xF4[\x80-\x8F][\x80-\xBF]?)     # U+100000 - U+10FFFF (invalid)
      |(.)                               # invalid 1-byte
    /xs';

    // $matches[1]: valid character
    // $matches[2]: invalid 3-byte or 4-byte character
    // $matches[3]: invalid 1-byte

    $ret = preg_replace_callback($regex, function($matches) use($substitute) {

        if (isset($matches[2]) || isset($matches[3])) {

            return $substitute;

        }
    
        return $matches[1];

    }, $str);

    return $ret;
}

この方法により、バイトを直接比較し、バイト サイズに関する preg_match の制限を回避できます。

function replace_invalid_byte_sequence6($str) {

    $size = strlen($str);
    $substitute = "\xEF\xBF\xBD";
    $ret = '';

    $pos = 0;
    $char;
    $char_size;
    $valid;

    while (utf8_get_next_char($str, $size, $pos, $char, $char_size, $valid)) {
        $ret .= $valid ? $char : $substitute;
    }

    return $ret;
}

function utf8_get_next_char($str, $str_size, &$pos, &$char, &$char_size, &$valid)
{
    $valid = false;

    if ($str_size <= $pos) {
        return false;
    }

    if ($str[$pos] < "\x80") {

        $valid = true;
        $char_size =  1;

    } else if ($str[$pos] < "\xC2") {

        $char_size = 1;

    } else if ($str[$pos] < "\xE0")  {

        if (!isset($str[$pos+1]) || $str[$pos+1] < "\x80" || "\xBF" < $str[$pos+1]) {

            $char_size = 1;

        } else {

            $valid = true;
            $char_size = 2;

        }

    } else if ($str[$pos] < "\xF0") {

        $left = "\xE0" === $str[$pos] ? "\xA0" : "\x80";
        $right = "\xED" === $str[$pos] ? "\x9F" : "\xBF";

        if (!isset($str[$pos+1]) || $str[$pos+1] < $left || $right < $str[$pos+1]) {

            $char_size = 1;

        } else if (!isset($str[$pos+2]) || $str[$pos+2] < "\x80" || "\xBF" < $str[$pos+2]) {

            $char_size = 2;

        } else {

            $valid = true;
            $char_size = 3;

       }

    } else if ($str[$pos] < "\xF5") {

        $left = "\xF0" === $str[$pos] ? "\x90" : "\x80";
        $right = "\xF4" === $str[$pos] ? "\x8F" : "\xBF";

        if (!isset($str[$pos+1]) || $str[$pos+1] < $left || $right < $str[$pos+1]) {

            $char_size = 1;

        } else if (!isset($str[$pos+2]) || $str[$pos+2] < "\x80" || "\xBF" < $str[$pos+2]) {

            $char_size = 2;

        } else if (!isset($str[$pos+3]) || $str[$pos+3] < "\x80" || "\xBF" < $str[$pos+3]) {

            $char_size = 3;

        } else {

            $valid = true;
            $char_size = 4;

        }

    } else {

        $char_size = 1;

    }

    $char = substr($str, $pos, $char_size);
    $pos += $char_size;

    return true;
}

テストケースはこちら。

function run(array $callables, array $arguments)
{
    return array_map(function($callable) use($arguments) {
         return array_map($callable, $arguments);
    }, $callables);
}
    
$data = [
    // Table 3-8. Use of U+FFFD in UTF-8 Conversion
    // http://www.unicode.org/versions/Unicode6.1.0/ch03.pdf)
    "\x61"."\xF1\x80\x80"."\xE1\x80"."\xC2"."\x62"."\x80"."\x63"
    ."\x80"."\xBF"."\x64",

    // 'FULL MOON SYMBOL' (U+1F315) and invalid byte sequence
    "\xF0\x9F\x8C\x95"."\xF0\x9F\x8C"."\xF0\x9F\x8C"
];

var_dump(run([
    'replace_invalid_byte_sequence', 
    'replace_invalid_byte_sequence2',
    'replace_invalid_byte_sequence3',
    'replace_invalid_byte_sequence4',
    'replace_invalid_byte_sequence5',
    'replace_invalid_byte_sequence6'
], $data));

注意として、mb_convert_encodingには、無効なバイト シーケンスの直後に有効な文字を壊すか、 U+FFFDを追加せずに有効な文字の後に無効なバイト シーケンスを削除するバグがあります。

$data = [
    // U+20AC
    "\xE2\x82\xAC"."\xE2\x82\xAC"."\xE2\x82\xAC",
    "\xE2\x82"    ."\xE2\x82\xAC"."\xE2\x82\xAC",

    // U+24B62
    "\xF0\xA4\xAD\xA2"."\xF0\xA4\xAD\xA2"."\xF0\xA4\xAD\xA2",
    "\xF0\xA4\xAD"    ."\xF0\xA4\xAD\xA2"."\xF0\xA4\xAD\xA2",
    "\xA4\xAD\xA2"."\xF0\xA4\xAD\xA2"."\xF0\xA4\xAD\xA2",

    // 'FULL MOON SYMBOL' (U+1F315)
    "\xF0\x9F\x8C\x95" . "\xF0\x9F\x8C",
    "\xF0\x9F\x8C\x95" . "\xF0\x9F\x8C" . "\xF0\x9F\x8C"
];

preg_match()はpreg_replace_callbackの代わりに使用できますが、この関数にはバイトサイズの制限があります。詳細については、バグ レポート#36463を参照してください。以下のテストケースで確認できます。

str_repeat('a', 10000)

最後に、私のベンチマークの結果は次のとおりです。

mb_convert_encoding()
0.19628190994263
htmlspecialchars()
0.082863092422485
UConverter::transcode()
0.15999984741211
UConverter::convert()
0.29843020439148
preg_replace_callback()
0.63967490196228
direct comparision
0.71933102607727

ベンチマークコードはこちら。

function timer(array $callables, array $arguments, $repeat = 10000)
{

    $ret = [];
    $save = $repeat;

    foreach ($callables as $key => $callable) {

        $start = microtime(true);

        do {
    
            array_map($callable, $arguments);

        } while($repeat -= 1);

        $stop = microtime(true);
        $ret[$key] = $stop - $start;
        $repeat = $save;

    }

    return $ret;
}

$functions = [
    'mb_convert_encoding()' => 'replace_invalid_byte_sequence',
    'htmlspecialchars()' => 'replace_invalid_byte_sequence2',
    'UConverter::transcode()' => 'replace_invalid_byte_sequence3',
    'UConverter::convert()' => 'replace_invalid_byte_sequence4',
    'preg_replace_callback()' => 'replace_invalid_byte_sequence5',
    'direct comparision' => 'replace_invalid_byte_sequence6'
];

foreach (timer($functions, $data) as $description => $time) {

    echo $description, PHP_EOL,
         $time, PHP_EOL;

}
于 2012-12-04T02:54:00.437 に答える