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

146

J'ai une application qui traite des clients du monde entier et, naturellement, je veux 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 sera le codage de la source d'une chaîne - cela pourrait provenir d'une zone de texte (l'utilisation <form accept-charset="utf-8">n'est utile que si l'utilisateur est effectivement soumis au formulaire), ou cela pourrait être à partir 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 m'assure que les éléments entrant dans ma base de données sont, dans la mesure du possible, encodés en UTF-8. J'ai essayé iconv(mb_detect_encoding($text), "UTF-8", $text); mais cela pose des problèmes (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 l'idée de demander à l'utilisateur final de spécifier l'encodage qu'il utilise et de lui montrer des aperçus de ce à quoi ressemblera la sortie, mais cela n'aide pas contre les pirates malveillants (en fait, cela pourrait leur rendre la vie un peu plus facile).

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

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

Sinistre...
la source
5
Il n'est fondamentalement pas possible par définition d'être absolument correct, en réalité le taux de réussite de deviner un encodage inconnu n'est pas terrible. Il est possible d'utiliser l'heuristique, mais elle sera correcte moins de 100% du temps, selon le matériau bien moins de 100%. Vous devez en être conscient. Peut-être que quelqu'un ici peut au moins suggérer une bibliothèque avec de bonnes heuristiques.
deceze
Bien sûr, je sais qu'il n'y a pas de solution parfaite - d'où le désir de quelque chose qui va au moins bien aller.
Grim ...
cela pourrait aider: stackoverflow.com/q/505562/642173
Melsi
Avez-vous essayé d'utiliser UTF-8//IGNOREcomme 2ème paramètre iconv?
feu
Ouais, c'est ce que j'ai fini par faire. Pas parfait, évidemment, car alors «fiancée» devient «fiancé», mais c'est certainement mieux. Pourquoi TRANSLIT ne fonctionne pas?
Grim ...

Réponses:

255

Ce que vous demandez est extrêmement difficile. Si possible, il est préférable que l'utilisateur spécifie l'encodage. Prévenir une attaque ne devrait pas être beaucoup plus facile ou plus difficile de cette façon.

Cependant, vous pouvez essayer de faire ceci:

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

Le définir sur strict peut vous aider à obtenir un meilleur résultat.

Jeff Day
la source
5
Veuillez jeter un œil au mb_detect_encodingcode source dans votre distribution php (quelque part ici: ext / mbstring / libmbfl / mbfl / mbfl_ident.c). Cette fonction ne fonctionne pas du tout correctement. Pour certains encodages, il a même "return true", lol. D'autres sont dans les fonctions Ctrl + c Ctrl + v. C'est parce que vous ne pouvez pas détecter l'encodage sans une sorte de dictionnaire ou une approche statistique (comme la mienne).
Oroboros102
1
La façon dont je le comprends, mb_detect_encodingparcourt la liste des encodages fournis et accepte le premier qui n'a pas de séquences d'octets invalides dans la chaîne ... Pour les codages qui n'ont pas de séquences d'octets invalides telles que ISO-8859-1, c'est toujours vrai . Aucune heuristique «intelligente», et les résultats varient considérablement avec la liste (et l'ordre) des encodages que vous passez.
wutz
Cela semble fonctionner pour moi. Mes utilisateurs soumettaient du texte sur une page utf8 avec tinymce, mais pour une raison inconnue, des caractères non utf8 se retrouvaient parfois dans la base de données. Cela a réglé le problème, alors merci beaucoup.
giorgio79
@Jeff Day - Merci pour cela. Pardonnez mon ignorance, que voulez-vous dire par «le mettre à la stricte»?
Ash501 du
[Jeff Day] envoie mb_detect_order()même s'il s'agit de la valeur par défaut de ce paramètre, car il voulait définir la détection d'encodage stricte sur true (le 3ème paramètre) :)
jave.web
28

En Russie mère patrie, 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 codes 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 encodages inconnus est de travailler avec des probabilités. Donc, nous ne voulons pas répondre à la question "qu'est-ce que le codage de ce texte?", Nous essayons de comprendre " quel est le codage le plus probable de ce texte? ".

Un gars ici dans un blog technologique russe populaire a inventé cette approche:

Créez la plage de probabilité des codes char dans chaque encodage que vous souhaitez prendre en charge. Vous pouvez le construire en utilisant de gros textes dans votre langue (par exemple de la fiction, utilisez Shakespeare pour l'anglais et Tolstoï pour le russe, lol). Vous obtiendrez 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 dans un codage inconnu et pour chaque codage de votre "dictionnaire de probabilité", vous recherchez la fréquence de chaque symbole dans un texte codé inconnu. Somme des probabilités des symboles. L'encodage avec une note plus élevée est probablement le gagnant. De meilleurs résultats pour des textes plus volumineux.

Si vous êtes intéressé , je peux volontiers vous aider 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 ne fonctionne certainement pas. Oui, pas du tout. Veuillez jeter un œil au code source de mb_detect_encoding dans "ext / mbstring / libmbfl / mbfl / mbfl_ident.c".

Oroboros102
la source
11

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

Aussi, j'ai essayé d'exécuter:

$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?

Alexey Gerasimov
la source
Dans la base de données, il semble que je viens d'essayer votre code et je suis d'accord.
Grim ...
1
Vérifiez que le classement que vous avez défini sur la table / colonne est également UTF-8.
Alexey Gerasimov
@AlexeyGerasimov Je suppose que j'ai vraiment besoin d'enquêter iconv. J'ai essayé de faire une méthode mb_ * presque pure. Que pensez-vous?
Anthony Rutledge
5

Il n'y a aucun moyen d'identifier le jeu de caractères d'une chaîne qui soit complètement précis. Il existe des moyens d'essayer de deviner le jeu de caractères. Une de ces méthodes, et probablement / actuellement la meilleure en PHP, est mb_detect_encoding (). Cela analysera votre chaîne et recherchera des occurrences de trucs uniques à certains jeux de caractères. En fonction de votre chaîne, il se peut qu'il n'y ait pas de telles occurrences distinctes.

Prenez le jeu de caractères ISO-8859-1 vs 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, recevoir une chaîne sans connaître son encodage, si l'octet 0xA4 est censé signifier ¤ ou € dans votre chaîne, il n'y a donc aucun moyen de savoir que c'est le 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 environnant, si le personnage doit être ¤ ou €, bien que cela semble être un pont trop loin)

Il y a des différences plus distinctes entre, par exemple, UTF-8 et ISO-8859-1, donc il vaut toujours la peine d'essayer de le comprendre lorsque vous n'êtes pas sûr, bien que vous ne puissiez et ne devriez jamais vous fier à son exactitude.

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

Il existe cependant d'autres moyens de garantir le jeu de caractères correct. En ce qui concerne les formulaires, essayez d'appliquer UTF-8 autant que possible (consultez 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 ) Cela étant fait, au moins vous pouvez être sûr que chaque texte soumis via vos formulaires est utf_8. Concernant les fichiers téléchargés, essayez d'exécuter la commande unix 'file -i' dessus via par exemple exec () (si possible sur votre serveur) pour faciliter la détection (en utilisant la nomenclature du document.) Concernant les données de scraping, vous pouvez lire les en-têtes HTTP, qui spécifient généralement le jeu de caractères. Lors de l'analyse des 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 devriez d'abord essayer de vous assurer un certain jeu de caractères dans la mesure du possible, ou essayer de saisir une définition de la source à partir de laquelle vous l'obtenez (le cas échéant) avant de recourir à la détection.

Matthiasmullie
la source
Formulaires et liens d'inscription par e-mail avec des données cryptées. C'est là que j'essaie de faire en sorte que mon entrée soit UTF-8 ou rien. Que pensez-vous de ma réponse? Les commentaires utiles sont appréciés. Merci.
Anthony Rutledge
3

Il y a de très bonnes réponses et tentatives pour répondre à votre question ici. Je ne suis pas un maître d'encodage, mais je comprends votre désir d'avoir une pile UTF-8 pure jusqu'à votre base de données. J'utilise l' utf8mb4encodage de MySQL pour les tables, les champs et les connexions.

Ma situation se résumait à "Je veux juste que mes désinfectants, mes validateurs, ma logique métier et mes déclarations préparées traitent UTF-8 lorsque les données proviennent de formulaires HTML ou de liens d'enregistrement de courrier électronique." Donc, à ma 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 l'encodage 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-1ouASCII

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

    b. Détecter le codage de la valeur convertie

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

    ré. Autre,throw new RuntimeException

De ma classe abstraite Sanitizer

Désinfectant

    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 préoccupations d'encodage de ma Sanitizerclasse abstraite et simplement injecter un Encoderobjet dans une instance enfant concrète de Sanitizer. Cependant, le principal problème avec mon approche est que, sans plus de connaissances, je rejette simplement les types d'encodage que je ne veux pas (et je me fie aux fonctions PHP mb_ *). Sans une étude plus approfondie, je ne peux pas savoir si cela fait mal ou non à certaines populations (ou si je perds des informations importantes). Alors, j'ai besoin d'en savoir plus. J'ai trouvé cet article.

Ce que tout programmeur a absolument besoin de savoir sur les encodages 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'enregistrement de courrier électronique (en utilisant OpenSSLou mcrypt)? Cela pourrait-il interférer avec le décodage? Qu'en est-il de Windows-1252? Qu'en est-il des implications en matière de sécurité? L'utilisation de utf8_decode()et utf8_encode()dans Sanitizer::isUTF8est douteuse.

Les gens ont signalé des lacunes dans les fonctions PHP mb_ *. Je n'ai jamais pris le temps d'enquêter iconv, mais si cela fonctionne mieux que les fonctions mb_ *, faites-le moi savoir.

Anthony Rutledge
la source
J'ai trouvé ceci, stackoverflow.com/a/3521396/1429677 excellente réponse à ce problème, voici la lib github.com/neitanod/forceutf8
Llewellyn
2

Le principal problème pour moi est que je ne sais pas quel sera le codage de 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 au formulaire), ou cela pourrait ê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 l'encodage UTF-8 dans votre cas. Ça marche. Vérifiez simplement que les données fournies sont correctement encodées (validation). Gardez à l'esprit que toutes les bases de données ne prennent pas en charge UTF-8 dans toute sa gamme.

Si c'est un fichier, vous ne l'enregistrez pas en UTF-8 encodé dans la base de données mais sous forme binaire. Lorsque vous sortez à nouveau le fichier, utilisez également la sortie binaire, cela est totalement transparent.

Votre idée est bien qu'un utilisateur puisse dire l'encodage, qu'il / elle puisse le dire de toute façon après le téléchargement du fichier, car il est binaire.

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

hakre
la source
Voudriez-vous voir et contester ma réponse? Les commentaires constructifs sont appréciés. Merci.
Anthony Rutledge
1

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

Parris Varney
la source
Oui, en parlant de mb_detect_encoding()ratés, pensez-vous que ma réponse a une chance de boule de neige en été au Sahara?
Anthony Rutledge
1

Si vous êtes prêt à "apporter ceci à la console", je vous le recommande enca. Contrairement à ce qui est plutôt simpliste mb_detect_encoding, il utilise "un mélange d'analyse, d'analyse statistique, de devinettes et de magie noire pour déterminer leurs encodages" (lol - voir la page de manuel ). Cependant, vous devez généralement transmettre la langue du fichier d'entrée si vous souhaitez détecter ces codages spécifiques au pays. (Cependant, a mb_detect_encodingessentiellement la même exigence, car le codage devrait apparaître "à la bonne place" dans la liste des codages passés pour qu'il soit détectable du tout.)

encaest également venu ici: Comment trouver l'encodage d'un fichier sous Unix via un ou plusieurs scripts

wutz
la source
1

Il semble que votre question soit tout à fait répondue, mais j'ai une approche qui peut simplifier votre cas:

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

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

É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, directement dans votre fichier de connexion php, de définir le classement sur utf-8, comme ceci:

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

Ce qui signifie que vous enregistrez d'abord les données dans n'importe quel format ou collation et que vous ne les convertissez qu'au retour dans votre fichier php.

J'espère que cela a été utile!

Quel Pino
la source
-2
public function convertToUtf8($text) {
    if(!$this->html)
        $this->html = cURL('http://'.$this->url, array('timeout' => 15));

    $html = $this->html;
    preg_match('/<meta.*?charset=(|\")(.*?)("|\")/i', $html, $matches);

    $charset = $matches[2];

    if($charset)
        return mb_convert_encoding($text, 'UTF-8', $charset);
    else
        return $text;
}

Options par défaut de cURL:

curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);

J'ai essayé quelque chose comme ça. Ça m'a aidé. Si trouvé sur les informations du jeu de caractères méta, je convertis, sinon je ne fais rien.

littlealien
la source
euh, pouvez-vous vérifier votre fonction et corriger les variables?
Martin
Qu'est-ce que $ url? Qu'est-ce que $ html?
Martin