代码之家  ›  专栏  ›  技术社区  ›  philfreo

如何处理用户输入的无效UTF-8字符?

  •  37
  • philfreo  · 技术社区  · 14 年前

    我正在寻找一个关于如何处理来自用户的无效UTF-8输入的一般策略/建议。

    尽管我的webapp使用UTF-8,但有些用户还是输入了无效字符。这会导致PHP的 json_encode() 总的来说,这是个坏主意。

    W3C I18N FAQ: Multilingual Forms 表示“如果接收到非UTF-8数据,则应发回错误消息。”。

    • 在一个有几十个不同地方可以输入数据的站点中,究竟应该如何实际地做到这一点?
    • 如何以一种有用的方式向用户呈现错误?
    • 如何临时存储和显示坏表单数据,以便用户不会丢失所有文本?脱掉坏角色?使用替换字符,以及如何使用?
    • 对于数据库中的现有数据,当检测到无效的UTF-8数据时,我是否应该尝试将其转换并保存回去(如何? utf8_encode mb_convert_encoding()

    编辑:我非常熟悉mbstring扩展,并不是问“UTF-8在PHP中如何工作”。我想从那些在现实生活中有经验的人那里得到一些建议。

    EDIT2:作为解决方案的一部分,我真的很想看到 快速的 方法将无效字符转换为U+FFFD

    9 回复  |  直到 12 年前
        1
  •  62
  •   Alix Axel    13 年前

    这个 accept-charset="UTF-8" 属性只是一个浏览器遵循的准则,他们没有被迫提交,以这种方式,垃圾表单提交机器人是一个很好的例子。。。

    我通常做的就是忽略坏角色,或者 iconv() 或者用不太可靠的 utf8_encode() utf8_decode() 函数,如果使用 iconv 你也可以选择音译坏字符。

    下面是一个使用 :

    $str_ignore = iconv('UTF-8', 'UTF-8//IGNORE', $str);
    $str_translit = iconv('UTF-8', 'UTF-8//TRANSLIT', $str);
    

    function utf8_clean($str)
    {
        return iconv('UTF-8', 'UTF-8//IGNORE', $str);
    }
    
    $clean_GET = array_map('utf8_clean', $_GET);
    
    if (serialize($_GET) != serialize($clean_GET))
    {
        $_GET = $clean_GET;
        $error_msg = 'Your data is not valid UTF-8 and has been stripped.';
    }
    
    // $_GET is clean!
    

    您可能还希望规范化新行并剥离(不可见的)控件字符,如下所示:

    function Clean($string, $control = true)
    {
        $string = iconv('UTF-8', 'UTF-8//IGNORE', $string);
    
        if ($control === true)
        {
                return preg_replace('~\p{C}+~u', '', $string);
        }
    
        return preg_replace(array('~\r\n?~', '~[^\P{C}\t\n]+~u'), array("\n", ''), $string);
    }
    

    要从UTF-8转换为Unicode码点的代码:

    function Codepoint($char)
    {
        $result = null;
        $codepoint = unpack('N', iconv('UTF-8', 'UCS-4BE', $char));
    
        if (is_array($codepoint) && array_key_exists(1, $codepoint))
        {
            $result = sprintf('U+%04X', $codepoint[1]);
        }
    
        return $result;
    }
    
    echo Codepoint('à'); // U+00E0
    echo Codepoint('ひ'); // U+3072
    

    可能 比其他任何选择都要快,但还没有进行广泛的测试。


    例子:

    $string = 'hello world�';
    
    // U+FFFEhello worldU+FFFD
    echo preg_replace_callback('/[\p{So}\p{Cf}\p{Co}\p{Cs}\p{Cn}]/u', 'Bad_Codepoint', $string);
    
    function Bad_Codepoint($string)
    {
        $result = array();
    
        foreach ((array) $string as $char)
        {
            $codepoint = unpack('N', iconv('UTF-8', 'UCS-4BE', $char));
    
            if (is_array($codepoint) && array_key_exists(1, $codepoint))
            {
                $result[] = sprintf('U+%04X', $codepoint[1]);
            }
        }
    
        return implode('', $result);
    }
    

        2
  •  4
  •   Arc    14 年前

    从web应用程序接收无效字符可能与HTML表单假定的字符集有关。可以指定要用于具有 accept-charset attribute

    <form action="..." accept-charset="UTF-8">
    

    您可能还想看看StackOverflow中关于指针如何处理无效字符的类似问题,例如右边列中的字符,但我认为,向用户发出错误信号要比试图清除那些导致重要数据意外丢失或用户输入意外更改的无效字符要好。

        3
  •  2
  •   Nev Stokes    14 年前

    我建立了一个相当简单的类来检查输入是否在UTF-8中,并通过 utf8_encode() 根据需要:

    class utf8
    {
    
        /**
         * @param array $data
         * @param int $options
         * @return array
         */
        public static function encode(array $data)
        {
            foreach ($data as $key=>$val) {
                if (is_array($val)) {
                    $data[$key] = self::encode($val, $options);
                } else {
                    if (false === self::check($val)) {
                        $data[$key] = utf8_encode($val);
                    }
                }
            }
    
            return $data;
        }
    
        /**
         * Regular expression to test a string is UTF8 encoded
         * 
         * RFC3629
         * 
         * @param string $string The string to be tested
         * @return bool
         * 
         * @link http://www.w3.org/International/questions/qa-forms-utf-8.en.php
         */
        public static function check($string)
        {
            return preg_match('%^(?:
                [\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
                )*$%xs',
                $string);
        }
    }
    
    // For example
    $data = utf8::encode($_POST);
    
        4
  •  1
  •   Otar    14 年前
        5
  •  1
  •   philfreo    14 年前

    为了这个问题的完整性(不一定是最好的答案)。。。

    function as_utf8($s) {
        return mb_convert_encoding($s, "UTF-8", mb_detect_encoding($s));
    }
    
        6
  •  1
  •   Geekster    14 年前

    我只建议不要让垃圾进入。不要依赖自定义函数,这会使系统陷入困境。只需对照您设计的字母表遍历提交的数据。创建一个可接受的字母字符串,并逐个字节遍历提交的数据,就像它是一个数组一样。将可接受的字符推送到新字符串中,并忽略不可接受的字符。然后,存储在数据库中的数据是由用户触发的数据,而不是实际由用户提供的数据。

    用entiy替换坏字符:

    编辑#3: 更新时间:2010年9月22日下午1:32 原因:现在返回的字符串是UTF-8,另外我使用了您提供的测试文件作为证明。

    <?php
    // build alphabet
    // optionally you can remove characters from this array
    
    $alpha[]= chr(0); // null
    $alpha[]= chr(9); // tab
    $alpha[]= chr(10); // new line
    $alpha[]= chr(11); // tab
    $alpha[]= chr(13); // carriage return
    
    for ($i = 32; $i <= 126; $i++) {
    $alpha[]= chr($i);
    }
    
    /* remove comment to check ascii ordinals */
    
    // /*
    // foreach ($alpha as $key=>$val){
    //  print ord($val);
    //  print '<br/>';
    // }
    // print '<hr/>';
    //*/
    // 
    // //test case #1
    // 
    // $str = 'afsjdfhasjhdgljhasdlfy42we875y342q8957y2wkjrgSAHKDJgfcv kzXnxbnSXbcv   '.chr(160).chr(127).chr(126);
    // 
    // $string = teststr($alpha,$str);
    // print $string;
    // print '<hr/>';
    // 
    // //test case #2
    // 
    // $str = ''.'©?™???';
    // $string = teststr($alpha,$str);
    // print $string;
    // print '<hr/>';
    // 
    // $str = '©';
    // $string = teststr($alpha,$str);
    // print $string;
    // print '<hr/>';
    
    $file = 'http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt';
    $testfile = implode(chr(10),file($file));
    
    $string = teststr($alpha,$testfile);
    print $string;
    print '<hr/>';
    
    
    function teststr(&$alpha, &$str){
        $strlen = strlen($str);
        $newstr = chr(0); //null
        $x = 0;
        if($strlen >= 2){
    
            for ($i = 0; $i < $strlen; $i++) {
                $x++;
                if(in_array($str[$i],$alpha)){
                    // passed
                    $newstr .= $str[$i];
                }else{
                    // failed
                    print 'Found out of scope character. (ASCII: '.ord($str[$i]).')';
                    print '<br/>';
                    $newstr .= '&#65533;';
                }
            }
        }elseif($strlen <= 0){
            // failed to qualify for test
            print 'Non-existent.';
    
        }elseif($strlen === 1){
            $x++;
            if(in_array($str,$alpha)){
                // passed
    
                $newstr = $str;
            }else{
                // failed
                print 'Total character failed to qualify.';
                $newstr = '&#65533;';
            }
        }else{
            print 'Non-existent (scope).';
            }
    
    if(mb_detect_encoding($newstr, "UTF-8") == "UTF-8"){
    // skip
    }else{
        $newstr = utf8_encode($newstr);
    }
    
    
    // test encoding:
    if(mb_detect_encoding($newstr, "UTF-8")=="UTF-8"){
        print 'UTF-8 :D<br/>';
        }else{
            print 'ENCODED: '.mb_detect_encoding($newstr, "UTF-8").'<br/>';
            }
    
    
    
    
    return $newstr.' (scope: '.$x.', '.$strlen.')';
    }
    
        7
  •  0
  •   Elzo Valugi    14 年前

    去掉给定子集之外的所有字符怎么样。至少在我的应用程序的某些部分,我不允许在[a-Z][0-9集]之外使用字符,例如用户名。您可以构建一个filter函数,将所有超出此范围的字符自动剥离,或者在检测到这些字符并将决策推送给用户时返回错误。

        8
  •  0
  •   yfeldblum    14 年前

    尝试像Rails那样强制所有浏览器始终发布UTF-8数据:

    <form accept-charset="UTF-8" action="#{action}" method="post"><div
        style="margin:0;padding:0;display:inline">
        <input name="utf8" type="hidden" value="&#x2713;" />
      </div>
      <!-- form fields -->
    </form>
    

    看到了吗 railssnowman.info the initial patch 为了解释。

    1. 要让浏览器以UTF-8编码发送表单提交数据,只需使用“text/html;charset=UTF-8”的内容类型标题呈现页面(或使用 meta http-equiv 标签)。
    2. accept-charset="UTF-8" 在表格中。
    3. 要让浏览器以UTF-8编码发送表单提交数据,即使用户摆弄页面编码(浏览器允许用户这样做),即使浏览器是IE并且用户将页面编码切换为朝鲜语并在表单字段中输入朝鲜语字符,也要向表单添加一个隐藏的输入值,例如 &#x2713; 它只能来自Unicode字符集(在本例中,不是韩语字符集)。
        9
  •  0
  •   Bhavin Patel Rafee    6 年前

    将UTF-8设置为PHP代码输出的所有头的字符集

    header('Content-Type: text/html; charset=utf-8');