Регулярное выражение для определения недопустимой строки UTF-8

В PHP мы можем использовать mb_check_encoding(), чтобы определить, действительно ли строка UTF-8. Но это не портативное решение, так как требуется, чтобы расширение mbstring было скомпилировано и включено. Кроме того, он не укажет нам, какой символ недопустим.

Есть ли регулярное выражение (или другой другой 100% -ный переносимый метод), который может соответствовать недопустимым байтам UTF-8 в данной строке. Таким образом, эти байты могут быть заменены при необходимости (сохранение двоичной информации, например, при создании тестового выходного XML файла, который включает двоичные данные). Поэтому преобразование символов в UTF-8 потеряло бы информацию. Итак, мы можем конвертировать:

"foo" . chr(128) . chr(255)

В

"foo<128><255>"

Итак, просто "обнаруживая", что строка недостаточно хороша, нам нужно будет определить, какие символы недопустимы.

Ответ 1

Вы можете использовать это регулярное выражение PCRE для проверки правильности UTF-8 в строке. Если регулярное выражение совпадает, строка содержит недопустимые последовательности байтов. Он на 100% переносим, поскольку не требует компиляции в PCRE_UTF8.

$regex = '/(
    [\xC0-\xC1] # Invalid UTF-8 Bytes
    | [\xF5-\xFF] # Invalid UTF-8 Bytes
    | \xE0[\x80-\x9F] # Overlong encoding of prior code point
    | \xF0[\x80-\x8F] # Overlong encoding of prior code point
    | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
    | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
    | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
    | (?<=[\x00-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
    | (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]|[\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
    | (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
    | (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
    | (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
)/x';

Мы можем проверить это, создав несколько вариантов текста:

// Overlong encoding of code point 0
$text = chr(0xC0) . chr(0x80);
var_dump(preg_match($regex, $text)); // int(1)
// Overlong encoding of 5 byte encoding
$text = chr(0xF8) . chr(0x80) . chr(0x80) . chr(0x80) . chr(0x80);
var_dump(preg_match($regex, $text)); // int(1)
// Overlong encoding of 6 byte encoding
$text = chr(0xFC) . chr(0x80) . chr(0x80) . chr(0x80) . chr(0x80) . chr(0x80);        
var_dump(preg_match($regex, $text)); // int(1)
// High code-point without trailing characters
$text = chr(0xD0) . chr(0x01);
var_dump(preg_match($regex, $text)); // int(1)

и т.д....

На самом деле, поскольку это соответствует недействительным байтам, вы можете использовать его в preg_replace для их замены:

preg_replace($regex, '', $text); // Remove all invalid UTF-8 code-points

Ответ 2

Я поставил это здесь для полноты:

Предполагая, что PHP скомпилирован с помощью PCRE, он чаще всего также включен с UTF-8. Так, как явно задано в вопросе, это очень простое регулярное выражение может обнаружить недопустимые строки UTF-8, потому что они не будут соответствовать:

preg_match('//u', $string);

Затем вы можете аргументировать, что модификатор u (PCRE_UTF8) не всегда доступен, и true, это может произойти, как показывает этот вопрос:

Однако в моем практическом разработчике это никогда не было проблемой. Более того, расширение PCRE вообще недоступно, что сделает любой ответ, содержащий pcre, бесполезным (даже моим здесь). Но чаще всего этот вопрос был скорее вопросом прошлого на сегодняшний день минус несколько лет.

Более длинный ответ, подобный этому, был задан в некотором дублирующем вопросе:

Итак, я думаю, что этот вопрос должен подчеркнуть больше преимуществ, предлагаемых предлагаемым кораблем для ответа.

Ответ 3

В W3C есть страница (называемая многоязычной кодировкой), в которой указано следующее регулярное выражение Perl, которое соответствует действительной UTF-8 строка.

(Обратите внимание, что это противоположно регулярному выражению, указанному в другом ответе на этот вопрос SO, который соответствует недопустимой строке UTF-8.)

#  Returns true if $field is UTF-8, and false otherwise.

$field =~
  m/\A(
     [\x09\x0A\x0D\x20-\x7E]            # ASCII
   | [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
   |  \xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
   | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}  # straight 3-byte
   |  \xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
   |  \xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
   | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
   |  \xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
  )*\z/x;