PHP中的UTF-8验证,不使用preg_match()

时间:2009-08-15 22:06:56

标签: php regex validation utf-8

我需要验证以UTF-8编码的一些用户输入。许多人建议使用以下代码:

preg_match('/\A(
     [\x09\x0A\x0D\x20-\x7E]
   | [\xC2-\xDF][\x80-\xBF]
   |  \xE0[\xA0-\xBF][\x80-\xBF]
   | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
   |  \xED[\x80-\x9F][\x80-\xBF]
   |  \xF0[\x90-\xBF][\x80-\xBF]{2}
   | [\xF1-\xF3][\x80-\xBF]{3}
   |  \xF4[\x80-\x8F][\x80-\xBF]{2}
  )*\z/x', $string);

这是来自http://www.w3.org/International/questions/qa-forms-utf-8的正则表达式。一切都很好,直到我发现PHP中的错误似乎至少自2006年以来一直存在。如果$ string太长,Preg_match()会导致seg错误。似乎没有任何解决方法。您可以在此处查看错误提交:http://bugs.php.net/bug.php?id=36463

现在,为了避免使用preg_match,我创建了一个与上面的正则表达式完全相同的函数。我不知道这个问题在Stack Overflow中是否合适,但我想知道我所做的功能是否正确。这是:

编辑[13.01.2010]: 如果有人有兴趣,我发布的先前版本中有几个错误。以下是我的函数的最终版本。

function check_UTF8_string(&$string) {
    $len = mb_strlen($string, "ISO-8859-1");
    $ok = 1;

    for ($i = 0; $i < $len; $i++) {
        $o = ord(mb_substr($string, $i, 1, "ISO-8859-1"));

        if ($o == 9 || $o == 10 || $o == 13 || ($o >= 32 && $o <= 126)) {

        }
        elseif ($o >= 194 && $o <= 223) {
            $i++;
            $o2 = ord(mb_substr($string, $i, 1, "ISO-8859-1"));
            if (!($o2 >= 128 && $o2 <= 191)) {
                $ok = 0;
                break;
            }
        }
        elseif ($o == 224) {
            $o2 = ord(mb_substr($string, $i + 1, 1, "ISO-8859-1"));
            $o3 = ord(mb_substr($string, $i + 2, 1, "ISO-8859-1"));
            $i += 2;
            if (!($o2 >= 160 && $o2 <= 191) || !($o3 >= 128 && $o3 <= 191)) {
                $ok = 0;
                break;
            }
        }
        elseif (($o >= 225 && $o <= 236) || $o == 238 || $o == 239) {
            $o2 = ord(mb_substr($string, $i + 1, 1, "ISO-8859-1"));
            $o3 = ord(mb_substr($string, $i + 2, 1, "ISO-8859-1"));
            $i += 2;
            if (!($o2 >= 128 && $o2 <= 191) || !($o3 >= 128 && $o3 <= 191)) {
                $ok = 0;
                break;
            }
        }
        elseif ($o == 237) {
            $o2 = ord(mb_substr($string, $i + 1, 1, "ISO-8859-1"));
            $o3 = ord(mb_substr($string, $i + 2, 1, "ISO-8859-1"));
            $i += 2;
            if (!($o2 >= 128 && $o2 <= 159) || !($o3 >= 128 && $o3 <= 191)) {
                $ok = 0;
                break;
            }
        }
        elseif ($o == 240) {
            $o2 = ord(mb_substr($string, $i + 1, 1, "ISO-8859-1"));
            $o3 = ord(mb_substr($string, $i + 2, 1, "ISO-8859-1"));
            $o4 = ord(mb_substr($string, $i + 3, 1, "ISO-8859-1"));
            $i += 3;
            if (!($o2 >= 144 && $o2 <= 191) ||
                !($o3 >= 128 && $o3 <= 191) ||
                !($o4 >= 128 && $o4 <= 191)) {
                $ok = 0;
                break;
            }
        }
        elseif ($o >= 241 && $o <= 243) {
            $o2 = ord(mb_substr($string, $i + 1, 1, "ISO-8859-1"));
            $o3 = ord(mb_substr($string, $i + 2, 1, "ISO-8859-1"));
            $o4 = ord(mb_substr($string, $i + 3, 1, "ISO-8859-1"));
            $i += 3;
            if (!($o2 >= 128 && $o2 <= 191) ||
                !($o3 >= 128 && $o3 <= 191) ||
                !($o4 >= 128 && $o4 <= 191)) {
                $ok = 0;
                break;
            }
        }
        elseif ($o == 244) {
            $o2 = ord(mb_substr($string, $i + 1, 1, "ISO-8859-1"));
            $o3 = ord(mb_substr($string, $i + 2, 1, "ISO-8859-1"));
            $o4 = ord(mb_substr($string, $i + 3, 1, "ISO-8859-1"));
            $i += 5;
            if (!($o2 >= 128 && $o2 <= 143) ||
                !($o3 >= 128 && $o3 <= 191) ||
                !($o4 >= 128 && $o4 <= 191)) {
                $ok = 0;
                break;
            }
        }
        else {
            $ok = 0;
            break;
        }
    }

    return $ok;
}

是的,这很长。我希望我已经正确理解了正则表达式是如何工作的。也希望它对别人有所帮助。

提前致谢!

5 个答案:

答案 0 :(得分:8)

您始终可以使用Multibyte String Functions

如果你想要经常使用它,可能会在某个时候改变它:

1)首先在配置文件中设置要使用的编码

/* Set internal character encoding to UTF-8 */
mb_internal_encoding("UTF-8");

2)检查字符串

if(mb_check_encoding($string))
{
    // do something
}

或者,如果您不打算更改它,您可以随时将编码直接放入函数中:

if(mb_check_encoding($string, 'UTF-8'))
{
    // do something
}

答案 1 :(得分:2)

鉴于PHP中仍然没有明确的isUtf8()函数,以下是根据您的PHP版本如何在PHP中准确验证UTF-8。

正确验证UTF-8的最简单且最向后兼容的方法仍然是使用以下函数的正则表达式:

function isValid($string)
{
    return preg_match(
        '/\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',
        $string
    ) === 1;
}

请注意W3C提供的正则表达式的两个主要差异。它只使用一次子模式,并且有一个&#39; +&#39;第一个字符类后的量词。 PCRE崩溃的问题仍然存在,但大部分是由使用重复捕获子模式引起的。通过将模式转换为仅一次模式并在单个子模式中捕获多个单字节字符,它应该防止PCRE快速耗尽堆栈(并导致段错误)。除非您正在验证具有大量多字节字符的字符串(在数千个范围内),否则这个正则表达式应该对您有用。

另一个好的选择是使用mb_check_encoding(),如果你有可用的mbstring扩展名。验证UTF-8可以简单地完成:

function isValid($string)
{
    return mb_check_encoding($string, 'UTF-8') === true;
}

但请注意,如果您使用 5.4.0 之前的PHP版本,则此功能在其验证中存在一些缺陷:

  • 5.4.0 之前,该函数接受超出允许的Unicode范围的代码点。这意味着它还允许使用5和6个字节的UTF-8字符。
  • 5.3.0 之前,该函数接受代理代码点作为有效的UTF-8字符。
  • 5.2.5 之前,由于无法正常工作,该功能完全无法使用。

由于互联网还列出了许多其他验证UTF-8的方法,我将在这里讨论其中的一些方法。请注意,在大多数情况下,应避免以下情况

有时可以使用mb_detect_encoding()来验证UTF-8。如果您至少拥有PHP版本 5.4.0 ,它实际上可以使用strict参数:

function isValid($string)
{
    return mb_detect_encoding($string, 'UTF-8', true) === 'UTF-8';
}

了解这在 5.4.0 之前不起作用非常重要。它在该版本之前是非常有缺陷的,因为它只检查无效序列但允许超长序列和无效代码点。此外,如果没有将strict参数设置为true,则不应将其用于此目的(如果没有strict参数,它实际上不会进行验证)。

验证UTF-8的一种有效方法是使用&#39; u&#39; PCRE中的旗帜。虽然记录不完整,但它也验证了主题字符串。一个例子可能是:

function isValid($string)
{
    return preg_match('//u', $string) === 1;
}

每个字符串都应该匹配一个空模式,但使用&#39; u&#39; flag仅匹配有效的UTF-8字符串。但是,除非您至少使用 5.5.10 。验证存在以下缺陷:

  • 5.5.10 之前,它不会将3和4字节序列识别为有效的UTF-8。由于它排除了大多数unicode代码点,这是一个非常重要的缺陷。
  • 5.2.5 之前,它还允许代理和代码点超出允许的unicode空间(例如5和6字节字符)

使用&#39; u&#39;旗帜行为确实有一个优点:它是所讨论方法中最快的。如果您需要速度并且您正在运行最新且最好的PHP版本,则此验证方法可能适合您。

验证UTF-8的另一种方法是通过json_encode(),它要求输入字符串为UTF-8。它在 5.5.0 之前不起作用,但在此之后,无效序列返回false而不是字符串。例如:

function isValid($string)
{
    return json_encode($string) !== false;
}
但是,我不建议继续依赖此行为。以前的PHP版本只会在无效序列上产生错误,因此无法保证当前行为是最终的。

答案 2 :(得分:1)

您是否尝试过ereg()而不是preg_match?也许这个没有那个bug,而且你不需要一个潜在的错误解决方法。

答案 3 :(得分:1)

您应该可以使用iconv来检查有效性。只需尝试将其转换为UTF-16,看看是否收到错误。

答案 4 :(得分:0)

这是一个基于字符串函数的解决方案:

http://www.php.net/manual/en/function.mb-detect-encoding.php#85294

<?php
function is_utf8($str) {
    $c=0; $b=0;
    $bits=0;
    $len=strlen($str);
    for($i=0; $i<$len; $i++){
        $c=ord($str[$i]);
        if($c > 128){
            if(($c >= 254)) return false;
            elseif($c >= 252) $bits=6;
            elseif($c >= 248) $bits=5;
            elseif($c >= 240) $bits=4;
            elseif($c >= 224) $bits=3;
            elseif($c >= 192) $bits=2;
            else return false;
            if(($i+$bits) > $len) return false;
            while($bits > 1){
                $i++;
                $b=ord($str[$i]);
                if($b < 128 || $b > 191) return false;
                $bits--;
            }
        }
    }
    return true;
}
?>