如何在PHP中替换/删除UTF-8字符串中的4(+)字节字符?

43

看起来MySQL在其默认的UTF-8字符集中不支持超过3个字节的字符。

那么,在PHP中,我该如何摆脱字符串中所有4(及更多)字节的字符,并用其他字符替换它们?


1
你确定你要操作的数据里面不会包含不能适配mysql的3字节utf8字符吗? - newtover
1
你确定有相似的字符吗?3个字节可以覆盖整个基本多语言平面;如果需要超出此范围的稀有字符,请考虑使用另一个Unicode编码(例如UTF-16)。 - Piskvor left the building
1
问题在于我想避免这些特殊字符,因为如果有人在其中输入了这些特殊字符,MySQL会在那一点截断文本。 - Franz
1
@Franz:非常抱歉,但这就像是说“好吧,ß和ž和ḉ无法适应ASCII,所以让我们将它们变成ss和z和c;意义和语法的正确性?我不在乎。” - Piskvor left the building
3
MySQL现在支持这些字符,通过utf8mb4字符集。 - BenMorel
显示剩余3条评论
7个回答

59

15

由于4字节的UTF-8序列始终以字节0xF0-0xF7开头,因此以下方法应该有效:

$str = preg_replace('/[\xF0-\xF7].../s', '', $str);

另外,您可以在UTF-8模式下使用preg_replace,但这可能会更慢:

$str = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $str);

这是因为在Unicode补充平面中,码点需要使用4字节UTF-8序列,起始值为0x10000


在第一个例子中,为什么我们需要s修饰符? - stardust
@stardust s 修饰符 (PCRE_DOTALL) 使得 . 还能匹配换行符。 - nwellnhof

4

这是一个例子:

<?php 

 mb_internal_encoding("UTF-8");

 //utf8 string,  13 bytes, 9 utf8 chars, 7 ASCII, 1 in latin1, 1 outside the BMP
 $str = "qué \xF0\x9D\x92\xB3 tal"; 
 $array = mbStringToArray($str);
 print "str: [$str]  strlen:" . strlen($str) . " chars:" . count($array) . "\n";
 $str1 = "";
 foreach($array as $c) {
   //  print "$c : " .  strlen($c)  ."\n";
   $str1 .= strlen($c)<=3? $c : '?';
 }
 print "[$str1]\n";


 function mbStringToArray ($str) {
    if (empty($str)) return false;
    $len = mb_strlen($str);
    $array = array();
    for ($i = 0; $i < $len; $i++) {
        $array[] = mb_substr($str, $i, 1);
    }
    return $array;
 }

或者更加紧凑和高效:

<?php /// 

 mb_internal_encoding("UTF-8");

 //utf8 string,  13 bytes, 9 utf8 chars, 7 ASCII, 1 in latin1, 1 outside the BMP
 $str = "qué \xF0\x9D\x92\xB3 tal";
 $str1 = trimOutsideBMP($str);
 print "original: [$str]\n";
 print "trimmed:  [$str1]\n";


 // Replaces non-BMP characters in the UTF-8 string by a '?' character 
 // Assumes UTF-8 default encoding ( if not sure, call first mb_internal_encoding("UTF-8"); )
 function trimOutsideBMP($str) {
    if (empty($str)) return $str;
    $len = mb_strlen($str);
    $str1 = '';
    for ($i = 0; $i < $len; $i++) {
        $c = mb_substr($str, $i, 1);
        $str1 .= strlen($c) <= 3 ? $c : '?';
    }
    return $str1;
 }

哦,我应该提到我需要一个不需要 mbstring 扩展的解决方案吗? - Franz
嗯,那很丑。在这里查看灵感:http://noteslog.com/post/full-utf-8-support-in-wordpress/ - leonbloy

1

这是我用来过滤掉4字节字符的实现

$string = preg_replace_callback(
    '/./u',
    function (array $match) {
        return strlen($match[0]) >= 4 ? null : $match[0];
    },
    $string
);

你可以调整它并将null(用于删除字符)替换为某个替代字符串。你还可以将>= 4替换为其他字节长度检查。

1

另一种更复杂的过滤器实现。

它尝试将字符转换为ASCII字符,否则插入Unicode替换字符以避免XSS攻击,例如:<a href='java\uFEFFscript:alert("XSS")'>

$tr = preg_replace_callback('/([\x{10000}-\x{10FFFF}])/u', function($m){
    $c = iconv('ISO-8859-2', 'UTF-8',iconv('utf-8','ISO-8859-2//TRANSLIT//IGNORE', $m[1]));
    if($c == '')
        return '�';
    return $c;

}, $s);

1

下面的函数将utf8字符串中的3个字节和4个字节字符更改为“#”:

function remove3and4bytesCharFromUtf8Str($str) {
        return preg_replace('/([\xF0-\xF7]...)|([\xE0-\xEF]..)/s', '#', $str);
    }

1
我在解决自己的问题时遇到了这个问题(Facebook将某些表情符号输出为4字节字符,而Amazon Mechanical Turk不接受4字节字符)。
最终我使用了以下方法,无需mbstring扩展:
function remove_4_byte($string) {
    $char_array = preg_split('/(?<!^)(?!$)/u', $string );
    for($x=0;$x<sizeof($char_array);$x++) {
        if(strlen($char_array[$x])>3) {
            $char_array[$x] = "";
        }
    }
    return implode($char_array, "");
}

出于某种原因,我无法让其他的代码工作,但这个可以解决问题。 - Mahn

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