Regex检测无效的UTF-8字符串


Regex to detect invalid UTF-8 string

在PHP中,我们可以使用mb_check_encoding()来确定字符串是否是有效的UTF-8。但这不是一个可移植的解决方案,因为它需要编译并启用mbstring扩展。此外,它不会告诉我们哪个字符无效。

是否有正则表达式(或其他100%可移植的方法)可以匹配给定字符串中的无效UTF-8字节?

这样,如果需要,可以替换这些字节(保留二进制信息,例如在构建包含二进制数据的测试输出XML文件时)。因此,将字符转换为UTF-8会丢失信息。因此,我们可能需要转换:

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

进入

"foo<128><255>"

所以只是";检测";如果字符串不够好,我们需要能够检测出哪些字符是无效的。

您可以使用此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

假设PHP是用PCRE编译的,那么它通常也是用UTF-8启用的。因此,正如问题中明确要求的那样,这个非常简单的正则表达式可以检测无效的UTF-8字符串,因为它们不匹配:

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

然后,您可以争辩说u修饰符(PCRE_UTF8)并不总是可用的,这是真的,正如这个问题所示:

  • preg_match_all u标志依赖于什么

然而,在我的实际开发人员生活中,这从来都不是一个问题。更重要的是,PCRE扩展根本不可用,这将使任何包含PCRE的答案都变得无用(甚至我的答案也是如此)。但最常见的情况是,从今天起,这个问题更多的是过去的问题,减去了几年。

类似于这个问题的一个更冗长的答案已经在某种程度上重复的问题中给出:

  • 如何在PHP中检测格式错误的UTF-8字符串

因此,我认为这个问题应该突出建议的答案所带来的更多好处。

W3C有一个页面(标题为多语言形式编码),其中列出了以下Perl正则表达式,该表达式与有效的UTF-8字符串匹配。

(请注意,这与SO问题的另一个答案中列出的正则表达式相反,后者与无效UTF-8字符串匹配。)

#  Returns true if $field is UTF-8, and false otherwise.
$field =~
  m/'A(
     ['x00-'x7F]                        # 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;

这适用于检测Unicode字符、链接表情符号、俄语或中文:

private function has_unicode($string)
{
    $pattern = '/^.*[^'x{00}-'x{00FF}]+.*$/u';
    return preg_match($pattern, $string) ? true : false;
}