319 votes

Détecter l'encodage et rendre tout UTF-8

Je lis de nombreux textes à partir de divers flux RSS et je les insère dans ma base de données.

Bien entendu, il existe plusieurs codages de caractères différents utilisés dans les flux, par exemple UTF-8 et ISO 8859-1.

Malheureusement, il y a parfois des problèmes avec les encodages des textes. Exemple :

  1. Le "ß" de "Fußball" devrait ressembler à ça dans ma base de données : "". Si c'est un "", il est affiché correctement.

  2. Parfois, le "ß" de "Fußball" ressemble à ça dans ma base de données : "Ã". Dans ce cas, l'affichage est bien sûr erroné.

  3. Dans d'autres cas, le "ß" est enregistré comme un "ß" - donc sans aucune modification. Il est alors également affiché de manière erronée.

Que puis-je faire pour éviter les cas 2 et 3 ?

Comment faire pour que tout ait le même encodage, de préférence UTF-8 ? Quand dois-je utiliser utf8_encode() quand dois-je utiliser utf8_decode() (l'effet est clair, mais quand dois-je utiliser les fonctions ?) et quand dois-je ne rien faire avec l'entrée ?

Comment faire pour que tout ait le même encodage ? Peut-être avec la fonction mb_detect_encoding() ? Puis-je écrire une fonction pour cela ? Mes problèmes sont donc les suivants :

  1. Comment puis-je savoir quel est le codage utilisé par le texte ?
  2. Comment puis-je le convertir en UTF-8 - quel que soit l'ancien encodage ?

Une telle fonction fonctionnerait-elle ?

function correct_encoding($text) {
    $current_encoding = mb_detect_encoding($text, 'auto');
    $text = iconv($current_encoding, 'UTF-8', $text);
    return $text;
}

Je l'ai testé, mais ça ne fonctionne pas. Quel est le problème ?

38 votes

"Le "ß" de "Fußball" devrait ressembler à ça dans ma base de données : "Ÿ".". Non, il devrait ressembler à ß. Assurez-vous que votre collation et votre connexion sont correctement configurées. Sinon, le tri et la recherche ne fonctionneront pas.

5 votes

Votre base de données est mal configurée. Si vous voulez stocker du contenu Unicode, il suffit de la configurer pour cela. Ainsi, au lieu d'essayer de contourner le problème dans votre code PHP, vous devriez d'abord réparer la base de données.

2 votes

USE : $from=mb_detect_encoding($text) ; $text=mb_convert_encoding($text, 'UTF-8',$from) ;

376voto

Sebastián Grignoli Points 7670

Si vous appliquez utf8_encode() à une chaîne déjà UTF-8, il retournera une sortie UTF-8 déformée.

J'ai créé une fonction qui résout tous ces problèmes. Elle s'appelle Encoding::toUTF8() .

Vous n'avez pas besoin de savoir quel est l'encodage de vos chaînes de caractères. Il peut être Latin1 ( ISO 8859-1) , Windows-1252 ou UTF-8, ou la chaîne peut avoir un mélange des deux. Encoding::toUTF8() convertira tout en UTF-8.

Je l'ai fait parce qu'un service me donnait un flux de données tout chamboulé, mélangeant UTF-8 et Latin1 dans la même chaîne.

Utilisation :

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

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

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

Télécharger :

https://github.com/neitanod/forceutf8

J'ai inclus une autre fonction, Encoding::fixUFT8() qui corrigera toutes les chaînes UTF-8 qui semblent déformées.

Utilisation :

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

produira un résultat :

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

J'ai transformé la fonction ( forceUTF8 ) en une famille de fonctions statiques sur une classe appelée Encoding . La nouvelle fonction est Encoding::toUTF8() .

0 votes

Merci beaucoup, c'est exactement ce que je cherchais :) Mais il serait mieux d'avoir une seule et unique fonction qui fait tout. Donc forceUTF8() devrait inclure les compétences de fixUTF8().

1 votes

Eh bien, si vous regardez le code, fixUTF8 appelle simplement forceUTF8 une fois et encore jusqu'à ce que la chaîne soit retournée inchangée. Un appel à fixUTF8() prend au moins deux fois plus de temps qu'un appel à forceUTF8(), donc c'est beaucoup moins performant. J'ai fait fixUTF8() juste pour créer un programme en ligne de commande qui corrigerait les fichiers "corrompus par l'encodage", mais dans un environnement réel, c'est rarement nécessaire.

3 votes

Comment convertir les caractères non-UTF8 en UTF8, sans savoir dans quel encodage se trouvent les caractères invalides ?

75voto

Gumbo Points 279147

Vous devez d'abord détecter quel encodage a été utilisé. Comme vous analysez des flux RSS (probablement via HTTP), vous devez lire l'encodage à partir de la balise charset du paramètre Content-Type Champ d'en-tête HTTP . S'il n'est pas présent, l'encodage est lu à partir de la balise encoding de l'attribut Instruction de traitement XML . Si cela manque aussi, utiliser UTF-8 tel que défini dans la spécification .


Modifier    Voici ce que je ferais probablement :

J'utiliserais cURL pour envoyer et récupérer la réponse. Cela vous permet de définir des champs d'en-tête spécifiques et de récupérer également l'en-tête de la réponse. Après avoir récupéré la réponse, vous devez analyser la réponse HTTP et la diviser en en-tête et corps. L'en-tête doit alors contenir le champ Content-Type qui contient le type MIME et (avec un peu de chance) le nom de l'utilisateur. charset avec le codage/charset également. Si ce n'est pas le cas, nous analyserons l'IP XML pour vérifier la présence de la balise encoding et récupérer l'encodage à partir de là. Si cet attribut est également absent, les spécifications XML définissent l'utilisation de l'UTF-8 comme encodage.

$url = 'http://www.lr-online.de/storage/rss/rss/sport.xml';

$accept = array(
    'type' => array('application/rss+xml', 'application/xml', 'application/rdf+xml', 'text/xml'),
    'charset' => array_diff(mb_list_encodings(), array('pass', 'auto', 'wchar', 'byte2be', 'byte2le', 'byte4be', 'byte4le', 'BASE64', 'UUENCODE', 'HTML-ENTITIES', 'Quoted-Printable', '7bit', '8bit'))
);
$header = array(
    'Accept: '.implode(', ', $accept['type']),
    'Accept-Charset: '.implode(', ', $accept['charset']),
);
$encoding = null;
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
$response = curl_exec($curl);
if (!$response) {
    // error fetching the response
} else {
    $offset = strpos($response, "\r\n\r\n");
    $header = substr($response, 0, $offset);
    if (!$header || !preg_match('/^Content-Type:\s+([^;]+)(?:;\s*charset=(.*))?/im', $header, $match)) {
        // error parsing the response
    } else {
        if (!in_array(strtolower($match[1]), array_map('strtolower', $accept['type']))) {
            // type not accepted
        }
        $encoding = trim($match[2], '"\'');
    }
    if (!$encoding) {
        $body = substr($response, $offset + 4);
        if (preg_match('/^<\?xml\s+version=(?:"[^"]*"|\'[^\']*\')\s+encoding=("[^"]*"|\'[^\']*\')/s', $body, $match)) {
            $encoding = trim($match[1], '"\'');
        }
    }
    if (!$encoding) {
        $encoding = 'utf-8';
    } else {
        if (!in_array($encoding, array_map('strtolower', $accept['charset']))) {
            // encoding not accepted
        }
        if ($encoding != 'utf-8') {
            $body = mb_convert_encoding($body, 'utf-8', $encoding);
        }
    }
    $simpleXML = simplexml_load_string($body, null, LIBXML_NOERROR);
    if (!$simpleXML) {
        // parse error
    } else {
        echo $simpleXML->asXML();
    }
}

0 votes

Merci. Ce serait facile. Mais cela fonctionnerait-il vraiment ? Il y a souvent des encodages erronés dans les en-têtes HTTP ou dans les attributs du XML.

25 votes

Encore une fois : Ce n'est pas votre problème. Les normes ont été établies pour éviter de tels problèmes. Si les autres ne les suivent pas, c'est leur problème, pas le vôtre.

0 votes

Ok, je pense que vous avez fini par me convaincre maintenant. :)

40voto

troelskn Points 51966

La détection de l'encodage est difficile.

mb_detect_encoding fonctionne en devinant, sur la base d'un certain nombre de candidats que vous lui transmettez. Dans certains codages, certaines séquences d'octets ne sont pas valides, et il peut donc faire la distinction entre plusieurs candidats. Malheureusement, il y a beaucoup de codages, où les mêmes octets sont valides (mais différents). Dans ces cas, il n'y a aucun moyen de déterminer l'encodage ; vous pouvez implémenter votre propre logique pour faire des suppositions dans ces cas. Par exemple, les données provenant d'un site japonais sont plus susceptibles d'avoir un encodage japonais.

Tant que vous ne traitez que des langues d'Europe occidentale, les trois principaux codages à prendre en compte sont les suivants utf-8 , iso-8859-1 et cp-1252 . Comme il s'agit de valeurs par défaut pour de nombreuses plates-formes, elles sont également les plus susceptibles d'être signalées à tort. Par exemple, si des personnes utilisent des encodages différents, elles sont susceptibles d'être franches à ce sujet, sinon leurs logiciels tomberaient très souvent en panne. Par conséquent, une bonne stratégie consiste à faire confiance au fournisseur, sauf si l'encodage est signalé comme étant l'un de ces trois. Vous devez tout de même vérifier que l'encodage est valide, en utilisant la commande mb_check_encoding (notez que valide n'est pas la même chose que être - la même entrée peut être valable pour plusieurs encodages). S'il s'agit de l'un d'entre eux, vous pouvez alors utiliser mb_detect_encoding pour les distinguer. Heureusement, c'est assez déterministe ; vous devez juste utiliser la séquence de détection appropriée, qui est UTF-8,ISO-8859-1,WINDOWS-1252 .

Une fois que vous avez détecté l'encodage, vous devez le convertir en votre représentation interne ( UTF-8 est le seul choix sensé). La fonction utf8_encode transforme ISO-8859-1 à UTF-8 Il ne peut donc être utilisé que pour ce type d'entrée particulier. Pour les autres codages, utilisez mb_convert_encoding .

0 votes

Merci beaucoup ! Qu'est-ce qui est mieux : mb-convert-encoding() ou iconv() ? Je ne sais pas quelles sont les différences. Oui, je n'aurai à analyser que les langues d'Europe occidentale, en particulier l'anglais, l'allemand et le français.

8 votes

Je viens de voir : mb-detect-encoding() est inutile. Il ne supporte que UTF-8, UTF-7, ASCII, EUC-JP,SJIS, eucJP-win, SJIS-win, JIS et ISO-2022-JP. Les plus importants pour moi, ISO-8859-1 et Windows-1252, ne sont pas supportés. Je ne peux donc pas utiliser mb-detect-encoding().

1 votes

Vous avez raison. Cela fait un moment que je ne l'ai pas utilisé. Vous devrez donc écrire votre propre code de détection, ou utiliser un utilitaire externe. UTF-8 peut être déterminé de manière assez fiable, parce que ses séquences d'échappement sont assez caractéristiques. wp-1252 et iso-8859-1 peuvent être distingués parce que wp-1252 peut contenir des octets qui sont illégaux dans iso-8859-1. Utilisez Wikipedia pour obtenir les détails, ou regardez dans la section des commentaires de php.net, sous diverses fonctions liées au jeu de caractères.

14voto

miek Points 2173

Cette aide-mémoire liste les problèmes courants liés à la gestion de l'UTF-8 en PHP : http://developer.loftdigital.com/blog/php-utf-8-cheatsheet

Cette fonction de détection des caractères multi-octets dans une chaîne de caractères peut également s'avérer utile ( source ) :

function detectUTF8($string)
{
    return preg_match('%(?:
        [\xC2-\xDF][\x80-\xBF]             # non-overlong 2-byte
        |\xE0[\xA0-\xBF][\x80-\xBF]        # excluding overlongs
        |[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
        |\xED[\x80-\x9F][\x80-\xBF]        # excluding surrogates
        |\xF0[\x90-\xBF][\x80-\xBF]{2}     # planes 1-3
        |[\xF1-\xF3][\x80-\xBF]{3}         # planes 4-15
        |\xF4[\x80-\x8F][\x80-\xBF]{2}     # plane 16
        )+%xs', 
    $string);
}

2 votes

Je pense que cela ne fonctionne pas correctement : echo detectUTF8('33') ; # 1

14voto

harpax Points 2635

A vraiment une bonne façon de mettre en œuvre un isUTF8 -peut être trouvée sur php.net :

function isUTF8($string) {
    return (utf8_encode(utf8_decode($string)) == $string);
}

16 votes

Malheureusement, cela ne fonctionne que lorsque la chaîne de caractères est constituée uniquement de caractères inclus dans la norme ISO-8859-1. Mais ceci pourrait fonctionner : @iconv('utf-8', 'utf-8//IGNORE', $str) == $str

0 votes

@Christian : En effet, c'est ce que les auteurs de High Performance MySQL recommandent également.

1 votes

Cela ne fonctionne pas correctement : echo (int)isUTF8(' z') ; # 1 echo (int)isUTF8(NULL) ; # 1

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X