PHP: Converta qualquer string em UTF-8 sem conhecer o conjunto de caracteres original ou pelo menos tentar

146

Eu tenho um aplicativo que lida com clientes de todo o mundo e, naturalmente, quero que tudo que entra nos meus bancos de dados seja codificado em UTF-8.

O principal problema para mim é que não sei qual será a codificação da fonte de qualquer sequência - pode ser de uma caixa de texto (usar <form accept-charset="utf-8">é útil apenas se o usuário realmente enviar o formulário) ou pode ser de um arquivo de texto carregado, então eu realmente não tenho controle sobre a entrada.

O que eu preciso é de uma função ou classe que garanta que o material que entra no meu banco de dados seja, na medida do possível, codificado em UTF-8. Eu tentei, iconv(mb_detect_encoding($text), "UTF-8", $text); mas isso tem problemas (se a entrada for 'noiva', ela retorna 'noiva'). Eu tentei muitas coisas = /

Para uploads de arquivos, gosto da ideia de pedir ao usuário final que especifique a codificação que eles usam e mostro-lhes visualizações de como será a saída, mas isso não ajuda contra hackers desagradáveis ​​(na verdade, poderia dar vida a eles). um pouco mais fácil).

Eu li as outras perguntas do SO sobre o assunto, mas todas parecem ter diferenças sutis como "Preciso analisar feeds RSS" ou "Raspe dados de sites" (ou, na verdade, "Você não pode").

Mas deve haver algo que pelo menos tenha uma boa tentativa !

Grim ...
fonte
5
Basicamente, não é possível, por definição, ficar absolutamente correto; na realidade, a taxa de sucesso de adivinhar uma codificação desconhecida não é fantástica. É possível usar heurísticas, mas ela estará correta em menos de 100% das vezes, dependendo do material em muito menos de 100%. Você precisa estar ciente disso. Talvez alguém aqui possa pelo menos sugerir uma biblioteca com boas heurísticas.
deceze
Claro, eu sei que não há solução perfeita - daí o desejo por algo que terá pelo menos uma boa chance.
Grim ...
Isso pode ajudar: stackoverflow.com/q/505562/642173
Melsi
Você já tentou usar UTF-8//IGNOREcomo o 2º param iconv?
fogo
Sim, foi o que acabei fazendo. Obviamente não é perfeito, já que a 'noiva' se torna 'noivo', mas certamente é melhor. Como o TRANSLIT não funciona?
Grim ...

Respostas:

255

O que você está pedindo é extremamente difícil. Se possível, conseguir que o usuário especifique a codificação é o melhor. Prevenir um ataque não deve ser muito mais fácil ou mais difícil dessa maneira.

No entanto, você pode tentar fazer isso:

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

Configurá-lo para estrito pode ajudar a obter um resultado melhor.

Jeff Day
fonte
5
Por favor, dê uma olhada no mb_detect_encodingcódigo fonte em sua distribuição php (em algum lugar aqui: ext / mbstring / libmbfl / mbfl / mbfl_ident.c). Esta função não funciona corretamente. Para algumas codificações, ele ainda tem "return true", lol. Outros estão nas funções Ctrl + c Ctrl + v. Isso ocorre porque você não pode detectar a codificação sem algum tipo de dicionário ou abordagem estatística (como a minha).
precisa
1
mb_detect_encodingPelo que entendi, percorre a lista de codificações fornecidas e aceita a primeira que não possui sequências de bytes inválidas na string ... Para codificações que não possuem sequências de bytes inválidas, como ISO-8859-1, é sempre verdade . Nenhuma heurística "inteligente" e os resultados variam muito com a lista (e a ordem) das codificações que você passa.
Wutz
Isso parece estar funcionando para mim. Meus usuários estavam enviando texto em uma página utf8 com tinymce, mas, por algum motivo desconhecido, os caracteres não utf8 às vezes terminavam no banco de dados. Isso foi corrigido, então muito obrigado.
Giorgio79 # 13/12
@ Day Jeff - Obrigado por isso. Perdoe minha ignorância, o que você quer dizer com "Definindo isso como rigoroso"?
Ash501
[Jeff Day] está enviando mb_detect_order()mesmo que é o valor padrão para este parâmetro, porque ele queria para definir a detecção de codificação rigorosa para true (o 3º param) :)
jave.web
28

Na Rússia, pátria, temos quatro codificações populares, então sua pergunta é muito procurada aqui.

Somente pelos códigos de caracteres dos símbolos você não pode detectar a codificação, porque as páginas de código se cruzam. Algumas páginas de código em diferentes idiomas têm até uma interseção completa. Então, precisamos de outra abordagem .

A única maneira de trabalhar com codificações desconhecidas é trabalhar com probabilidades. Portanto, não queremos responder à pergunta "o que é codificação deste texto?", Estamos tentando entender "o que é a codificação mais provável desse texto? ".

Um cara aqui no popular blog de tecnologia russo inventou essa abordagem:

Crie o intervalo de probabilidade dos códigos de caracteres em todas as codificações que você deseja suportar. Você pode construí-lo usando alguns textos grandes no seu idioma (por exemplo, ficção, use Shakespeare para inglês e Tolstoi para russo, lol). Você ficará smth assim:

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

Próximo. Você pega o texto em codificação desconhecida e, para cada codificação no seu "dicionário de probabilidades", procura a frequência de cada símbolo no texto codificado em desconhecida. Soma probabilidades de símbolos. A codificação com uma classificação maior provavelmente é a vencedora. Melhores resultados para textos maiores.

Se você estiver interessado , posso ajudá-lo com prazer nessa tarefa. Podemos aumentar bastante a precisão criando uma lista de probabilidades de dois códigos.

Btw. mb_detect_encoding certanly não funciona. Sim mesmo. Por favor, dê uma olhada no código fonte mb_detect_encoding em "ext / mbstring / libmbfl / mbfl / mbfl_ident.c".

Oroboros102
fonte
11

Você provavelmente já tentou isso, mas por que não usar apenas a função mb_convert_encoding? Ele tentará detectar automaticamente o conjunto de caracteres do texto fornecido ou você pode passar uma lista para ele.

Além disso, tentei executar:

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

e os resultados são os mesmos para ambos. Como você vê que seu texto está truncado para 'noivo'? está no banco de dados ou em um navegador?

Alexey Gerasimov
fonte
Parece que no banco de dados - acabei de experimentar o seu código e concordo.
Grim ...
1
Verifique se o agrupamento que você definiu na tabela / coluna também é UTF-8.
Alexey Gerasimov
@AlexeyGerasimov Acho que realmente preciso investigar iconv. Eu tentei fazer um mb_ ​​* quase puro. O que você acha?
Anthony Rutledge
5

Não há como identificar o conjunto de caracteres de uma sequência que é completamente precisa. Existem maneiras de tentar adivinhar o conjunto de caracteres. Uma dessas maneiras, e provavelmente / atualmente a melhor em PHP, é mb_detect_encoding (). Isso examinará sua string e procurará ocorrências de itens exclusivos de determinados conjuntos de caracteres. Dependendo da sua sequência, pode não haver tais ocorrências distinguíveis.

Pegue o conjunto de caracteres ISO-8859-1 x ISO-8859-15 ( http://en.wikipedia.org/wiki/ISO/IEC_8859-15#Changes_from_ISO-8859-1 )

Existem apenas alguns caracteres diferentes e, para piorar, eles são representados pelos mesmos bytes. Não há como detectar, receber uma sequência sem saber a codificação, se o byte 0xA4 deve significar ¤ ou € na sua sequência, portanto não há como saber se é o conjunto de caracteres exato.

(Nota: você pode adicionar um fator humano, ou uma técnica de varredura ainda mais avançada (por exemplo, o que Oroboros102 sugere), para tentar descobrir com base no contexto circundante, se o personagem deve ser ¤ ou €, embora isso pareça uma ponte muito longe)

Existem diferenças mais distintas entre, por exemplo, UTF-8 e ISO-8859-1, por isso ainda vale a pena tentar descobrir quando não tiver certeza, embora você possa e nunca deva confiar que isso esteja correto.

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

Existem outras maneiras de garantir o conjunto de caracteres correto. Em relação aos formulários, tente aplicar o máximo possível o UTF-8 (confira o boneco de neve para garantir que seu envio seja UTF-8 em todos os navegadores: http://intertwingly.net/blog/2010/07/29/Rails-and -Bonecos de neve ) Ao fazer isso, pelo menos você pode ter certeza de que todo texto enviado através de seus formulários é utf_8. Em relação aos arquivos enviados, tente executar o comando unix 'file -i' nele, por exemplo, exec () (se possível no seu servidor) para ajudar na detecção (usando a lista técnica do documento). Em relação à raspagem de dados, você pode ler os cabeçalhos HTTP, que geralmente especificam o conjunto de caracteres. Ao analisar arquivos XML, verifique se os metadados XML contêm uma definição de conjunto de caracteres.

Em vez de tentar adivinhar automaticamente o conjunto de caracteres, você deve primeiro tentar garantir um determinado conjunto de caracteres sempre que possível, ou tentar obter uma definição da fonte de origem (se aplicável) antes de recorrer à detecção.

matthiasmullie
fonte
Formulários e links de registro de e-mail com dados criptografados. É aí que estou tentando fazer minha entrada ser UTF-8 ou nada. O que você acha da minha resposta? Comentários úteis são apreciados. Obrigado.
Anthony Rutledge
3

Existem algumas boas respostas e tentativas de responder sua pergunta aqui. Não sou um mestre de codificação, mas entendo seu desejo de ter uma pilha UTF-8 pura até o banco de dados. Eu tenho usado a utf8mb4codificação do MySQL para tabelas, campos e conexões.

Minha situação se resumia a "Eu só quero que meus desinfetantes, validadores, lógica de negócios e instruções preparadas lidem com o UTF-8 quando os dados vierem de formulários HTML ou links de registro de email". Então, da minha maneira simples, comecei com esta ideia:

  1. Tente detectar a codificação: $encodings = ['UTF-8', 'ISO-8859-1', 'ASCII'];
  2. Se a codificação não puder ser detectada, throw new RuntimeException
  3. Se houver entrada UTF-8, continue.
  4. Senão, se é ISO-8859-1ouASCII

    uma. Tentativa de conversão para UTF-8 (espera, não concluída)

    b. Detectar a codificação do valor convertido

    c. Se a codificação relatada e o valor convertido forem ambos UTF-8, continue.

    d. Outro,throw new RuntimeException

Da minha aula abstrata Sanitizer

Desinfetante

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

Alguém poderia argumentar que eu deveria separar as preocupações de codificação da minha Sanitizerclasse abstrata e simplesmente injetar um Encoderobjeto em uma instância filha concreta de Sanitizer. No entanto, o principal problema com a minha abordagem é que, sem mais conhecimento, simplesmente rejeito os tipos de codificação que não quero (e confio nas funções do PHP mb_ *). Sem um estudo mais aprofundado, não sei se isso machuca algumas populações ou não (ou se estou perdendo informações importantes). Então, eu preciso aprender mais. Encontrei este artigo.

O que todo programador precisa absolutamente e positivamente sobre codificações e conjuntos de caracteres para trabalhar com texto

Além disso, o que acontece quando dados criptografados são adicionados aos meus links de registro de e-mail (usando OpenSSLou mcrypt)? Isso poderia interferir na decodificação? E o Windows-1252? E as implicações de segurança? O uso de utf8_decode()e utf8_encode()em Sanitizer::isUTF8é duvidoso.

As pessoas apontaram falhas nas funções do PHP mb_ *. Nunca levei tempo para investigar iconv, mas se funcionar melhor que as funções mb_ *, informe-me.

Anthony Rutledge
fonte
Eu encontrei este, stackoverflow.com/a/3521396/1429677 excelente resposta a esta questão, aqui é o lib github.com/neitanod/forceutf8
Llewellyn
2

O principal problema para mim é que não sei qual será a codificação da fonte de qualquer sequência - pode ser de uma caixa de texto (usar é útil apenas se o usuário realmente enviar o formulário) ou pode ser de um arquivo de texto carregado, então eu realmente não tenho controle sobre a entrada.

Eu não acho que seja um problema. Um aplicativo conhece a fonte da entrada. Se for de um formulário, use a codificação UTF-8 no seu caso. Isso funciona. Basta verificar se os dados fornecidos estão codificados corretamente (validação). Lembre-se de que nem todos os bancos de dados oferecem suporte ao UTF-8 em toda a sua extensão.

Se for um arquivo, você não o salvará codificado em UTF-8 no banco de dados, mas em formato binário. Quando você produzir o arquivo novamente, use a saída binária também, então isso é totalmente transparente.

Sua idéia é boa de que um usuário possa dizer a codificação, seja ele mesmo assim após o download do arquivo, pois é binário.

Portanto, devo admitir que não vejo um problema específico que você levanta com sua pergunta. Mas talvez você possa adicionar mais alguns detalhes sobre qual é o seu problema.

hakre
fonte
Você veria e emitiria minha resposta? Comentários construtivos são apreciados. Obrigado.
Anthony Rutledge
1

Você pode configurar um conjunto de métricas para tentar adivinhar qual codificação está sendo usada. Novamente, não é perfeito, mas pode pegar algumas das falhas de mb_detect_encoding ().

Parris Varney
fonte
Sim, falando bem de mb_detect_encoding()erros, você acha que minha resposta tem uma chance de bola de neve no verão no Saara?
Anthony Rutledge
1

Se você estiver disposto a "levar isso para o console", eu recomendo enca. Diferente do bastante simplista mb_detect_encoding, ele usa "uma mistura de análise, análise estatística, adivinhação e magia negra para determinar suas codificações" (lol - veja a página de manual ). No entanto, você geralmente precisa passar o idioma do arquivo de entrada se quiser detectar essas codificações específicas do país. (No entanto, mb_detect_encodingpossui essencialmente o mesmo requisito, pois a codificação teria que aparecer "no lugar certo" na lista de codificações passadas para que seja detectável.)

encatambém veio aqui: Como encontrar a codificação de um arquivo no Unix via script (s)

wutz
fonte
1

Parece que sua pergunta foi bastante respondida, mas eu tenho uma abordagem que pode simplificar seu caso:

Eu tive um problema semelhante ao tentar retornar dados de string do mysql, até mesmo configurando o banco de dados e o php para retornar as strings formatadas em utf-8. A única maneira de obter o erro era realmente devolvê-los do banco de dados.

Finalmente, navegando pela web, encontrei uma maneira muito fácil de lidar com isso:

Dando que você pode salvar todos esses tipos de dados de string no mysql em diferentes formatos e agrupamentos, o que você só precisa fazer é, diretamente no seu arquivo de conexão php, definir o agrupamento para utf-8, assim:

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

O que significa que primeiro você salva os dados em qualquer formato ou agrupamento e os converte apenas no retorno ao seu arquivo php.

Espero que tenha sido útil!

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

Opções padrão de cURL:

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

Eu tentei algo assim. Isso me ajudou. Se encontrado nas informações de meta charset, estou convertendo, caso contrário, não faço nada.

littlealien
fonte
errr, você pode verificar sua função e corrigir as variáveis?
Martin
O que é $ url? O que é $ html?
Martin