【问题标题】:UTF-8 validation in PHP without using preg_match()不使用 preg_match() 在 PHP 中进行 UTF-8 验证
【发布时间】:2010-11-19 23:00:36
【问题描述】:

我需要验证一些以 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() 会导致段错误。似乎没有任何解决方法。你可以在这里查看提交的bug: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;
}

是的,很长。我希望我已经正确理解了该正则表达式的工作原理。也希望对其他人有所帮助。

提前致谢!

【问题讨论】:

  • 为什么要检查这么多特殊值?它可以简单得多。
  • 我试图准确检查 W3C 的正则表达式正在检查的内容。
  • 如果字符串不是有效的 UTF-8,你打算怎么做?有乱码的数据总比没有数据好?

标签: php regex validation utf-8


【解决方案1】:

您可以随时使用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
}

【讨论】:

    【解决方案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 提供的正则表达式的两个主要区别。它只使用一次子模式,并且在第一个字符类之后有一个“+”量词。 PCRE 崩溃的问题依然存在,但大部分是由于使用重复捕获子模式造成的。通过将模式转换为仅一次模式并在单个子模式中捕获多个单字节字符,它应该可以防止 PCRE 快速耗尽堆栈(并导致段错误)。除非您要验证包含大量多字节字符(数千个)的字符串,否则此正则表达式应该可以很好地为您服务。

    如果您有可用的 mbstring 扩展名,另一个不错的选择是使用 mb_check_encoding()。验证 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 之前,由于未按预期工作,该功能完全无法使用。

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

    有时会看到使用 mb_detect_encoding() 来验证 UTF-8。如果您至少有 PHP 版本 5.4.0,它确实可以通过以下方式使用严格参数:

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

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

    验证 UTF-8 的一个好方法是在 PCRE 中使用 'u' 标志。尽管文档记录不佳,但它也验证了主题字符串。一个例子可能是:

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

    每个字符串都应该匹配一个空模式,但使用 'u' 标志只会匹配有效的 UTF-8 字符串。但是,除非您至少使用 5.5.10。验证存在如下缺陷:

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

    使用 'u' 标志行为确实有一个优势:它是所讨论的方法中最快的。如果您需要速度并且正在运行最新最好的 PHP 版本,那么这种验证方法可能适合您。

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

    function isValid($string)
    {
        return json_encode($string) !== false;
    }
    

    但是,我不建议依靠这种行为来持续。以前的 PHP 版本只是在无效序列上产生错误,因此不能保证当前行为是最终的。

    【讨论】:

      【解决方案3】:

      您应该能够使用iconv 来检查有效性。只需尝试将其转换为 UTF-16 并查看是否出现错误。

      【讨论】:

        【解决方案4】:

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

        【讨论】:

        • 我没有尝试 ereg,它可能会工作,但我真的不想使用它,因为:“此函数 (ereg) 自 PHP 5.3.0 起已弃用,自PHP 6.0.0。强烈建议不要依赖此功能。”
        • 好的,但是你很有可能在 6.0.0 中修复了 preg_match 错误。执行if (function_exists('ereg')) 并使用 preg_match 作为后备。
        • 但是,请使用其他建议之一。 Chacha102 的那个真的很好,因为你在你的例子中使用了 mb_substr,我猜你已经启用了 MB 字符串函数。不要忘记接受他(或任何其他人)的回答。
        【解决方案5】:

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

        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;
        }
        ?>
        

        【讨论】:

          猜你喜欢
          • 2010-12-16
          • 2011-04-08
          • 2016-09-06
          • 2019-03-08
          • 1970-01-01
          • 1970-01-01
          • 2018-10-11
          • 1970-01-01
          相关资源
          最近更新 更多