PHP: Convierta cualquier cadena a UTF-8 sin conocer el conjunto de caracteres original, o al menos intente

146

Tengo una aplicación que trata con clientes de todo el mundo y, naturalmente, quiero que todo lo que ingrese a mis bases de datos esté codificado en UTF-8.

El principal problema para mí es que no sé qué codificación va a ser la fuente de cualquier cadena; podría ser desde un cuadro de texto (el uso <form accept-charset="utf-8">solo es útil si el usuario realmente envía el formulario), o podría ser de un archivo de texto cargado, por lo que realmente no tengo control sobre la entrada.

Lo que necesito es una función o clase que se asegure de que todo lo que vaya a mi base de datos esté, en la medida de lo posible, codificado en UTF-8. Lo he intentado iconv(mb_detect_encoding($text), "UTF-8", $text); pero eso tiene problemas (si la entrada es 'prometida', devuelve 'prometido'). He intentado muchas cosas = /

Para la carga de archivos, me gusta la idea de pedirle al usuario final que especifique la codificación que usan y les muestre vistas previas de cómo se verá la salida, pero esto no ayuda contra los piratas informáticos desagradables (de hecho, podría hacerles la vida un poco más fácil)

He leído las otras preguntas SO sobre el tema, pero parece que todas tienen diferencias sutiles como "Necesito analizar fuentes RSS" o "Raspar datos de sitios web" (o, de hecho, "No se puede").

¡Pero debe haber algo que al menos tenga un buen intento !

Severo...
fuente
55
Básicamente, no es posible, por definición, ser absolutamente correcto, en realidad, la tasa de éxito de adivinar una codificación desconocida no es excelente. Es posible usar heurística, pero será correcta menos del 100% del tiempo, dependiendo del material mucho menos del 100%. Necesitas ser consciente de eso. Quizás alguien aquí pueda al menos sugerir una biblioteca con buena heurística.
Fallecimiento
Claro, sé que no hay una solución perfecta, de ahí el deseo de algo que al menos tenga una buena oportunidad.
Grim ...
esto podría ayudar: stackoverflow.com/q/505562/642173
Melsi
¿Has intentado usar UTF-8//IGNOREcomo el segundo parámetro en iconv?
fuego el
Sí, eso es lo que terminé haciendo. No es perfecto, obviamente, ya que 'prometida' se convierte en 'prometida', pero ciertamente es mejor. ¿Cómo es que TRANSLIT no funciona?
Grim ...

Respuestas:

255

Lo que estás pidiendo es extremadamente difícil. Si es posible, lograr que el usuario especifique la codificación es lo mejor. Prevenir un ataque no debería ser mucho más fácil o más difícil de esa manera.

Sin embargo, podrías intentar hacer esto:

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

Establecerlo en estricto podría ayudarlo a obtener un mejor resultado.

Jeff Day
fuente
55
Por favor, eche un vistazo al mb_detect_encodingcódigo fuente en su distribución de php (en algún lugar aquí: ext / mbstring / libmbfl / mbfl / mbfl_ident.c). Esta función no funciona correctamente en absoluto. Para algunas codificaciones incluso tiene "return true", lol. Otros están en las funciones Ctrl + c Ctrl + v. Esto se debe a que no puede detectar la codificación sin algún tipo de diccionario o enfoque estadístico (como el mío).
Oroboros102
1
Según tengo entendido, mb_detect_encodingrevisa la lista de codificaciones suministradas y acepta la primera que no tiene secuencias de bytes no válidas en la cadena ... Para codificaciones que no tienen secuencias de bytes no válidas como ISO-8859-1, siempre es cierto . No hay heurísticas "inteligentes", y los resultados varían mucho con la lista (y el orden) de las codificaciones que pasa.
wutz
Esto parece estar funcionando para mí. Mis usuarios estaban enviando texto en una página utf8 con tinymce, sin embargo, por alguna razón desconocida, los caracteres no utf8 a veces terminaban en la base de datos. Esto lo solucionó, así que muchas gracias.
giorgio79
@Jeff Day - Gracias por esto. Perdón por mi ignorancia, ¿qué quieres decir con 'Ponerlo en estricto'?
Ash501
[Jeff Day] está enviando mb_detect_order()aunque es el valor predeterminado para este parámetro, porque quería establecer la detección de codificación estricta en verdadero (el tercer parámetro) :)
jave.web
28

En la patria de Rusia tenemos 4 codificaciones populares, por lo que su pregunta tiene una gran demanda aquí.

Solo mediante códigos de caracteres de símbolos no puede detectar la codificación, porque las páginas de códigos se cruzan. Algunas páginas de códigos en diferentes idiomas tienen incluso una intersección completa. Entonces, necesitamos otro enfoque .

La única forma de trabajar con codificaciones desconocidas es trabajar con probabilidades. Por lo tanto, no queremos responder la pregunta "¿qué es la codificación de este texto?", Estamos tratando de entender " ¿qué es lo más probable la codificación de este texto? ".

Un tipo aquí en el popular blog ruso de tecnología inventó este enfoque:

Cree el rango de probabilidad de los códigos char en cada codificación que desee admitir. Puedes construirlo usando algunos textos grandes en tu idioma (por ejemplo, algo de ficción, usa Shakespeare para inglés y Tolstoi para ruso, jajaja). Obtendrás algo así:

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

Próximo. Toma texto en codificación desconocida y para cada codificación en su "diccionario de probabilidad" busca la frecuencia de cada símbolo en texto codificado desconocido. Suma de probabilidades de símbolos. La codificación con mayor calificación es probablemente el ganador. Mejores resultados para textos más grandes.

Si está interesado , con gusto puedo ayudarlo con esta tarea. Podemos aumentar enormemente la precisión mediante la creación de una lista de probabilidades de dos códigos.

Por cierto. mb_detect_encoding certanly no funciona. Si, en absoluto. Por favor, eche un vistazo al código fuente mb_detect_encoding en "ext / mbstring / libmbfl / mbfl / mbfl_ident.c".

Oroboros102
fuente
11

Probablemente haya intentado esto, pero ¿por qué no usar la función mb_convert_encoding? Intentará detectar automáticamente el conjunto de caracteres del texto proporcionado o puede pasarle una lista.

Además, intenté ejecutar:

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

y los resultados son los mismos para ambos. ¿Cómo ves que tu texto está truncado a 'novio'? ¿Está en la base de datos o en un navegador?

Alexey Gerasimov
fuente
Parece que en la base de datos: acabo de probar su código y estoy de acuerdo.
Siniestro ...
1
Compruebe para asegurarse de que la clasificación que ha definido en la tabla / columna es también UTF-8.
Alexey Gerasimov
@AlexeyGerasimov Creo que realmente necesito investigar iconv. Traté de hacer una forma casi pura mb_ *. Que te parece
Anthony Rutledge
5

No hay forma de identificar el conjunto de caracteres de una cadena que es completamente precisa. Hay formas de tratar de adivinar el juego de caracteres. Una de estas formas, y probablemente / actualmente la mejor en PHP, es mb_detect_encoding (). Esto escaneará su cadena y buscará ocurrencias de cosas únicas para ciertos conjuntos de caracteres. Dependiendo de su cadena, puede que no haya tales eventos distinguibles.

Tome el juego de caracteres ISO-8859-1 vs ISO-8859-15 ( http://en.wikipedia.org/wiki/ISO/IEC_8859-15#Changes_from_ISO-8859-1 )

Solo hay un puñado de caracteres diferentes, y para empeorar, están representados por los mismos bytes. No hay forma de detectar, recibir una cadena sin saber que está codificando, ya sea que el byte 0xA4 signifique ¤ o € en su cadena, por lo que no hay forma de saber si es un juego de caracteres exacto.

(Nota: podría agregar un factor humano, o una técnica de escaneo aún más avanzada (por ejemplo, lo que sugiere Oroboros102), para tratar de averiguar en función del contexto circundante, si el personaje debe ser ¤ o €, aunque esto parece un puente Muy lejos)

Hay diferencias más distinguibles entre, por ejemplo, UTF-8 e ISO-8859-1, por lo que vale la pena intentar resolverlo cuando no estás seguro, aunque puedes y nunca debes confiar en que sea correcto.

Lectura interesante: http://kore-nordmann.de/blog/php_charset_encoding_FAQ.html#how-do-i-determine-the-charset-encoding-of-a-string

Sin embargo, hay otras formas de garantizar el juego de caracteres correcto. Con respecto a los formularios, intente aplicar UTF-8 tanto como sea posible (consulte muñeco de nieve para asegurarse de que su envío será UTF-8 en cada navegador: http://intertwingly.net/blog/2010/07/29/Rails-and -Snowmen ) Una vez hecho esto, al menos puede estar seguro de que cada texto enviado a través de sus formularios es utf_8. Con respecto a los archivos cargados, intente ejecutar el comando 'file -i' de unix a través de, por ejemplo, exec () (si es posible en su servidor) para ayudar a la detección (usando la lista de materiales del documento). En cuanto a los datos de raspado, puede leer los encabezados HTTP, que generalmente especifican el juego de caracteres. Al analizar archivos XML, vea si los metadatos XML contienen una definición de conjunto de caracteres.

En lugar de tratar de adivinar automáticamente el conjunto de caracteres, primero debe intentar asegurarse de que un cierto conjunto de caracteres usted mismo sea posible, o tratar de obtener una definición de la fuente de la que lo obtiene (si corresponde) antes de recurrir a la detección.

matthiasmullie
fuente
Formularios y enlaces de registro de correo electrónico con datos cifrados. Ahí es donde estoy tratando de hacer que mi entrada sea UTF-8 o nada. ¿Qué opinas de mi respuesta? Se agradecen los comentarios útiles. Gracias.
Anthony Rutledge
3

Hay algunas respuestas realmente buenas e intenta responder a su pregunta aquí. No soy un maestro de codificación, pero entiendo su deseo de tener una pila UTF-8 pura hasta su base de datos. He estado usando la utf8mb4codificación de MySQL para tablas, campos y conexiones.

Mi situación se redujo a "Solo quiero que mis desinfectantes, validadores, lógica de negocios y declaraciones preparadas se ocupen de UTF-8 cuando los datos provienen de formularios HTML o enlaces de registro por correo electrónico". Entonces, de manera simple, comencé con esta idea:

  1. Intenta detectar la codificación: $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];
  2. Si no se puede detectar la codificación, throw new RuntimeException
  3. Si la entrada es UTF-8, continúe.
  4. De lo contrario, si es ISO-8859-1oASCII

    a. Intento de conversión a UTF-8 (espera, no terminado)

    si. Detectar la codificación del valor convertido.

    C. Si la codificación informada y el valor convertido son ambos UTF-8, continúe.

    re. Más,throw new RuntimeException

De mi clase abstracta Sanitizer

Desinfectante

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

Se podría argumentar que debería separar las preocupaciones de codificación de mi Sanitizerclase abstracta y simplemente inyectar un Encoderobjeto en una instancia secundaria concreta de Sanitizer. Sin embargo, el principal problema con mi enfoque es que, sin más conocimiento, simplemente rechazo los tipos de codificación que no quiero (y confío en las funciones PHP mb_ *). Sin más estudios, no puedo saber si eso perjudica a algunas poblaciones o no (o si estoy perdiendo información importante). Entonces, necesito aprender más. Encontré este artículo.

Lo que todo programador debe saber de manera absoluta y positiva sobre las codificaciones y los conjuntos de caracteres para trabajar con texto

Además, ¿qué sucede cuando se agregan datos cifrados a mis enlaces de registro de correo electrónico (usando OpenSSLo mcrypt)? ¿Podría esto interferir con la decodificación? ¿Qué pasa con Windows-1252? ¿Qué pasa con las implicaciones de seguridad? El uso de utf8_decode()y utf8_encode()en Sanitizer::isUTF8es dudoso.

La gente ha señalado fallas en las funciones PHP mb_ *. Nunca me tomé el tiempo para investigar iconv, pero si funciona mejor que las funciones mb_ *, avíseme.

Anthony Rutledge
fuente
Encontré esto, stackoverflow.com/a/3521396/1429677 excelente respuesta a este problema, aquí está la lib github.com/neitanod/forceutf8
Llewellyn
2

El principal problema para mí es que no sé qué codificación va a ser la fuente de cualquier cadena; podría ser desde un cuadro de texto (el uso solo es útil si el usuario realmente envía el formulario), o podría ser de un archivo de texto cargado, por lo que realmente no tengo control sobre la entrada.

No creo que sea un problema. Una aplicación conoce la fuente de la entrada. Si es de un formulario, use la codificación UTF-8 en su caso. Eso funciona. Simplemente verifique que los datos proporcionados estén codificados correctamente (validación). Tenga en cuenta que no todas las bases de datos admiten UTF-8 en su rango completo.

Si es un archivo, no lo guardará codificado en UTF-8 en la base de datos sino en forma binaria. Cuando vuelva a generar el archivo, use también la salida binaria, entonces esto es totalmente transparente.

Su idea es buena para que un usuario pueda decir la codificación, ya que él / ella puede decirlo de todos modos después de descargar el archivo, ya que es binario.

Así que debo admitir que no veo un problema específico que plantees con tu pregunta. Pero tal vez pueda agregar más detalles sobre cuál es su problema.

hakre
fuente
¿Verías y emitirías mi respuesta? Los comentarios constructivos son apreciados. Gracias.
Anthony Rutledge
1

Puede configurar un conjunto de métricas para tratar de adivinar qué codificación se está utilizando. Nuevamente, no es perfecto, pero podría detectar algunos de los errores de mb_detect_encoding ().

Parris Varney
fuente
Sí, hablando de mb_detect_encoding()fallas, ¿crees que mi respuesta tiene una posibilidad de bola de nieve en verano en el Sahara?
Anthony Rutledge
1

Si está dispuesto a "llevar esto a la consola", lo recomendaría enca. A diferencia del más bien simplista mb_detect_encoding, utiliza "una mezcla de análisis estadístico, análisis estadístico, adivinanzas y magia negra para determinar sus codificaciones" (risas, vea la página del manual ). Sin embargo, generalmente debe pasar el idioma del archivo de entrada si desea detectar tales codificaciones específicas del país. (Sin embargo, mb_detect_encodingesencialmente tiene el mismo requisito, ya que la codificación debería aparecer "en el lugar correcto" en la lista de codificaciones aprobadas para que sea detectable).

encatambién apareció aquí: Cómo encontrar la codificación de un archivo en Unix a través de script (s)

wutz
fuente
1

Parece que su pregunta está bastante respondida, pero tengo un enfoque que puede simplificar su caso:

Tuve un problema similar al intentar devolver datos de cadena de mysql, incluso configurando tanto la base de datos como php para devolver cadenas formateadas a utf-8. La única forma en que obtuve el error fue en realidad devolviéndolos de la base de datos.

Finalmente, navegando por la web encontré una manera realmente fácil de manejarlo:

Dado que puede guardar todos esos tipos de datos de cadena en su mysql en diferentes formatos y colaciones, lo que solo necesita hacer es, directamente en su archivo de conexión php, establecer la colación en utf-8, así:

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

Lo que significa que primero guarda los datos en cualquier formato o clasificación y los convierte solo al regresar a su archivo php.

¡Espero que haya sido útil!

Quel Pino
fuente
-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;
}

opciones predeterminadas de cURL:

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

Intenté algo como esto. Me ayudó. Si se encuentra en la información del conjunto de caracteres meta, estoy convirtiendo, de lo contrario no haré nada.

littlealien
fuente
errr, ¿puede verificar su función y corregir las variables?
Martin
¿Qué es $ url? ¿Qué es $ html?
Martin