ruby 1.9: séquence d'octets invalide en UTF-8

109

J'écris un robot d'exploration en Ruby (1.9) qui consomme beaucoup de HTML provenant de nombreux sites aléatoires.
En essayant d'extraire des liens, j'ai décidé d'utiliser simplement à la .scan(/href="(.*?)"/i)place de nokogiri / hpricot (accélération majeure). Le problème est que je reçois maintenant beaucoup d' invalid byte sequence in UTF-8erreurs " ".
D'après ce que j'ai compris, la net/httpbibliothèque n'a pas d'options spécifiques d'encodage et les éléments qui y sont fournis ne sont fondamentalement pas correctement étiquetés.
Quelle serait la meilleure façon de travailler réellement avec ces données entrantes? J'ai essayé .encodeavec l'ensemble d'options de remplacement et non valide, mais sans succès jusqu'à présent ...

Marc Seeger
la source
quelque chose qui peut casser des caractères, mais garde la chaîne valide pour d'autres bibliothèques: valid_string = untrusted_string.unpack ('C *'). pack ('U *')
Marc Seeger
Ayant le problème exact, j'ai essayé les mêmes autres solutions. Pas d'amour. J'ai essayé celui de Marc, mais il semble tout brouiller. Êtes-vous sûr d' 'U*'annuler 'C*'?
Jordan Feldstein
Non, ça ne marche pas :) Je viens de l'utiliser dans un webcrawler où je me soucie des bibliothèques tierces qui ne plantent pas plus que je ne le fais à propos d'une phrase ici et là.
Marc Seeger

Réponses:

172

Dans Ruby 1.9.3, il est possible d'utiliser String.encode pour "ignorer" les séquences UTF-8 invalides. Voici un extrait de code qui fonctionnera à la fois en 1.8 ( iconv ) et en 1.9 ( String # encode ):

require 'iconv' unless String.method_defined?(:encode)
if String.method_defined?(:encode)
  file_contents.encode!('UTF-8', 'UTF-8', :invalid => :replace)
else
  ic = Iconv.new('UTF-8', 'UTF-8//IGNORE')
  file_contents = ic.iconv(file_contents)
end

ou si vous avez une entrée vraiment gênante, vous pouvez faire une double conversion de UTF-8 à UTF-16 et revenir à UTF-8:

require 'iconv' unless String.method_defined?(:encode)
if String.method_defined?(:encode)
  file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
  file_contents.encode!('UTF-8', 'UTF-16')
else
  ic = Iconv.new('UTF-8', 'UTF-8//IGNORE')
  file_contents = ic.iconv(file_contents)
end
RubenLaguna
la source
3
Avec une entrée problématique, j'utilise également une double conversion de UTF-8 à UTF-16, puis de retour à UTF-8 file_contents.encode!('UTF-16', 'UTF-8', :invalid => :replace, :replace => '') file_contents.encode!('UTF-8', 'UTF-16')
RubenLaguna
7
Il existe également l'option de force_encoding. Si vous avez lu un ISO8859-1 comme UTF-8 (et que cette chaîne contient donc un UTF-8 invalide), vous pouvez le "réinterpréter" comme ISO8859-1 avec the_string.force_encoding ("ISO8859-1") et travailler simplement avec cette chaîne dans son codage réel.
RubenLaguna
3
Cette astuce de double encodage vient de sauver mon bacon! Je me demande pourquoi il est nécessaire?
johnf
1
Où dois-je mettre ces lignes?
Lefsler
5
Je pense que la double conversion fonctionne car elle force une conversion d'encodage (et avec elle la vérification des caractères invalides). Si la chaîne source est déjà encodée en UTF-8, alors le simple appel .encode('UTF-8')est un no-op et aucune vérification n'est exécutée. Documentation Ruby Core pour encoder . Cependant, le convertir en UTF-16 force d'abord toutes les vérifications de séquences d'octets non valides à être exécutées, et les remplacements sont effectués si nécessaire.
Jo Hund du
79

La réponse acceptée ni l'autre réponse fonctionnent pour moi. J'ai trouvé ce post qui suggérait

string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')

Cela a résolu le problème pour moi.

Amir Raminfar
la source
1
Cela a résolu le problème pour moi et j'aime utiliser des méthodes non obsolètes (j'ai maintenant Ruby 2.0).
La-comadreja
1
Celui-ci est le seul qui fonctionne! J'ai essayé toutes les solutions ci-dessus, aucune d'elles ne fonctionne Chaîne utilisée pour tester "fdsfdsf dfsf sfds fs sdf <div> bonjour <p> fooo ??? {! @ # $% ^ & * () _ +} < / p> </div> \ xEF \ xBF \ xBD \ xef \ xbf \ x9c <div> \ xc2 \ x90 </div> \ xc2 \ x90 "
Chihung Yu
1
À quoi sert le deuxième argument «binaire»?
Henley Chiu
24

Ma solution actuelle est d'exécuter:

my_string.unpack("C*").pack("U*")

Cela éliminera au moins les exceptions qui étaient mon principal problème

Marc Seeger
la source
3
J'utilise cette méthode en combinaison avec valid_encoding?qui semble détecter quand quelque chose ne va pas. val.unpack('C*').pack('U*') if !val.valid_encoding?.
Aaron Gibralter
Celui-ci a fonctionné pour moi. Convertit avec succès mon \xB0dos en symboles de degrés. Même le valid_encoding?revient vrai , mais je vérifie toujours si elle ne fonctionne pas et les caractères dépouilleront offensants à l' aide de la réponse de Amir ci - dessus: string.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ''). J'avais également essayé l' force_encodingitinéraire mais cela a échoué.
hamstar
C'est bien. Merci.
d_ethier
8

Essaye ça:

def to_utf8(str)
  str = str.force_encoding('UTF-8')
  return str if str.valid_encoding?
  str.encode("UTF-8", 'binary', invalid: :replace, undef: :replace, replace: '')
end
Ranjithkumar Ravi
la source
Meilleure réponse pour mon cas! Merci
Aldo
4

Je vous recommande d'utiliser un analyseur HTML. Trouvez simplement le plus rapide.

L'analyse HTML n'est pas aussi simple que cela puisse paraître.

Les navigateurs analysent les séquences UTF-8 non valides, dans les documents HTML UTF-8, en mettant simplement le symbole « ». Ainsi, une fois que la séquence UTF-8 invalide dans le HTML est analysée, le texte résultant est une chaîne valide.

Même à l'intérieur des valeurs d'attribut, vous devez décoder des entités HTML comme amp

Voici une excellente question qui résume pourquoi vous ne pouvez pas analyser de manière fiable le HTML avec une expression régulière: RegEx correspond aux balises ouvertes sauf les balises autonomes XHTML

Eduardo
la source
2
J'adorerais garder l'expression rationnelle car elle est environ 10 fois plus rapide et je ne veux vraiment pas analyser correctement le code HTML, mais je veux juste extraire des liens. Je devrais pouvoir remplacer les parties invalides dans ruby ​​en faisant simplement: ok_string = bad_string.encode ("UTF-8", {: invalid =>: replace,: undef =>: replace}) mais cela ne semble pas travail :(
Marc Seeger
3

Cela semble fonctionner:

def sanitize_utf8(string)
  return nil if string.nil?
  return string if string.valid_encoding?
  string.chars.select { |c| c.valid_encoding? }.join
end
Spajus
la source
3
attachment = file.read

begin
   # Try it as UTF-8 directly
   cleaned = attachment.dup.force_encoding('UTF-8')
   unless cleaned.valid_encoding?
     # Some of it might be old Windows code page
     cleaned = attachment.encode( 'UTF-8', 'Windows-1252' )
   end
   attachment = cleaned
 rescue EncodingError
   # Force it to UTF-8, throwing out invalid bits
   attachment = attachment.force_encoding("ISO-8859-1").encode("utf-8", replace: nil)
 end
rusllonrails
la source
2

J'ai rencontré des chaînes, qui avaient des mélanges d'anglais, de russe et d'autres alphabets, ce qui a causé une exception. Je n'ai besoin que du russe et de l'anglais, et cela fonctionne actuellement pour moi:

ec1 = Encoding::Converter.new "UTF-8","Windows-1251",:invalid=>:replace,:undef=>:replace,:replace=>""
ec2 = Encoding::Converter.new "Windows-1251","UTF-8",:invalid=>:replace,:undef=>:replace,:replace=>""
t = ec2.convert ec1.convert t
Nakilon
la source
1

Alors que la solution de Nakilon fonctionne, au moins en ce qui concerne l'erreur, dans mon cas, j'ai converti en CSV ce personnage étrange f-ed up provenant de Microsoft Excel en CSV qui s'enregistrait en ruby ​​en tant que (obtenez ceci) cyrillique K qui dans ruby était un K. en gras. Pour résoudre ce problème, j'ai utilisé «iso-8859-1» à savoir. CSV.parse(f, :encoding => "iso-8859-1"), qui a transformé mes freaky cyrillic K's en un beaucoup plus gérable /\xCA/, que je pourrais ensuite supprimer avecstring.gsub!(/\xCA/, '')

boulder_ruby
la source
Encore une fois, je veux juste noter que si le correctif de Nakilon (et d'autres) concernait les caractères cyrilliques provenant de (haha) Cyrillia, cette sortie est une sortie standard pour un csv qui a été converti à partir de xls!
boulder_ruby
0

Avant de l'utiliser scan, assurez-vous que l'en- Content-Typetête de la page demandée est text/html, car il peut y avoir des liens vers des éléments comme des images qui ne sont pas encodées en UTF-8. La page peut également être non html si vous avez choisi un hrefélément comme un <link>élément. La façon de vérifier cela varie en fonction de la bibliothèque HTTP que vous utilisez. Ensuite, assurez-vous que le résultat est uniquement ascii avec String#ascii_only?(pas UTF-8 car HTML est censé utiliser uniquement ascii, les entités peuvent être utilisées autrement). Si ces deux tests réussissent, leur utilisation est sûre scan.

Adrian
la source
merci, mais ce n'est pas mon problème :) De toute façon, je n'extrait que la partie hôte de l'URL et ne frappe que la page d'accueil. Mon problème est que mon entrée n'est apparemment pas UTF-8 et que l'encodage 1.9 foo se détraque
Marc Seeger
@Marc Seeger: Qu'entendez-vous par «mon entrée»? Stdin, l'URL ou le corps de la page?
Adrian
Le HTML peut être encodé en UTF-8: en.wikipedia.org/wiki/Character_encodings_in_HTML
Eduardo
mon entrée = le corps de la page @Eduardo: je sais. Mon problème est que les données provenant de net / http semblent avoir un mauvais encodage de temps en temps
Marc Seeger
Il n'est pas rare que les pages Web aient un mauvais encodage pour de vrai. L'en-tête de réponse peut indiquer qu'il s'agit d'un codage mais qu'il sert en fait un autre codage.
sunkencity
-1

Si vous ne vous «souciez» pas des données, vous pouvez simplement faire quelque chose comme:

search_params = params[:search].valid_encoding? ? params[:search].gsub(/\W+/, '') : "nothing"

J'avais l'habitude valid_encoding?de le faire passer. Le mien est un champ de recherche, et donc je trouvais la même bizarrerie encore et encore, alors j'ai utilisé quelque chose comme: juste pour que le système ne se brise pas. Étant donné que je ne contrôle pas l'expérience utilisateur pour procéder à une validation automatique avant d'envoyer ces informations (comme un retour automatique pour dire "factice!"), Je peux simplement les intégrer, les supprimer et renvoyer des résultats vides.

pjammer
la source