Вы можете использовать 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)
Вы можете обратиться к следующим ресурсам для проверки диапазона байтов.
- " Синтаксис байтовых последовательностей UTF-8 " в RFC 3629
- " Таблица 3-7. Хорошо сформированные последовательности байтов UTF-8 " в стандарте Unicode 6.1
- " Многоязычная кодировка форм " в W3C Internationalization "
Ниже приведена таблица диапазонов байтов.
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 показывает пример:
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 содержит ошибку, которая нарушает допустимый символ s сразу после неверной последовательности байтов или удаляет недопустимую последовательность байтов после допустимых символов без добавления 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;
}