Remover caracteres não utf8 da string

112

Estou tendo problemas para remover caracteres não utf8 da string, que não estão sendo exibidos corretamente. Os caracteres são assim 0x97 0x61 0x6C 0x6F (representação hexadecimal)

Qual é a melhor maneira de removê-los? Expressão regular ou outra coisa?

Dan Sosedoff
fonte
1
As soluções listadas aqui não funcionaram para mim, então encontrei minha resposta aqui na seção "Validação de caracteres": webcollab.sourceforge.net/unicode.html
bobef
Relacionado a isso , mas não necessariamente uma duplicata, mais como um primo próximo :)
Wayne Weibel

Respostas:

87

Usando uma abordagem regex:

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

Ele procura sequências UTF-8 e as captura no grupo 1. Também corresponde a bytes únicos que não puderam ser identificados como parte de uma sequência UTF-8, mas não os captura. Substituição é tudo o que foi capturado no grupo 1. Isso remove efetivamente todos os bytes inválidos.

É possível reparar a string, codificando os bytes inválidos como caracteres UTF-8. Mas se os erros forem aleatórios, isso pode deixar alguns símbolos estranhos.

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

EDITAR:

  • !empty(x) corresponderá a valores não vazios ("0" é considerado vazio).
  • x != ""corresponderá a valores não vazios, incluindo "0".
  • x !== ""corresponderá a qualquer coisa, exceto "".

x != "" parece o melhor para usar neste caso.

Também acelerei um pouco a partida. Em vez de combinar cada caractere separadamente, ele combina sequências de caracteres UTF-8 válidos.

Markus Jarderot
fonte
o que usar em vez $regex = <<<'END'do PHP <5.3.x?
serhio de
Você pode convertê-los para o formato heredoc em vez disso, com uma pequena penalidade para a legibilidade. Outra possibilidade é usar strings de aspas simples, mas então você terá que remover os comentários.
Markus Jarderot
Há um pequeno erro de digitação nesta linha elseif (!empty($captures([2])) {e você deve usar em !== ""vez de vazio, pois "0"é considerado vazio. Além disso, essa função é muito lenta, isso poderia ser feito mais rápido?
Kendall Hopkins
2
Esta expressão tem um grande problema de memória, veja aqui .
Ja͢ck
1
@MarkusJarderot, Regex ....... hmm, esta função está pronta para produção? Existem casos de teste para esta função?
Pacerier
132

Se você aplicar utf8_encode() a uma string UTF8, ela retornará uma saída UTF8 truncada.

Fiz uma função que aborda todas essas questões. É chamadoEncoding::toUTF8() .

Você não precisa saber qual é a codificação de suas strings. Pode ser Latin1 (ISO8859-1), Windows-1252 ou UTF8, ou a string pode ter uma combinação deles.Encoding::toUTF8()irá converter tudo para UTF8.

Fiz isso porque um serviço estava me dando um feed de dados todos bagunçados, misturando essas codificações na mesma string.

Uso:

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

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

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

Incluí outra função, Encoding :: fixUTF8 (), que corrigirá cada string UTF8 que pareça um produto ilegível por ter sido codificado em UTF8 várias vezes.

Uso:

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

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

Exemplos:

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");

irá produzir:

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

Baixar:

https://github.com/neitanod/forceutf8

Sebastián Grignoli
fonte
13
Coisas excelentes! Todas as outras soluções descartam caracteres inválidos, mas esta corrige. Impressionante.
giorgio79
4
Você fez a grande função! Trabalhei muito com XML Feeds no passado e sempre tive problemas com codificação. Obrigado.
Kostanos
5
EU TE AMO. Você me salvou HORAS de trabalho "bloomoin" em caracteres UTF8 ruins. Obrigado.
John Ballinger
4
Isto é fantástico. Obrigado
EdgeCaseBerg
2
maravilhoso, muito bem! Que bom que encontrei isso. Gostaria de poder votar com +100 ;-)
Codebeat de
61

Você pode usar mbstring:

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

... irá remover caracteres inválidos.

Consulte: Substituindo caracteres UTF-8 inválidos por pontos de interrogação, mbstring.substitute_character parece ignorado

Frosty Z
fonte
1
@Alliswell quais? Você poderia dar um exemplo?
Frosty Z de
com certeza,<0x1a>
Alliswell
1
@ Alliswell Se não me engano <0x1a>, embora não seja um caractere imprimível, é uma sequência UTF-8 perfeitamente válida. Você pode ter problemas com caracteres não imprimíveis? Verifique isto: stackoverflow.com/questions/1176904/…
Frosty Z
sim, esse é o caso. Obrigado, cara!
Alliswell
Antes de chamar mb convert, eu tinha que definir o caractere substituto mbstring como nenhum, ini_set('mbstring.substitute_character', 'none');caso contrário, recebia pontos de interrogação no resultado.
cby016
21

Esta função remove todos os caracteres NÃO ASCII, é útil, mas não resolve a questão:
Esta é minha função que sempre funciona, independentemente da codificação:

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

Como funciona:

echo remove_bs('Hello õhowå åare youÆ?'); // Hello how are you?
David D
fonte
8
Por que nomes de função all-caps? Ewww.
Chris Baker de
5
é ASCII e não chega nem perto do que a pergunta queria.
misaxi
1
Este funcionou. Eu enfrentei o problema quando a API do Google Maps relatou o erro devido ao 'caractere não UTF-8' no URL de solicitação da API. O culpado foi o ícaractere no campo de endereço, que É um caractere UTF-8 válido, consulte a tabela . Moral: não confie nas mensagens de erro de API :)
Valentine Shi
14

tente isto:

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

De acordo com o manual do iconv , a função terá o primeiro parâmetro como o conjunto de caracteres de entrada, o segundo parâmetro como o conjunto de caracteres de saída e o terceiro como a string de entrada real.

Se você definir o conjunto de caracteres de entrada e saída como UTF-8 e anexar o //IGNOREsinalizador ao conjunto de caracteres de saída, a função descartará (removerá) todos os caracteres na string de entrada que não podem ser representados pelo conjunto de caracteres de saída. Assim, filtrando a string de entrada em vigor.

technoarya
fonte
Explique o que sua resposta faz em vez de despejar um trecho de código.
Tomasz Kowalczyk
3
Tentei fazer isso e //IGNOREnão parece suprimir o aviso de que UTF-8 inválido está presente (o que, é claro, eu conheço e desejo corrigir). Um comentário bem avaliado no manual parece pensar que é um bug há alguns anos.
Halfer
É sempre melhor usar iconv. @halfer Talvez seus dados de entrada não sejam do utf-8. Outra opção é fazer uma reconversão para ascii e, em seguida, voltar para utf-8 novamente. No meu caso, usei iconvcomo$output = iconv("UTF-8//", "ISO-8859-1//IGNORE", $input );
m3nda
@ erm3nda: Eu exatamente não me lembro do meu caso de uso para isso - pode ter uma análise de um site UTF-8 declarada com o conjunto de caracteres errado. Obrigado pela nota, tenho certeza que será útil para um futuro leitor.
Halfer
Sim, se você não sabe de algo, apenas teste e finalmente você
apertará
6

UConverter pode ser usado desde o PHP 5.5. UConverter é a melhor escolha se você usar extensão intl e não usar mbstring.

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

htmlspecialchars podem ser usados ​​para remover sequências de bytes inválidas desde o PHP 5.4. Htmlspecialchars é melhor do que preg_match para lidar com grandes tamanhos de bytes e precisão. Muitas das implementações erradas usando expressões regulares podem ser vistas.

function replace_invalid_byte_sequence3($str)
{
    return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE, 'UTF-8'));
}
masakielástico
fonte
Você tem três boas soluções, mas não está claro como um usuário escolheria entre elas.
Bob Ray,
6

Criei uma função que exclui caracteres UTF-8 inválidos de uma string. Estou usando para limpar a descrição de 27.000 produtos antes de gerar o arquivo de exportação 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;
}
mumin
fonte
De todas as respostas complexas acima, esta funcionou para mim! Obrigado.
Emin Özlem
Estou confuso com esta função. ord()retorna resultados no intervalo 0-255. O gigante ifnesta função testa os intervalos de Unicode que ord()nunca retornarão. Se alguém quiser esclarecer por que essa função funciona dessa maneira, gostaria de receber o insight.
i336_
4

Bem-vindo a 2019 e ao /umodificador em regex que tratará de caracteres multibyte UTF-8 para você

Se você usar apenas mb_convert_encoding($value, 'UTF-8', 'UTF-8'), ainda assim ficará com caracteres não imprimíveis em sua string

Este método irá:

  • Remova todos os caracteres multibyte UTF-8 inválidos com mb_convert_encoding
  • Remova todos os caracteres não imprimíveis como \r, \x00(NULL-byte) e outros caracteres de controle compreg_replace

método:

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

[:print:]corresponder a todos os caracteres e \nnovas linhas imprimíveis e remover todo o resto

Você pode ver a tabela ASCII abaixo. Os caracteres imprimíveis variam de 32 a 127, mas a nova linha \né uma parte dos caracteres de controle que variam de 0 a 31, então temos que adicionar nova linha ao regex/[^[:print:]\n]/u

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

Você pode tentar enviar strings através do regex com caracteres fora do intervalo de impressão, como \x7F(DEL), \x1B(Esc) etc. e ver como eles são removidos

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

Clark
fonte
Bem-vindo em 2047, onde php-mbstringnão é empacotado em php por padrão.
NVRM
3
$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'));
Alix Axel
fonte
2

Do patch recente para o módulo analisador JSON de feeds do Drupal:

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

Se você está preocupado, sim, ele retém espaços como caracteres válidos.

Fiz o que eu precisava. Ele remove os caracteres emoji comuns hoje em dia que não cabem no conjunto de caracteres 'utf8' do MySQL e que me deram erros como "SQLSTATE [HY000]: Erro geral: 1366 Valor de string incorreto".

Para obter detalhes, consulte https://www.drupal.org/node/1824506#comment-6881382

Oleksii Chekulaiev
fonte
A iconvé muito melhor do que o antigo regexp moda baseada preg_replace, wich é obsoleto hoje em dia.
m3nda de
3
preg_replace não está obsoleto
Oleksii Chekulaiev
1
Você está totalmente certo ereg_replace(), desculpe.
m3nda de
2

Talvez não seja a solução mais precisa, mas realiza o trabalho com uma única linha de código:

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

utf8_decodeirá converter os caracteres em um ponto de interrogação;
str_replaceirá remover os pontos de interrogação.

user12602477
fonte
Depois de tentar centenas de soluções, a única solução que funcionou é a sua.
Haritsinh Gohil
1

Portanto, as regras são que o primeiro octlet UTF-8 tem o bit alto definido como marcador e, em seguida, 1 a 4 bits para indicar quantos octetos adicionais; então, cada um dos octlets adicionais deve ter os dois bits altos definidos como 10.

O pseudo-python seria:

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

Esta mesma lógica deve ser traduzida para php. No entanto, não está claro que tipo de remoção deve ser feito quando você obtém um caractere malformado.

Vai
fonte
c = (ch << 1)fará (c & 1)zero na primeira vez, pulando o loop. O teste provavelmente deveria ser(c & 128)
Markus Jarderot
1

Para remover todos os caracteres Unicode fora do plano de linguagem básico Unicode:

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

Um pouco diferente da pergunta, mas o que estou fazendo é usar HtmlEncode (string),

pseudo código aqui

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

entrada e saída

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

Eu sei que não é perfeito, mas faz o trabalho para mim.

misaxi
fonte
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 = '';
}

funciona em nosso serviço

Iluo
fonte
2
Você pode adicionar algum contexto para explicar como isso responderá à pergunta, em vez de uma resposta apenas de código.
Arun Vinoth
-1

Que tal o iconv:

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

Não usei dentro do PHP, mas sempre funcionou bem para mim na linha de comando. Você pode obtê-lo para substituir caracteres inválidos.

Ben
fonte