从字符串中删除非UTF-8字符

135

我在从字符串中删除非UTF-8字符方面遇到了问题,这些字符无法正确显示。这些字符的表示形式如0x97 0x61 0x6C 0x6F(十六进制表示)。

最好的方法是什么?使用正则表达式还是其他什么?


1
这里列出的解决方案对我没有用,所以我在“字符验证”部分找到了我的答案:http://webcollab.sourceforge.net/unicode.html - bobef
与此相关(https://dev59.com/cnM_5IYBdhLWcg3w43pt#20766625),但不一定是重复的,更像是近亲 :) - Wayne Weibel
$string = mb_convert_encoding($string, 'UTF-8', 'UTF-8');(对于非UTF8字符,会保留“?”符号) - Avatar
24个回答

7

欢迎来到2019年和正则表达式中的/u修饰符,它将为您处理UTF-8多字节字符

如果您只使用mb_convert_encoding($value, 'UTF-8', 'UTF-8'),您仍然会在字符串中遇到非可打印字符

此方法将:

  • 使用mb_convert_encoding删除所有无效的UTF-8多字节字符
  • 使用preg_replace删除所有不可打印的字符,如\r\x00(NULL-byte)和其他控制字符

方法:

function utf8_filter(string $value): string{
    return preg_replace('/[^[:print:]\n]/u', '', mb_convert_encoding($value, 'UTF-8', 'UTF-8'));
}

[:print:] 匹配所有可打印字符和换行符 \n 并剥离其他所有内容。

您可以在下面看到ASCII表。可打印字符的范围为32到127,但换行符\n是控制字符的一部分,控制字符的范围从0到31,因此我们必须将换行符添加到正则表达式/[^[:print:]\n]/u中。

https://cdn.shopify.com/s/files/1/1014/5789/files/Standard-ASCII-Table_large.jpg?10669400161723642407

您可以尝试发送字符串通过使用可打印范围以外的字符进行正则表达式匹配,例如\x7F(DEL),\x1B(Esc)等,并查看它们如何被剥离。

function utf8_filter(string $value): string{
    return preg_replace('/[^[:print:]\n]/u', '', mb_convert_encoding($value, 'UTF-8', 'UTF-8'));
}

$arr = [
    'Danish chars'          => 'Hello from Denmark with æøå',
    'Non-printable chars'   => "\x7FHello with invalid chars\r \x00"
];

foreach($arr as $k => $v){
    echo "$k:\n---------\n";
    
    $len = strlen($v);
    echo "$v\n(".$len.")\n";
    
    $strip = utf8_decode(utf8_filter(utf8_encode($v)));
    $strip_len = strlen($strip);
    echo $strip."\n(".$strip_len.")\n\n";
    
    echo "Chars removed: ".($len - $strip_len)."\n\n\n";
}

https://www.tehplayground.com/q5sJ3FOddhv1atpR


欢迎来到2047年,在这里php-mbstring不再默认打包在php中。 - NVRM
1
如果您的环境使用\r\n,为什么不使用\R呢? - mickmackusa
1
在调用此函数之前运行 utf8_encode 没有意义。如果您的字符串采用 ISO 8859-1 编码,则该函数将其转换为 UTF-8。如果它是其他任何编码,包括 UTF-8,它都将把它转换为乱码字符串,这将是有效的 UTF-8。因此,最终您将在乱码字符串上运行“删除非可打印字符”的正则表达式,并从另一端获得一堆无意义的结果。 - IMSoP
这仍然会用?替换ó或类似字符吗? - Gabriel I.

5
$string = preg_replace('~&([a-z]{1,2})(acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml);~i', '$1', htmlentities($string, ENT_COMPAT, 'UTF-8'));

5

substr()可能会破坏您的多字节字符!

在我的情况下,我使用substr($string,0,255)来确保用户提供的值可以适合数据库。有时它会将多字节字符拆分成两半,并导致数据库出现“不正确的字符串值”错误。

您可以使用mb_substr($string,0,255),对于MySQL 5而言这可能没问题,但是MySQL 4会计算字节而不是字符,因此根据多字节字符的数量仍然会太长。

为了避免这些问题,我实施了以下步骤:

  1. 我增加了字段的大小(在这种情况下,它是更改日志,因此无法防止更长的输入)。
  2. 仍然执行了mb_substring以防仍然太长。
  3. 我使用上面@Markus Jarderot提供的答案,以确保如果有一个非常长的条目,在长度限制处有一个多字节字符,我们可以去掉最后一半的多字节字符。

非常被低估。substr 把它搞砸了 :) - ALZlper

4

最近对Drupal的Feeds JSON解析模块进行了补丁升级:

//remove everything except valid letters (from any language)
$raw = preg_replace('/(?:\\\\u[\pL\p{Zs}])+/', '', $raw);

如果您担心的话,它会保留空格作为有效字符。我所需的功能已经实现了。它会移除现今流行的表情符号字符,这些字符不适合MySQL的“utf8”字符集,并且会导致错误,例如“SQLSTATE [HY000]: General error: 1366 Incorrect string value”。详细信息请参见https://www.drupal.org/node/1824506#comment-6881382

iconv 要比老式的基于正则表达式的 preg_replace 好得多,后者现在已经过时了。 - m3nda
4
preg_replace函数没有被弃用。 - Oleksii Chekulaiev
1
你是完全正确的,应该使用ereg_replace(),抱歉。 - m3nda
我不确定我同意这段代码片段。为什么在\u后面的字符类中匹配unicode空格字符?我很少使用表情符号,所以也许你知道比我更多。请设置一个工作演示来证明你的preg调用如何以及做了什么。 - mickmackusa
无法工作。查询参数包含%C4,这是坏的utf8,并且未从您的正则表达式中删除。 - user706420

4

规则是第一个UTF-8八位字节的高位被设置为标记,然后使用1到4个比特表示有多少个附加八位字节;然后每个附加的八位字节必须将高两位设置为10。

伪Python代码如下:

newstring = ''
cont = 0
for each ch in string:
  if cont:
    if (ch >> 6) != 2: # high 2 bits are 10
      # do whatever, e.g. skip it, or skip whole point, or?
    else:
      # acceptable continuation of multi-octlet char
      newstring += ch
    cont -= 1
  else:
    if (ch >> 7): # high bit set?
      c = (ch << 1) # strip the high bit marker
      while (c & 1): # while the high bit indicates another octlet
        c <<= 1
        cont += 1
        if cont > 4:
           # more than 4 octels not allowed; cope with error
      if !cont:
        # illegal, do something sensible
      newstring += ch # or whatever
if cont:
  # last utf-8 was not terminated, cope

这个逻辑同样适用于php。然而,一旦出现格式错误的字符,需要进行哪种类型的剥离处理并不清楚。


c = (ch << 1)会使得第一次(c & 1)为零,从而跳过循环。测试应该是(c & 128) - Markus Jarderot

1
下一个对我有用的清理工作如下:

$string = mb_convert_encoding($string, 'UTF-8', 'UTF-8');
$string = iconv("UTF-8", "UTF-8//IGNORE", $string);

0

我尝试了许多关于这个主题的解决方案,但是在我的特定情况下,它们都没有起作用。但是我在这个链接中找到了一个好的解决方案:

https://www.ryadel.com/en/php-skip-invalid-characters-utf-8-xml-file-string/

基本上,这个函数解决了我的问题:

function sanitizeXML($string)
{
    if (!empty($string)) 
    {
        // remove EOT+NOREP+EOX|EOT+<char> sequence (FatturaPA)
        $string = preg_replace('/(\x{0004}(?:\x{201A}|\x{FFFD})(?:\x{0003}|\x{0004}).)/u', '', $string);
 
        $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
            | (?<=[\x0-\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';
        $string = preg_replace($regex, '', $string);
 
        $result = "";
        $current;
        $length = strlen($string);
        for ($i=0; $i < $length; $i++)
        {
            $current = ord($string{$i});
            if (($current == 0x9) ||
                ($current == 0xA) ||
                ($current == 0xD) ||
                (($current >= 0x20) && ($current <= 0xD7FF)) ||
                (($current >= 0xE000) && ($current <= 0xFFFD)) ||
                (($current >= 0x10000) && ($current <= 0x10FFFF)))
            {
                $result .= chr($current);
            }
            else
            {
                $ret;    // use this to strip invalid character(s)
                // $ret .= " ";    // use this to replace them with spaces
            }
        }
        $string = $result;
    }
    return $string;
}

希望能对你们中的一些人有所帮助。

0

要删除Unicode基本语言平面之外的所有Unicode字符:

$str = preg_replace("/[^\\x00-\\xFFFF]/", "", $str);

你的解决方案有效,尽管无效字符不在上述范围之外! - a55
如果 $str 是从查询参数中传递的并且包含 %C4,则无法正常工作。 - user706420
无法正常工作,字符串在Laravel中仍被标记为“b”。 - Gabriel I.

0

与问题略有不同,但我正在做的是使用HtmlEncode(string),

伪代码如下:

var encoded = HtmlEncode(string);
encoded = Regex.Replace(encoded, "&#\d+?;", "");
var result = HtmlDecode(encoded);

输入和输出

"Headlight\x007E Bracket, &#123; Cafe Racer<> Style, Stainless Steel 中文呢?"
"Headlight~ Bracket, &#123; Cafe Racer<> Style, Stainless Steel 中文呢?"

我知道它不是完美的,但对我来说已经足够了。


0

对我来说,上面列出的所有UTF函数或替换方法都没有起作用。唯一有效的方法是明确允许我想要允许的字符。这可能是因为问题并不特别是UTF-8问题,尽管json_last_error_msg()告诉我是这样。

$text = preg_replace('/[^0-9a-zA-Z\.\-\,\/\ ]/m', '', $text);

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