用正则表达式检测无效的UTF-8字符串

21
在PHP中,我们可以使用mb_check_encoding()来确定一个字符串是否为有效的UTF-8编码。但这并不是一个可移植的解决方案,因为它需要编译和启用mbstring扩展。此外,它也无法告诉我们哪个字符是无效的。
有没有一个正则表达式(或其他100%可移植的方法)可以匹配给定字符串中的无效UTF-8字节?
这样,如果需要的话(保留二进制信息,比如构建包含二进制数据的测试输出XML文件时),这些字节可以被替换。因此,将字符转换为UTF-8会丢失信息。所以,我们可能想要进行转换:
"foo" . chr(128) . chr(255)

进入

"foo<128><255>"

所以仅仅“检测”字符串是不够的,我们需要能够检测出哪些字符是无效的。
4个回答

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

@hakre:除非它依赖于编译时选项(PCRE_UTF8)。因此它不是可移植的... - ircmaxell
如果PCRE根本没有编译进来,你该怎么办? - hakre
@hakre 我认为在配置时无法禁用pcre? - Ja͢ck
@Jack:这是一个扩展,你可以编译没有 PCRE 扩展的 PHP。https://github.com/php/php-src/tree/PHP-5.4/ext/pcre 还有 --without-pcre-regex 开关。 - hakre
1
也许值得将建议更改为删除无效序列,而是用U+FFFD "\xEF\xBF\xBD"替换它们,请参见http://www.unicode.org/reports/tr36/#Ill-Formed_Subsequences - hakre

11

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

preg_match('//u', $string);
您可以提出一个论点,即u修饰符(PCRE_UTF8)并不总是可用,这可以发生,就像这个问题所展示的那样:What is the preg_match_all u flag dependent on?。然而,在我实际的开发者生活中,这从未成为问题。更大的问题是PCRE扩展根本不可用,这将使包含PCRE的任何答案都无用(即使是我在这里的答案)。但大多数时候,这个问题过去几年已经不再是问题了。
类似于这个答案的更长回答已在某些重复问题中给出:How can I detect a malformed UTF-8 string in PHP? 因此,我认为这个问题应该更加突出建议答案所带来的好处。

也许是 PHP Apache 模块,而且 Apache 没有编译 PCRE UTF-8 支持? - hakre

6

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;

2
这个正则表达式无法匹配有效的ASCII字符(控制字符)[\x09\x0A\x0D\x20-\x7E] 应该改为 [\x00-\x7F] - Brad Kent
1
@BradKent 实际上,W3C页面确实说 [\x00-\x7F],并且仅在段落末尾作为注释显示更受限制的集合。我已编辑答案。 - zgpmax

-1

这对我来说是有效的,可以检测Unicode字符,如表情符号、俄语或中文:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接