Supprimer les caractères non utf8 de la chaîne

112

J'ai un problème avec la suppression des caractères non utf8 de la chaîne, qui ne s'affichent pas correctement. Les caractères sont comme ceci 0x97 0x61 0x6C 0x6F (représentation hexadécimale)

Quelle est la meilleure façon de les supprimer? Expression régulière ou autre chose?

Dan Sosedoff
la source
1
Les solutions listées ici n'ont pas fonctionné pour moi donc j'ai trouvé ma réponse ici dans la section "Validation des caractères": webcollab.sourceforge.net/unicode.html
bobef
Lié à cela , mais pas nécessairement un doublon, plus comme un cousin proche :)
Wayne Weibel

Réponses:

87

En utilisant une approche 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);

Il recherche les séquences UTF-8 et les capture dans le groupe 1. Il correspond également à des octets uniques qui n'ont pas pu être identifiés comme faisant partie d'une séquence UTF-8, mais ne les capture pas. Le remplacement est ce qui a été capturé dans le groupe 1. Cela supprime efficacement tous les octets non valides.

Il est possible de réparer la chaîne en codant les octets invalides en caractères UTF-8. Mais si les erreurs sont aléatoires, cela pourrait laisser des symboles étranges.

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

ÉDITER:

  • !empty(x)correspondra aux valeurs non vides ( "0"est considéré comme vide).
  • x != ""correspondra aux valeurs non vides, y compris "0".
  • x !== ""correspondra à tout sauf "".

x != "" semble le meilleur à utiliser dans ce cas.

J'ai aussi un peu accéléré le match. Au lieu de faire correspondre chaque caractère séparément, il correspond aux séquences de caractères UTF-8 valides.

Markus Jarderot
la source
que utiliser à la place $regex = <<<'END'pour PHP <5.3.x?
serhio
Vous pouvez les convertir au format heredoc à la place, avec une légère pénalité pour la lisibilité. Une autre possibilité est d'utiliser des chaînes de guillemets simples, mais vous devrez ensuite supprimer les commentaires.
Markus Jarderot
Il y a une petite faute de frappe dans cette ligne elseif (!empty($captures([2])) {et vous devriez utiliser !== ""au lieu de vide car "0"est considéré comme vide. Cette fonction est également très lente, cela pourrait-il être fait plus rapidement?
Kendall Hopkins
2
Cette expression a un problème de mémoire majeur, voir ici .
Ja͢ck
1
@MarkusJarderot, Regex ....... hmm, cette fonction est-elle prête pour la production? Existe-t-il des cas de test pour cette fonction?
Pacerier
132

Si vous appliquez utf8_encode()à une chaîne déjà UTF8, il renverra une sortie UTF8 déformée.

J'ai créé une fonction qui aborde tous ces problèmes. Ça s'appelle Encoding::toUTF8().

Vous n'avez pas besoin de savoir quel est l'encodage de vos chaînes. Il peut s'agir de Latin1 (ISO8859-1), Windows-1252 ou UTF8, ou la chaîne peut en avoir un mélange. Encoding::toUTF8()convertira tout en UTF8.

Je l'ai fait parce qu'un service me donnait un flux de données tout foiré, mélangeant ces encodages dans la même chaîne.

Usage:

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

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

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

J'ai inclus une autre fonction, Encoding :: fixUTF8 (), qui corrigera chaque chaîne UTF8 qui semble être un produit déformé d'avoir été encodée en UTF8 plusieurs fois.

Usage:

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

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

Exemples:

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

affichera:

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

Télécharger:

https://github.com/neitanod/forceutf8

Sebastián Grignoli
la source
13
Des trucs exceptionnels! Toutes les autres solutions suppriment les caractères invalides, mais celle-ci le corrige. Impressionnant.
giorgio79
4
Vous avez fait la grande fonction! J'ai beaucoup travaillé avec les flux XML dans le passé et j'ai toujours eu un problème d'encodage. Je vous remercie.
Kostanos
5
JE T'AIME. Vous m'avez sauvé des HEURES de travail "bloomoin" sur les mauvais caractères UTF8. Merci.
John Ballinger
4
C'est fantastique. Merci
EdgeCaseBerg
2
magnifique, bien fait! Heureux d'avoir trouvé ça. J'aimerais pouvoir voter avec +100 ;-)
Codebeat
61

Vous pouvez utiliser mbstring:

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

... supprimera les caractères invalides.

Voir: Remplacement des caractères UTF-8 non valides par des points d'interrogation, mbstring.substitute_character semble ignoré

Frosty Z
la source
1
@Alliswell lesquels? Pouvez-vous donner un exemple?
Frosty Z
sûr,<0x1a>
Alliswell
1
@Alliswell Si je ne me trompe pas <0x1a>, bien que non imprimable, c'est une séquence UTF-8 parfaitement valide. Vous pourriez avoir des problèmes avec les caractères non imprimables? Vérifiez ceci: stackoverflow.com/questions/1176904/…
Frosty Z
ouais, c'est le cas. Merci mon pote!
Alliswell
Avant d'appeler mb convert, je devais définir le caractère de remplacement mbstring sur aucun, ini_set('mbstring.substitute_character', 'none');sinon j'obtenais des points d'interrogation dans le résultat.
cby016
21

Cette fonction supprime tous les caractères NON ASCII, c'est utile mais ne résout pas la question:
c'est ma fonction qui fonctionne toujours, quel que soit l'encodage:

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

Comment ça fonctionne:

echo remove_bs('Hello õhowå åare youÆ?'); // Hello how are you?
David D
la source
8
Pourquoi des noms de fonctions en majuscules? Ewww.
Chris Baker
5
c'est ASCII et pas même proche de ce que la question voulait.
misaxi
1
Celui-ci a fonctionné. J'ai rencontré le problème lorsque l'API Google Maps a signalé l'erreur en raison d'un "caractère non UTF-8" dans l'URL de la demande d'API. Le coupable était un ícaractère dans le champ d'adresse qui EST un caractère UTF-8 valide voir tableau . Le moral: ne faites pas confiance aux messages d'erreur de l'API :)
Valentine Shi
17
$text = iconv("UTF-8", "UTF-8//IGNORE", $text);

C'est ce que j'utilise. Semble fonctionner plutôt bien. Tiré de http://planetozh.com/blog/2005/01/remove-invalid-characters-in-utf-8/

Znarkus
la source
n'a pas fonctionné pour moi. J'aimerais pouvoir attacher la ligne testée, mais malheureusement, elle contient des caractères invalides.
Nir O.
3
Désolé, après quelques tests, j'ai réalisé que cela ne faisait pas vraiment ce que je pensais. J'utilise maintenant stackoverflow.com/a/8215387/138023
Znarkus
14

essaye ça:

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

Selon le manuel iconv , la fonction prendra le premier paramètre comme jeu de caractères d'entrée, le deuxième paramètre comme jeu de caractères de sortie et le troisième comme chaîne d'entrée réelle.

Si vous définissez à la fois le jeu de caractères d'entrée et de sortie sur UTF-8 et que vous ajoutez l' //IGNOREindicateur au jeu de caractères de sortie, la fonction supprime (supprime) tous les caractères de la chaîne d'entrée qui ne peuvent pas être représentés par le jeu de caractères de sortie. Ainsi, filtrage de la chaîne d'entrée en vigueur.

technoarya
la source
Expliquez ce que fait votre réponse plutôt que de vider un extrait de code.
Tomasz Kowalczyk
3
J'ai essayé cela, et le //IGNOREne semble pas supprimer l'avis selon lequel un UTF-8 invalide est présent (ce que, bien sûr, je connais et que je veux corriger). Un commentaire très apprécié dans le manuel semble penser que c'est un bogue depuis quelques années.
halfer
Est toujours préférable d'utiliser iconv. @halfer Peut-être que vos données d'entrée ne proviennent pas d'utf-8. Une autre option consiste à effectuer une reconversion en ascii puis de nouveau à utf-8. Dans mon cas, j'ai utilisé iconvcomme$output = iconv("UTF-8//", "ISO-8859-1//IGNORE", $input );
m3nda
@ erm3nda: Je ne me souviens pas exactement de mon cas d'utilisation pour cela - j'ai peut-être analysé un site Web UTF-8 déclaré avec le mauvais jeu de caractères. Merci pour la note, je suis sûr que cela sera utile pour un futur lecteur.
halfer
Oui, si vous ne savez pas quelque chose, testez-le et enfin vous
appuierez
6

UConverter peut être utilisé depuis PHP 5.5. UConverter est le meilleur choix si vous utilisez l'extension intl et n'utilisez pas 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 peut être utilisé pour supprimer une séquence d'octets invalide depuis PHP 5.4. Htmlspecialchars est meilleur que preg_match pour gérer une grande taille d'octet et la précision. Une grande partie de l'implémentation incorrecte en utilisant une expression régulière peut être vue.

function replace_invalid_byte_sequence3($str)
{
    return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE, 'UTF-8'));
}
masakielastic
la source
Vous avez trois bonnes solutions, mais on ne sait pas comment un utilisateur choisirait parmi elles.
Bob Ray
6

J'ai créé une fonction qui supprime les caractères UTF-8 invalides d'une chaîne. Je l'utilise pour clarifier la description de 27000 produits avant de générer le fichier d'exportation 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
la source
De toutes les réponses complexes ci-dessus, celle-ci a fait l'affaire pour moi! Merci.
Emin Özlem
Je suis confus par cette fonction. ord()renvoie des résultats compris entre 0 et 255. Le géant ifde cette fonction teste les plages unicode qui ord()ne reviendront jamais. Si quelqu'un veut clarifier pourquoi cette fonction fonctionne de cette manière, j'apprécierais la perspicacité.
i336_
4

Bienvenue dans 2019 et le /umodificateur de regex qui gérera les caractères multioctets UTF-8 pour vous

Si vous utilisez uniquement, mb_convert_encoding($value, 'UTF-8', 'UTF-8')vous vous retrouverez toujours avec des caractères non imprimables dans votre chaîne

Cette méthode:

  • Supprimez tous les caractères multioctets UTF-8 non valides avec mb_convert_encoding
  • Supprimez tous les caractères non imprimables comme \r, \x00(NULL-byte) et les autres caractères de contrôle avecpreg_replace

méthode:

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

[:print:]faire correspondre tous les caractères et \nnouvelles lignes imprimables et supprimer tout le reste

Vous pouvez voir le tableau ASCII ci-dessous. Les caractères imprimables vont de 32 à 127, mais le saut de ligne \nfait partie des caractères de contrôle qui vont de 0 à 31, nous devons donc ajouter une nouvelle ligne à l'expression régulière/[^[:print:]\n]/u

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

Vous pouvez essayer d'envoyer des chaînes via l'expression régulière avec des caractères en dehors de la plage imprimable comme \x7F(DEL), \x1B(Esc) etc. et voir comment ils sont supprimés

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

Clarkk
la source
Bienvenue en 2047, où php-mbstringn'est pas emballé en php par défaut.
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
la source
2

Du patch récent au module d'analyseur JSON Feeds de Drupal:

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

Si vous êtes concerné, oui, il conserve les espaces comme caractères valides.

J'ai fait ce dont j'avais besoin. Il supprime les caractères emoji répandus de nos jours qui ne rentrent pas dans le jeu de caractères 'utf8' de MySQL et qui m'ont donné des erreurs comme "SQLSTATE [HY000]: Erreur générale: 1366 Valeur de chaîne incorrecte".

Pour plus de détails, voir https://www.drupal.org/node/1824506#comment-6881382

Oleksii Chekulaiev
la source
C'est iconvbien mieux que l'ancienne expression rationnelle preg_replace, qui est aujourd'hui obsolète.
m3nda
3
preg_replace n'est pas obsolète
Oleksii Chekulaiev
1
Vous avez tout à fait raison, est le ereg_replace(), désolé.
m3nda
2

Peut-être pas la solution la plus précise, mais elle fait le travail avec une seule ligne de code:

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

utf8_decodeconvertira les caractères en point d'interrogation;
str_replacesupprimera les points d'interrogation.

user12602477
la source
Après avoir essayé des centaines de solutions, la seule solution qui a fonctionné est la vôtre.
Haritsinh Gohil le
1

Ainsi, les règles sont que le premier octlet UTF-8 a le bit haut défini comme marqueur, puis 1 à 4 bits pour indiquer le nombre d'octlets supplémentaires; alors chacun des octets supplémentaires doit avoir les deux bits hauts mis à 10.

Le pseudo-python serait:

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

Cette même logique devrait être traduisible en php. Cependant, il n'est pas clair quel type de décapage doit être fait une fois que vous avez un personnage mal formé.

Volonté
la source
c = (ch << 1)fera (c & 1)zéro la première fois, en sautant la boucle. Le test devrait probablement être(c & 128)
Markus Jarderot
1

Pour supprimer tous les caractères Unicode en dehors du plan de langage de base Unicode:

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

Légèrement différent de la question, mais ce que je fais est d'utiliser HtmlEncode (string),

pseudo code ici

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

entrée et sortie

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

Je sais que ce n'est pas parfait, mais fait le travail pour moi.

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

ça marche sur notre service

llluo
la source
2
Pouvez-vous ajouter un contexte pour expliquer comment cela répondra à la question, au lieu d'une réponse basée uniquement sur le code.
Arun Vinoth le
-1

Que diriez-vous d'iconv:

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

Je ne l'ai pas utilisé dans PHP lui-même, mais il a toujours bien fonctionné pour moi sur la ligne de commande. Vous pouvez l'obtenir pour remplacer des caractères invalides.

Ben
la source