从字符串中删除非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个回答

149
如果您对已经是UTF8的字符串应用utf8_encode(),它将返回一段乱码的UTF8输出。
我创建了一个函数来解决所有这些问题。它叫做Encoding::toUTF8()
您不需要知道字符串的编码方式。它可以是Latin1(ISO8859-1)、Windows-1252或UTF8,或者该字符串可能以这些编码混合。 Encoding::toUTF8() 将把一切都转换为UTF8。
我这么做是因为某个服务向我提供的数据源非常混乱,将这些编码混合在同一个字符串中。
用法:
require_once('Encoding.php'); 
use \ForceUTF8\Encoding;  // It's namespaced now.

$utf8_string = Encoding::toUTF8($mixed_string);

$latin1_string = Encoding::toLatin1($mixed_string);

我已经包含了另一个函数,Encoding::fixUTF8(),它将修复每个看起来像是被多次编码成UTF8后出现乱码的字符串。

用法:

require_once('Encoding.php'); 
use \ForceUTF8\Encoding;  // It's namespaced now.

$utf8_string = Encoding::fixUTF8($garbled_utf8_string);

示例:

echo Encoding::fixUTF8("Fédération Camerounaise de Football");
echo Encoding::fixUTF8("Fédération Camerounaise de Football");
echo Encoding::fixUTF8("FÃÂédÃÂération Camerounaise de Football");
echo Encoding::fixUTF8("Fédération Camerounaise de Football");

将输出:

Fédération Camerounaise de Football
Fédération Camerounaise de Football
Fédération Camerounaise de Football
Fédération Camerounaise de Football

下载:

https://github.com/neitanod/forceutf8


14
棒极了!所有其他解决方案都会丢弃无效字符,但这个修复了它。太棒了。 - giorgio79
Markus Jarderot 的回答的第二部分也可以实现这个功能。 - Sebastián Grignoli
@AlfonsoFernandez-Ocampo,那么这不是你需要的函数。只有在想要对其应用“修复”时才使用它。 - Pacerier
@SebastiánGrignoli,是否有测试用例验证此函数的安全性? - Pacerier
@Pacerier,请在Github上联系我,讨论潜在的漏洞,以便我可以添加这些测试。 - Sebastián Grignoli
显示剩余3条评论

101

使用正则表达式方法:

$regex = <<<'END'
/
  (
    (?: [\x00-\x7F]                 # single-byte sequences   0xxxxxxx
    |   [\xC0-\xDF][\x80-\xBF]      # double-byte sequences   110xxxxx 10xxxxxx
    |   [\xE0-\xEF][\x80-\xBF]{2}   # triple-byte sequences   1110xxxx 10xxxxxx * 2
    |   [\xF0-\xF7][\x80-\xBF]{3}   # quadruple-byte sequence 11110xxx 10xxxxxx * 3 
    ){1,100}                        # ...one or more times
  )
| .                                 # anything else
/x
END;
preg_replace($regex, '$1', $text);
它搜索UTF-8序列,并将其捕获到第1组中。它还匹配无法识别为UTF-8序列的单个字节,但不会捕获这些字节。替换为捕获到第1组中的内容。这有效地删除了所有无效字节。
可以通过将无效字节编码为UTF-8字符来修复字符串。但如果错误是随机的,这可能会留下一些奇怪的符号。
$regex = <<<'END'
/
  (
    (?: [\x00-\x7F]               # single-byte sequences   0xxxxxxx
    |   [\xC0-\xDF][\x80-\xBF]    # double-byte sequences   110xxxxx 10xxxxxx
    |   [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences   1110xxxx 10xxxxxx * 2
    |   [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3 
    ){1,100}                      # ...one or more times
  )
| ( [\x80-\xBF] )                 # invalid byte in range 10000000 - 10111111
| ( [\xC0-\xFF] )                 # invalid byte in range 11000000 - 11111111
/x
END;
function utf8replacer($captures) {
  if ($captures[1] != "") {
    // Valid byte sequence. Return unmodified.
    return $captures[1];
  }
  elseif ($captures[2] != "") {
    // Invalid byte of the form 10xxxxxx.
    // Encode as 11000010 10xxxxxx.
    return "\xC2".$captures[2];
  }
  else {
    // Invalid byte of the form 11xxxxxx.
    // Encode as 11000011 10xxxxxx.
    return "\xC3".chr(ord($captures[3])-64);
  }
}
preg_replace_callback($regex, "utf8replacer", $text);

编辑:

  • !empty(x)将匹配非空值("0"被视为空)。
  • x != ""将匹配非空值,包括"0"
  • x !== ""将匹配除""以外的任何内容。

在这种情况下,x != ""似乎最好使用。

我还加快了匹配速度。它不再逐个字符匹配,而是匹配有效UTF-8字符序列。


92

1
@Alliswell 哪些?你能提供一个例子吗? - Maxime Pacary
当然,<0x1a> - Alliswell
2
如果我没记错的话,<0x1a>虽然不是可打印字符,但它是一个完全有效的UTF-8序列。你可能会遇到非可打印字符的问题?查看这个链接:https://dev59.com/cnM_5IYBdhLWcg3w43pt - Maxime Pacary
4
在调用 mb_convert 前,我必须将 mbstring 替换字符设置为 none ini_set('mbstring.substitute_character', 'none'); ,否则结果中会出现问号。 - cby016
@MaximePacary 如果是 "\xFF",那就是无效的 UTF-8。 - hanshenrik
@hanshenrik 然后相应的无效UTF-8序列应该被mb_convert_encoding()移除,就像OP所要求的那样(我不确定完全理解你的意思?) - Maxime Pacary

30

这个函数可以删除所有非ASCII字符,它很有用但不能解决问题:
这是我的函数,无论编码如何都可以正常工作:

function remove_bs($Str) {  
  $StrArr = str_split($Str); $NewStr = '';
  foreach ($StrArr as $Char) {    
    $CharNo = ord($Char);
    if ($CharNo == 163) { $NewStr .= $Char; continue; } // keep £ 
    if ($CharNo > 31 && $CharNo < 127) {
      $NewStr .= $Char;    
    }
  }  
  return $NewStr;
}

它是如何工作的:

echo remove_bs('Hello õhowå åare youÆ?'); // Hello how are you?

5
这是ASCII码,与问题所需的内容相去甚远。 - misaxi
1
这个解决了。当Google Maps API报告API请求URL中存在“非UTF-8字符”时,我遇到了问题。罪魁祸首是地址字段中的í字符,它是一个有效的UTF-8字符参见表格。教训是:不要相信API错误消息 :) - Valentine Shi
我喜欢这个,但使用 mb_str_split()mb_ord() 来获取正确的 CharNo。 - MrMacvos

22

对我来说不起作用。我希望我能够附上测试过的代码行,但不幸的是它包含无效字符。 - Nir O.
3
抱歉,在进行了更多测试后,我意识到这并没有实现我原先想要的功能。我现在正在使用https://dev59.com/V3M_5IYBdhLWcg3wZSE6#8215387。 - Markus Hedlund

18

试试这个:

$string = iconv("UTF-8","UTF-8//IGNORE",$string);
根据iconv手册所述,该函数将把第一个参数作为输入字符集,第二个参数作为输出字符集,第三个参数作为实际输入字符串。
如果将输入和输出字符集都设置为UTF-8,并在输出字符集后添加//IGNORE标志,则该函数将从输入字符串中删除(去除)所有无法由输出字符集表示的字符。因此,实际上对输入字符串进行了过滤。

3
我尝试过使用 //IGNORE,但似乎无法抑制无效的 UTF-8 通知(当然,我知道这个问题,并想要修复它)。手册中一条评分很高的评论似乎认为这是一个已经存在了几年的 bug。 - halfer
最好使用 iconv。@halfer,也许你的输入数据不是来自 utf-8。另一个选项是将其重新转换为 ascii,然后再次转换为 utf-8。在我的情况下,我使用了 iconv,如 $output = iconv("UTF-8//", "ISO-8859-1//IGNORE", $input ); - m3nda
@erm3nda:我确切地不记得我使用这个的原因 - 可能是解析一个声明错误字符集的UTF-8网站。感谢您的提醒,我相信这对未来的读者会有用。 - halfer
是的,如果你不知道某些东西,只需测试它,最终你就会找到答案;-) - m3nda

11

嗨,你可以使用简单的正则表达式

$text = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $text);

它将从字符串中截断所有非UTF-8字符


1
这也会删除有效的日语字符。 - user706420

10

9

自PHP 5.5开始,可以使用UConverter。如果您使用intl扩展且不使用mbstring,则最好选择UConverter。

function replace_invalid_byte_sequence($str)
{
    return UConverter::transcode($str, 'UTF-8', 'UTF-8');
}

function replace_invalid_byte_sequence2($str)
{
    return (new UConverter('UTF-8', 'UTF-8'))->convert($str);
}

自 PHP 5.4 起,可以使用 htmlspecialchars 函数来删除无效字节序列。和 preg_match 相比,htmlspecialchars 处理字节大小和准确性更好,尤其在处理大型字节时表现更佳。我们经常发现有很多错误的正则表达式实现方式。

function replace_invalid_byte_sequence3($str)
{
    return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE, 'UTF-8'));
}

你有三个不错的解决方案,但用户如何在它们之间进行选择并不清楚。 - Bob Ray

7

我已经编写了一个函数,可以从字符串中删除无效的UTF-8字符。在生成XML导出文件之前,我使用它来清除27000个产品的描述。

public function stripInvalidXml($value) {
    $ret = "";
    $current;
    if (empty($value)) {
        return $ret;
    }
    $length = strlen($value);
    for ($i=0; $i < $length; $i++) {
        $current = ord($value{$i});
        if (($current == 0x9) || ($current == 0xA) || ($current == 0xD) || (($current >= 0x20) && ($current <= 0xD7FF)) || (($current >= 0xE000) && ($current <= 0xFFFD)) || (($current >= 0x10000) && ($current <= 0x10FFFF))) {
                $ret .= chr($current);
        }
        else {
            $ret .= "";
        }
    }
    return $ret;
}

我对这个函数感到困惑。ord() 返回的结果在0-255范围内。这个函数中的巨大的 if 语句测试了 ord() 永远不会返回的 Unicode 范围。如果有人能够澄清这个函数为什么会这样工作,我将非常感激。 - i336_

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