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

Я хотел бы заменить недействительные символы 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() ?)

Благодарю.

Related of "Замена недопустимых символов UTF-8 на вопросительные знаки, mbstring.substitute_character кажется проигнорированным"

Вы можете использовать параметр mb_convert_encoding () или htmlspecialchars () ENT_SUBSTITUTE, начиная с PHP 5.4. В курсе вы также можете использовать preg_match () . Если вы используете intl, вы можете использовать UConverter с PHP 5.5.

Рекомендуемый символ-заменитель для недопустимой последовательности байтов – U + FFFD . см. « 3.1.2 Подстановка для некорректных подпоследовательностей » в UTR № 36: соображения безопасности Unicode для деталей.

При использовании 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. « Синтаксис последовательностей байтов UTF-8 » в RFC 3629
  2. « Таблица 3-7. Хорошо сформированные последовательности байтов UTF-8 » в стандарте Unicode 6.1
  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 

Как заменить недопустимую последовательность байтов без нарушения допустимых символов, показано в « 3.1.1 некорректированные подпоследовательности» в UTR # 36: соображения безопасности Unicode и « Таблица 3-8. Использование U + FFFD в конверсии UTF-8 » в разделе « Стандарт Unicode.

Стандарт 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; }