从字符串中删除非UTF8字符


112

我在从字符串中删除非utf8字符时出现问题,这些字符无法正确显示。像这样的字符0x97 0x61 0x6C 0x6F(十六进制表示)

删除它们的最佳方法是什么?正则表达式还是其他?


1
这里列出的解决方案对我不起作用,因此我在“字符验证”部分找到了我的答案:webcollab.sourceforge.net/unicode.html
bobef 2011年

与此相关的,但不一定是重复的,更像是一个表亲:)
Wayne Weibel 2013年

Answers:


87

使用正则表达式方法:

$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字符序列。


$regex = <<<'END'PHP <5.3.x 使用什么代替?
serhio 2010年

您可以将它们转换为Heredoc格式,但对可读性会有一点损失。另一种可能性是使用单引号字符串,但是您将必须删除注释。
Markus Jarderot 2010年

这行中有一个小错字,elseif (!empty($captures([2])) {您应该使用!== ""空而不是空,因为它"0"被认为是空的。而且此功能非常慢,可以更快吗?
肯德尔·霍普金斯

2
此表达式存在主要的内存问题,请参见此处
杰克

1
@MarkusJarderot,正则 表达式……。嗯,此功能可以投入生产了吗?是否有此功能的测试用例?
Pacerier

132

如果您utf8_encode()已将字符串应用于UTF8,它将返回乱码的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字符串,这些字符串看起来已经多次编码为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


13
优秀的东西!所有其他解决方案都将丢弃无效的字符,但这可以解决。太棒了
giorgio79 '11

4
您发挥了出色的功能!过去,我从事过XML Feed的很多工作,并且始终在编码方面遇到问题。谢谢。
Kostanos

5
我爱你。您为我节省了UTF8字符错误的“ bloomin”工作的小时。谢谢。
John Ballinger 2013年

4
这是太棒了。谢谢
EdgeCaseBerg 2014年

2
太好了,做得好!很高兴我找到了这个。我希望我可以投票给+100 ;-)
Codebeat 2015年

61

您可以使用mbstring:

$text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');

...将删除无效字符。

请参阅:用问号替换无效的UTF-8字符,mbstring.substitute_character似乎被忽略


1
@Alliswell哪一个?您能举个例子吗?
Frosty Z

当然,<0x1a>
Alliswell '19

1
@Alliswell如果我没记错的话<0x1a>,虽然不是可打印字符,但它是完全有效的UTF-8序列。您可能遇到不可打印字符的问题?检查:stackoverflow.com/questions/1176904/...
雾ž

是的,就是这样。谢了哥们!
Alliswell,

在调用mb convert之前,我必须将mbstring替代字符设置为none,ini_set('mbstring.substitute_character', 'none');否则结果中将出现问号。
cby016 '19

21

此函数删除所有NON 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?

8
为什么全大写功能名?Ewww。
克里斯·贝克

5
它是ASCII,甚至不接近问题所要的内容。
misaxi 2013年

1
这个工作了。当Google Maps API由于API请求网址中的“非UTF-8字符”而报告错误时,我遇到了问题。罪魁祸首是í地址字段中的字符,它是有效的UTF-8字符,请参见表。士气:不要相信API错误消息:)
Valentine Shi


14

试试这个:

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

根据iconv手册,该函数将第一个参数作为输入字符集,第二个参数作为输出字符集,第三个参数作为实际输入字符串。

如果将输入和输出字符集都设置为UTF-8,并将//IGNORE标志附加到输出字符集,则该函数将丢弃(剥离)输入字符串中所有不能由输出字符集表示的字符。因此,过滤输入字符串有效。


说明您的答案是什么,而不是转储代码片段。
Tomasz Kowalczyk 2014年

3
我已经尝试过了,并且//IGNORE似乎并没有抑制出现无效UTF-8的通知(当然,我知道并且想解决)。手册中受到高度评价的评论似乎认为它已经存在多年了。
2015年

总是更好用iconv。@halfer也许您的输入数据不是来自utf-8。另一个选择是重新转换为ascii,然后再次返回到utf-8。在我来说,我没有使用iconv$output = iconv("UTF-8//", "ISO-8859-1//IGNORE", $input );
m3nda

@ erm3nda:我完全不记得我的用例-可能已经解析了使用错误字符集声明的UTF-8网站。感谢您的来信,我相信这将对以后的读者有用。
Halfer

是的,如果您不知道某些内容,只需对其进行测试,最后您就会按关键键;-)
m3nda


6

从PHP 5.5开始可以使用UConverter。如果使用国际扩展名而不使用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可用于删除无效的字节序列。Htmlspecialchars在处理大字节大小和准确性方面优于preg_match。可以看到很多使用正则表达式的错误实现。

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

您有三种不错的解决方案,但尚不清楚用户将如何选择它们。
鲍勃·雷

6

我做了一个从字符串中删除无效的UTF-8字符的函数。我用它来清除27000产品的描述,然后再生成XML导出文件。

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

在上面所有复杂的答案中,这一个对我有用!谢谢。
EminÖzlem'16

我对此功能感到困惑。 ord()返回结果,范围为0-255。if此函数中的巨人ord()将测试永不返回的unicode范围。如果有人想弄清楚为什么该功能以它的工作方式起作用,我将不胜感激。
i336_ '18

4

欢迎来到2019和/uregex中的修饰符,它将为您处理UTF-8多字节字符

如果仅使用mb_convert_encoding($value, 'UTF-8', 'UTF-8'),字符串中仍然会出现不可打印的字符

该方法将:

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

方法:

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


2

从最近的补丁到Drupal的Feed JSON解析器模块:

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

如果您担心,它将保留空格作为有效字符。

做了我所需要的。它删除了当今不流行的表情符号字符,这些字符不适合MySQL的'utf8'字符集,并且给了我类似“ SQLSTATE [HY000]:常规错误:1366不正确的字符串值”的错误。

有关详细信息,请参见https://www.drupal.org/node/1824506#comment-6881382


iconv远比基于老式的正则表达式更好preg_replace,至极是时下弃用。
m3nda


1
您完全正确,是ereg_replace(),抱歉。
m3nda

2

也许不是最精确的解决方案,但是它只需一行代码即可完成工作:

echo str_replace("?","",(utf8_decode($str)));

utf8_decode将字符转换为问号;
str_replace将删除问号。


在尝试了数百种解决方案之后,唯一可行的解​​决方案就是您自己的解决方案。
Haritsinh Gohil

1

因此规则是,第一个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'2

1

要删除Unicode基本语言平面之外的所有Unicode字符,请执行以下操作:

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

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
static $preg = <<<'END'
%(
[\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}
)%xs
END;
if (preg_match_all($preg, $string, $match)) {
    $string = implode('', $match[0]);
} else {
    $string = '';
}

它对我们的服务有效


2
您是否可以添加一些上下文来解释这将如何回答问题,而不是仅针对代码的回答。
阿伦·维诺斯

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.