web-dev-qa-db-fra.com

PHP: Convertissez n'importe quelle chaîne en UTF-8 sans connaître le jeu de caractères d'origine, ou du moins essayez

J'ai une application qui traite avec des clients du monde entier et, naturellement, je souhaite que tout ce qui entre dans mes bases de données soit encodé en UTF-8.

Le principal problème pour moi est que je ne sais pas quel encodage sera la source d'une chaîne - cela pourrait être à partir d'une zone de texte (utiliser <form accept-charset="utf-8"> n'est utile que si l'utilisateur a effectivement soumis le formulaire) , ou cela pourrait provenir d’un fichier texte téléchargé, donc je n’ai vraiment aucun contrôle sur l’entrée.

Ce dont j'ai besoin, c'est d'une fonction ou d'une classe qui s'assure que tout ce qui entre dans ma base de données est, autant que possible, encodé en UTF-8. J'ai essayé iconv(mb_detect_encoding($text), "UTF-8", $text); mais cela pose problème (si l'entrée est "fiancée", elle renvoie "fiancé"). J'ai essayé beaucoup de choses = /

Pour les téléchargements de fichiers, j'aime bien l'idée de demander à l'utilisateur final de spécifier l'encodage qu'il utilise et de lui montrer un aperçu de la sortie, mais cela n'aide pas les pirates malveillants (en fait, leur vie un peu plus facile).

J'ai lu les autres SO questions sur le sujet, mais elles semblent toutes présenter des différences subtiles, telles que "J'ai besoin d'analyser les flux RSS" ou "Je récupère les données de sites Web" (ou même "Vous ne peut pas ").

Mais il doit y avoir quelque chose qui a au moins un bon essayer!

138
Grim...

Ce que vous demandez est extrêmement difficile. Si possible, demander à l'utilisateur de spécifier le codage est le meilleur. Prévenir une attaque ne devrait être ni plus facile ni plus difficile de cette façon.

Cependant, vous pouvez essayer ceci:

iconv(mb_detect_encoding($text, mb_detect_order(), true), "UTF-8", $text);

Si vous le définissez trop strict, vous obtiendrez un meilleur résultat.

235
Jeff Day

En Russie, nous avons 4 encodages populaires, votre question est donc très demandée ici.

Seuls les codes de caractères des symboles ne permettent pas de détecter le codage, car les pages de codes se croisent. Certaines pages de code dans différentes langues ont même une intersection complète. Donc, nous avons besoin d’une autre approche.

La seule façon de travailler avec des codages inconnus est de travailler avec des probabilités. Donc, nous ne voulons pas répondre à la question "qu'est-ce que l'encodage de ce texte?", Nous essayons de comprendre "qu'est-ce qui est probablement l'encodage de ce texte?".

Un gars du blog de technologie russe a inventé cette approche:

Construisez la plage de probabilités des codes de caractères dans chaque codage que vous souhaitez prendre en charge. Vous pouvez le construire en utilisant de gros textes dans votre langue (par exemple des fictions, utilisez Shakespeare pour l’anglais et Tolstoï pour le russe, lol). Vous obtiendrez qch comme ceci:

    encoding_1:
    190 => 0.095249209893009,
    222 => 0.095249209893009,
    ...
    encoding_2:
    239 => 0.095249209893009,
    207 => 0.095249209893009,
    ...
    encoding_N:
    charcode => probabilty

Prochain. Vous prenez du texte avec un codage inconnu et pour chaque codage de votre "dictionnaire de probabilités", vous recherchez la fréquence de chaque symbole dans un texte codé de manière inconnue. Somme des probabilités des symboles. Le codage avec une plus grande note est probablement le gagnant. De meilleurs résultats pour les plus gros textes.

Si vous êtes intéressé, je peux vous aider avec plaisir dans cette tâche. Nous pouvons considérablement augmenter la précision en construisant une liste de probabilités à deux caractères.

Btw. mb_detect_encoding certanly ne fonctionne pas. Oui du tout. Jetez un coup d’œil au code source de mb_detect_encoding dans "ext/mbstring/libmbfl/mbfl/mbfl_ident.c".

28
Oroboros102

Vous avez probablement déjà essayé cela, mais pourquoi ne pas utiliser simplement la fonction mb_convert_encoding? Il essaiera de détecter automatiquement le jeu de caractères du texte fourni ou vous pourrez lui transmettre une liste.

Aussi, j'ai essayé de courir:

$text = "fiancée";
echo mb_convert_encoding($text, "UTF-8");
echo "<br/><br/>";
echo iconv(mb_detect_encoding($text), "UTF-8", $text);

et les résultats sont les mêmes pour les deux. Comment voyez-vous que votre texte est tronqué en "fiancé"? est-ce dans la base de données ou dans un navigateur?

11
Alexey Gerasimov

Il n'y a aucun moyen d'identifier le jeu de caractères d'une chaîne qui est complètement précis. Il existe des moyens d'essayer de deviner le jeu de caractères. Une de ces manières, et probablement/actuellement la meilleure en PHP, est mb_detect_encoding (). Cela va scanner votre chaîne et rechercher des occurrences de choses uniques à certains jeux de caractères. Selon votre chaîne, il se peut que de telles occurrences ne se distinguent pas.

Prenez le jeu de caractères ISO-8859-1 contre ISO-8859-15 ( http://en.wikipedia.org/wiki/ISO/IEC_8859-15#Changes_from_ISO-8859-1 )

Il n'y a qu'une poignée de caractères différents et, pour aggraver les choses, ils sont représentés par les mêmes octets. Il n'y a aucun moyen de détecter, étant donné qu'une chaîne ne sache pas qu'il est encodé, si l'octet 0xA4 est censé signifier ¤ ou € dans votre chaîne, il n'y a donc aucun moyen de connaître son jeu de caractères exact.

(Remarque: vous pouvez ajouter un facteur humain, ou une technique de numérisation encore plus avancée (par exemple, ce que suggère Oroboros102), pour essayer de déterminer, en fonction du contexte, si le caractère doit être ¤ ou €, bien que cela ressemble à un pont. trop loin)

Il y a des différences plus distinguables entre par exemple UTF-8 et ISO-8859-1, il est donc toujours intéressant d'essayer de le comprendre lorsque vous n'êtes pas sûr, même si vous pouvez et ne devez jamais vous fier à l'exactitude.

Lecture intéressante: http://kore-nordmann.de/blog/php_charset_encoding_FAQ.html#how-do-i-determine-the-charset-encoding-of-a-string

Cependant, il existe d'autres moyens de s'assurer du bon jeu de caractères. En ce qui concerne les formulaires, essayez d'appliquer UTF-8 autant que possible (vérifiez le bonhomme de neige pour vous assurer que votre soumission sera UTF-8 dans chaque navigateur: http://intertwingly.net/blog/2010/07/29/Rails-and-Snowmen ) Ceci étant fait, vous pouvez au moins être sûr que chaque texte soumis via vos formulaires est bien utf_8. En ce qui concerne les fichiers téléchargés, essayez d’exécuter la commande unix 'file -i' dessus, par exemple. exec () (si possible sur votre serveur) pour faciliter la détection (à l'aide de la nomenclature du document). Concernant le nettoyage des données, vous pouvez lire les en-têtes HTTP, qui spécifient généralement le jeu de caractères. Lors de l'analyse de fichiers XML, vérifiez si les métadonnées XML contiennent une définition de jeu de caractères.

Plutôt que d'essayer de deviner automatiquement le jeu de caractères, vous devez d'abord essayer de vous assurer si possible d'un jeu de caractères vous-même, ou essayer de saisir une définition de la source d'où vous la tirez (le cas échéant) avant de recourir à la détection.

5
matthiasmullie

Le principal problème pour moi est que je ne sais pas quel encodage sera la source d'une chaîne - cela pourrait provenir d'une zone de texte (l'utilisation n'est utile que si l'utilisateur est effectivement soumis le formulaire), ou peut-être à partir d'un fichier texte téléchargé, donc je n'ai vraiment aucun contrôle sur l'entrée.

Je ne pense pas que ce soit un problème. Une application connaît la source de l'entrée. S'il s'agit d'un formulaire, utilisez le codage UTF-8 dans votre cas. Ça marche. Il suffit de vérifier que les données fournies sont correctement codées (validation). Gardez à l'esprit que toutes les bases de données ne prennent pas en charge UTF-8 dans sa gamme complète.

S'il s'agit d'un fichier, vous ne l'enregistrerez pas au format UTF-8 codé dans la base de données, mais sous forme binaire. Lorsque vous relancez le fichier, utilisez également une sortie binaire, qui est alors totalement transparente.

Votre idée est bien qu'un utilisateur puisse dire le codage, même s'il peut le savoir après le téléchargement du fichier, car il est binaire.

Donc, je dois admettre que je ne vois pas de problème spécifique que vous soulevez avec votre question. Mais peut-être pourriez-vous ajouter plus de détails sur votre problème.

2
hakre

Il existe de très bonnes réponses et tente de répondre à votre question ici. Je ne suis pas un maître d'encodage, mais je comprends votre désir de disposer d'une pile pure UTF-8 tout au long de votre base de données. J'utilise le codage utf8mb4 de MySQL pour les tables, les champs et les connexions.

Ma situation se résumait comme suit: "Je veux juste que mes désinfectants, mes validateurs, ma logique métier et mes instructions préparées traitent de UTF-8 lorsque les données proviennent de formulaires HTML ou de liens d’enregistrement par courrier électronique". Donc, de manière simple, j'ai commencé avec cette idée:

  1. Tentative de détection du codage: $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];
  2. Si le codage ne peut pas être détecté, throw new RuntimeException
  3. Si l'entrée est UTF-8, continuez.
  4. Sinon, si c'est ISO-8859-1 ou ASCII

    une. Tentative de conversion en UTF-8 (attendez, pas terminé)

    b. Détecter le codage de la valeur convertie

    c. Si le codage rapporté et la valeur convertie sont tous deux UTF-8, continuez.

    ré. Sinon, throw new RuntimeException

De ma classe abstraite Sanitizer

Sanitizer

    private function isUTF8($encoding, $value)
    {
        return (($encoding === 'UTF-8') && (utf8_encode(utf8_decode($value)) === $value));
    }

    private function utf8tify(&$value)
    {
        $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];

        mb_internal_encoding('UTF-8');
        mb_substitute_character(0xfffd); //REPLACEMENT CHARACTER
        mb_detect_order($encodings);

        $stringEncoding = mb_detect_encoding($value, $encodings, true);

        if (!$stringEncoding) {
            $value = null;
            throw new \RuntimeException("Unable to identify character encoding in sanitizer.");
        }

        if ($this->isUTF8($stringEncoding, $value)) {
            return;
        } else {
            $value = mb_convert_encoding($value, 'UTF-8', $stringEncoding);
            $stringEncoding = mb_detect_encoding($value, $encodings, true);

            if ($this->isUTF8($stringEncoding, $value)) {
                return;
            } else {
                $value = null;
                throw new \RuntimeException("Unable to convert character encoding from ISO-8859-1, or ASCII, to UTF-8 in Sanitizer.");
            }
        }

        return;
    }

On pourrait faire valoir que je devrais séparer les problèmes d’encodage de ma classe abstraite Sanitizer et simplement injecter un objet Encoder dans un instance concrète enfant de Sanitizer. Cependant, le principal problème de mon approche est que, sans plus de connaissances, je rejette simplement les types de codage que je ne souhaite pas (et je me base sur les fonctions PHP mb_ *). Sans autre étude, je ne peux pas savoir si cela fait mal à certaines populations ou pas (ou si je perds des informations importantes). Donc, j'ai besoin d'en savoir plus. J'ai trouvé cet article.

Ce que chaque programmeur a absolument besoin de savoir sur les codages et les jeux de caractères pour travailler avec du texte

De plus, que se passe-t-il lorsque des données cryptées sont ajoutées à mes liens d'inscription de courrier électronique (à l'aide de OpenSSL ou mcrypt)? Cela pourrait-il interférer avec le décodage? Qu'en est-il de Windows-1252? Qu'en est-il des implications pour la sécurité? L'utilisation de utf8_decode() et utf8_encode() dans Sanitizer::isUTF8 est douteuse.

Des personnes ont signalé des défauts dans les fonctions PHP mb_ *. Je n’ai jamais pris le temps d’enquêter sur iconv, mais si cela fonctionne mieux que mb_ *, faites-le moi savoir.

2
Anthony Rutledge

Si vous êtes prêt à "porter ceci à la console", je recommanderais enca. Contrairement au mb_detect_encoding plutôt simpliste, il utilise "un mélange d'analyse syntaxique, d'analyse statistique, de devinettes et de magie noire pour déterminer leurs codages" (lol - voir page de manuel ). Cependant, vous devez généralement transmettre la langue du fichier d'entrée si vous souhaitez détecter de tels encodages spécifiques à un pays. (Cependant, mb_detect_encoding a essentiellement les mêmes exigences, car le codage devrait apparaître "au bon endroit" dans la liste des codages transmis pour qu'il soit détectable.)

enca est aussi venu ici: Comment trouver le codage d'un fichier sous Unix via un script

1
wutz

Vous pouvez configurer un ensemble de métriques pour essayer de deviner quel encodage est utilisé. Encore une fois, pas parfait, mais pourrait attraper quelques-unes des erreurs de mb_detect_encoding ().

1
Parris Varney

Il semble que votre question soit assez bien résolue, mais j'ai une approche qui peut vous simplifier la tâche:

J'ai eu un problème similaire en essayant de renvoyer des données de chaîne à partir de mysql, même en configurant à la fois la base de données et php pour renvoyer des chaînes au format utf-8. La seule façon pour laquelle j'ai eu l'erreur était de les renvoyer de la base de données.

Enfin, en naviguant sur le Web, j’ai trouvé un moyen très simple de gérer ce problème:

Étant donné que vous pouvez enregistrer tous ces types de données de chaîne dans votre mysql dans différents formats et classements, il vous suffit de définir, au niveau de votre fichier de connexion php, le classement à utf-8, comme ceci:

$connection = new mysqli($server, $user, $pass, $db);
$connection->set_charset("utf8");

Cela signifie que vous enregistrez d’abord les données dans n’importe quel format ou classement et que vous ne les convertissez qu’au retour dans votre fichier php.

J'espère que c'était utile!

0
Quel Pino