关于PHP:从字符串中删除非UTF8字符

Remove non-utf8 characters from string

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

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


如果将utf8_encode()应用于已经存在的UTF8字符串,它将返回乱码的UTF8输出。

我做了一个解决所有这些问题的功能。它称为Encoding::toUTF8()

您无需知道字符串的编码是什么。它可以是Latin1(ISO8859-1),Windows-1252或UTF8,或者字符串可以混合使用。 Encoding::toUTF8()会将所有内容转换为UTF8。

我这样做是因为某项服务使我的数据馈送全乱了,将那些编码混合在同一字符串中。

用法:

1
2
3
4
5
6
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,因此看起来是乱码。

用法:

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

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

例子:

1
2
3
4
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");

将输出:

1
2
3
4
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


使用正则表达式方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
$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字符,可以修复字符串。但是,如果错误是随机的,则可能会留下一些奇怪的符号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$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字符序列。


您可以使用mbstring:

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

...将删除无效字符。

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


此函数删除所有NON ASCII字符,这很有用,但不能解决问题:
这是我的函数,无论编码如何,它始终有效:

1
2
3
4
5
6
7
8
9
10
11
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;
}

这个怎么运作:

1
echo remove_bs('Hello ?how? ?are you??'); // Hello how are you?


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

这就是我正在使用的。似乎工作得很好。取自http://planetozh.com/blog/2005/01/remove-invalid-characters-in-utf-8/


尝试这个:

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

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

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


文本可能包含非utf8字符。尝试先做:

1
$nonutf8 = mb_convert_encoding($nonutf8 , 'UTF-8', 'UTF-8');

您可以在此处了解更多信息:http://php.net/manual/en/function.mb-convert-encoding.phpnews


从PHP 5.5开始可以使用UConverter。如果使用intl扩展名而不使用mbstring,则UConverter是更好的选择。

1
2
3
4
5
6
7
8
9
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。可以看到很多使用正则表达式的错误实现。

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


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}


1
$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'));

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

1
2
//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


因此规则是,第一个UTF-8八位位组将高位设置为标记,然后再设置1至4位以指示还有多少个八位位组;那么每个附加八位字节都必须将高两位设置为10。

伪python将是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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。但是,不清楚在获得格式错误的字符后将执行哪种剥离。


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

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

欢迎使用2019和regex中的/u修饰符,它将为您处理UTF-8多字节字符

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

该方法将:

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

方法:

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function utf8_filter(string $value): string{
    return preg_replace('/[^[:print:]
]/u'
, '', mb_convert_encoding($value, 'UTF-8', 'UTF-8'));
}

$arr = [
    'Danish chars'          => 'Hello from Denmark with ???',
    'Non-printable chars'   =>"\x7FHello with invalid chars
 \x00"

];

foreach($arr as $k => $v){
    echo"$k:
---------
"
;

    $len = strlen($v);
    echo"$v
("
.$len.")
"
;

    $strip = utf8_decode(utf8_filter(utf8_encode($v)));
    $strip_len = strlen($strip);
    echo $strip."
("
.$strip_len.")

"
;

    echo"Chars removed:".($len - $strip_len)."


"
;
}

https://www.tehplayground.com/q5sJ3FOddhv1atpR


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

伪代码在这里

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

输入和输出

1
2
"Headlight\x007E Bracket,  Cafe Racer<> Style,? Stainless Steel 中文呢?"
"Headlight~ Bracket,  Cafe Racer<> Style, Stainless Steel 中文呢?"

我知道这并不完美,但可以为我完成工作。


iconv怎么样:

http://php.net/manual/zh/function.iconv.php

尚未在PHP本身内部使用过它,但它在命令行上对我而言始终表现良好。您可以用它代替无效字符。