web-dev-qa-db-fra.com

ruby 1.9, force_encoding, mais vérifiez

J'ai une chaîne que j'ai lue à partir d'une sorte d'entrée. 

Au meilleur de ma connaissance, il s'agit de UTF8. D'accord:

string.force_encoding("utf8")

Mais si cette chaîne contient des octets qui ne sont pas en fait des UTF8 légaux, je veux le savoir maintenant et agir. 

Normalement, force_encoding ("utf8") sera-t-il déclenché s'il rencontre de tels octets? Je crois que ce ne sera pas le cas. 

Si je faisais un #encode je pourrais choisir parmi les options pratiques avec quoi faire avec les caractères qui ne sont pas valables dans le codage source (ou codage de destination). 

Mais je ne fais pas un #encode, je fais un #force_encoding. Il n'a pas de telles options. 

Serait-il logique de 

string.force_encoding("utf8").encode("utf8")

obtenir une exception tout de suite? Normalement, encoder de utf8 en utf8 n'a aucun sens. Mais peut-être que c'est le moyen de l'obtenir immédiatement s'il y a des octets invalides? Ou utilisez l'option :replace etc pour faire quelque chose de différent avec des octets non valides?

Mais non, n'arrive pas à faire ce travail non plus. 

Quelqu'un sait?

1.9.3-p0 :032 > a = "bad: \xc3\x28 okay".force_encoding("utf-8")
=> "bad: \xC3( okay"
1.9.3-p0 :033 > a.valid_encoding?
=> false

D'accord, mais comment puis-je trouver et éliminer ces mauvais octets? Bizarrement, cela ne soulève PAS:

1.9.3-p0 :035 > a.encode("utf-8")
 => "bad: \xC3( okay"

Si je convertissais en un encodage différent, ce serait le cas!

1.9.3-p0 :039 > a.encode("ISO-8859-1")
Encoding::InvalidByteSequenceError: "\xC3" followed by "(" on UTF-8

Ou si je le lui disais, il le remplacerait par un "?" =>

1.9.3-p0 :040 > a.encode("ISO-8859-1", :invalid => :replace)
=> "bad: ?( okay"

Ainsi, Ruby a la capacité de savoir quels sont les mauvais octets dans utf-8 et de les remplacer par autre chose - lors de la conversion en un encodage différent. Mais je ne veux pas convertir en un encodage différent, je veux rester utf8 - mais je pourrais vouloir augmenter s'il y a un octet invalide, ou je pourrais vouloir remplacer des octets invalides par des caractères de remplacement. 

N'y a-t-il pas moyen d'amener Ruby à faire ça?

update Je pense que cela a finalement été ajouté à Ruby en 2.1, avec String # scrub présent dans la version de prévisualisation 2.1 pour le faire. Alors cherchez ça!

25
jrochkind

(mise à jour: voir https://github.com/jrochkind/scrub_rb )

J'ai donc codé une solution répondant à mes besoins: https://github.com/jrochkind/ensure_valid_encoding/blob/master/lib/ensure_valid_encoding.rb

Mais ce n’est que beaucoup plus récemment que j’ai réalisé que IS était intégré à la bibliothèque stdlib. Il vous suffisait de passer, quelque peu contre-intuitif, le mot "binaire" comme "encodage source":

a = "bad: \xc3\x28 okay".force_encoding("utf-8")
a.encode("utf-8", "binary", :undef => :replace)
=> "bad: �( okay"

Oui, c'est exactement ce que je voulais. Donc, il s’avère que ce IS est intégré à la norme 1.9 stdlib, il est simplement non documenté et peu de gens le savent (ou peut-être que peu de gens qui parlent anglais le savent?). Bien que ces arguments aient été utilisés de cette manière sur un blog, quelqu'un d'autre le savait!

16
jrochkind

Dans Ruby 2.1, stdlib supporte finalement ceci avec scrub

http://Ruby-doc.org/core-2.1.0/String.html#method-i-scrub

6
jrochkind

assurez-vous que votre fichier script lui-même est enregistré au format UTF8 et procédez comme suit:

# encoding: UTF-8
p [a = "bad: \xc3\x28 okay", a.valid_encoding?]
p [a.force_encoding("utf-8"), a.valid_encoding?]
p [a.encode!("ISO-8859-1", :invalid => :replace), a.valid_encoding?]

Cela donne sur mon système Windows7 ce qui suit

["bad: \xC3( okay", false]
["bad: \xC3( okay", false]
["bad: ?( okay", true]

Donc, votre mauvais personnage est remplacé, vous pouvez le faire immédiatement comme suit

a = "bad: \xc3\x28 okay".encode!("ISO-8859-1", :invalid => :replace)
=> "bad: ?( okay"

EDIT: ici une solution qui fonctionne sur n’importe quel encodage arbitraire, le premier ne code que les mauvais caractères, le second vient simplement d’être remplacé par un?

def validate_encoding(str)
  str.chars.collect do |c| 
    (c.valid_encoding?) ? c:c.encode!(Encoding.locale_charmap, :invalid => :replace)
  end.join 
end

def validate_encoding2(str)
  str.chars.collect do |c| 
    (c.valid_encoding?) ? c:'?'
  end.join 
end

a = "bad: \xc3\x28 okay"

puts validate_encoding(a)                  #=>bad: ?( okay
puts validate_encoding(a).valid_encoding?  #=>true


puts validate_encoding2(a)                  #=>bad: ?( okay
puts validate_encoding2(a).valid_encoding?  #=>true
4
peter

Pour vérifier qu’une chaîne ne contient aucune séquence non valide, essayez de la convertir en binary encoding:

# Returns true if the string has only valid sequences
def valid_encoding?(string)
  string.encode('binary', :undef => :replace)
  true
rescue Encoding::InvalidByteSequenceError => e
  false
end

p valid_encoding?("\xc0".force_encoding('iso-8859-1'))    # true
p valid_encoding?("\u1111")                               # true
p valid_encoding?("\xc0".force_encoding('utf-8'))         # false

Ce code remplace les caractères non définis, car nous ne nous soucions pas de savoir s'il existe des séquences valides qui ne peuvent pas être représentées en binaire. Nous nous soucions seulement s'il y a des séquences invalides.

Une légère modification de ce code renvoie l'erreur réelle, qui contient des informations précieuses sur le codage incorrect:

# Returns the encoding error, or nil if there isn't one.

def encoding_error(string)
  string.encode('binary', :undef => :replace)
  nil
rescue Encoding::InvalidByteSequenceError => e
  e.to_s
end

# Returns truthy if the string has only valid sequences

def valid_encoding?(string)
  !encoding_error(string)
end

puts encoding_error("\xc0".force_encoding('iso-8859-1'))    # nil
puts encoding_error("\u1111")                               # nil
puts encoding_error("\xc0".force_encoding('utf-8'))         # "\xC0" on UTF-8
3
Wayne Conrad

Voici 2 situations courantes et comment les résoudre dans Ruby 2.1+ . Je sais que la question fait référence à Ruby v1.9, mais cela est peut-être utile pour les autres utilisateurs qui trouvent cette question via Google.

Situation 1

Vous avez une chaîne UTF-8 avec éventuellement quelques octets non valides
Supprimez les octets non valides:

str = "Partly valid\xE4 UTF-8 encoding: äöüß"

str.scrub('')
 # => "Partly valid UTF-8 encoding: äöüß"

Situation 2

Vous avez une chaîne qui pourrait être au format UTF-8 ou ISO-8859-1
Vérifiez son codage et convertissez-le en UTF-8 (si nécessaire):

str = "String in ISO-8859-1 encoding: \xE4\xF6\xFC\xDF"

unless str.valid_encoding?
  str.encode!( 'UTF-8', 'ISO-8859-1', invalid: :replace, undef: :replace, replace: '?' )
end #unless
 # => "String in ISO-8859-1 encoding: äöüß"

Remarques

  • Les extraits de code ci-dessus supposent que Ruby encode par défaut toutes vos chaînes dans UTF-8. Même si c'est presque toujours le cas, vous pouvez vous en assurer en démarrant vos scripts avec # encoding: UTF-8.

  • Si non valide, il est possible par programme de détecter la plupart des codages sur plusieurs octets tels que UTF-8 (en Ruby, voir: #valid_encoding?). Cependant, il n'est PAS (facilement) possible de détecter par programme l'invalidité d'encodages à un octet tels que ISO-8859-1. Ainsi, l’extrait de code ci-dessus ne fonctionne pas dans l’inverse, c’est-à-dire détecter si une chaîne est un encodage ISO-8859-1 valide.

  • Même si UTF-8 est devenu de plus en plus populaire en tant que codage par défaut sur le Web, ISO-8859-1 et d'autres versions de Latin1 sont toujours très populaires dans les pays occidentaux, notamment en Amérique du Nord. Sachez qu’il existe plusieurs codages sur un octet très similaires, mais qui varient légèrement par rapport à ISO-8859-1. Exemples: CP1252 (a.k.a. Windows-1252), ISO-8859-15

0
Andreas Rayo Kniep

Un moyen simple de provoquer une exception semble être:

untrusted_string.match /./

0
Tallak Tveide

Si vous faites cela pour un cas d'utilisation "réel" - par exemple pour analyser différentes chaînes entrées par les utilisateurs, et pas uniquement pour pouvoir "décoder" un fichier totalement aléatoire pouvant être composé d'autant d'encodages comme vous le souhaitez, vous pouvez au moins supposer que tous les caractères de chaque chaîne ont le même encodage.

Alors, dans ce cas, que penseriez-vous de cela?

strings = [ "UTF-8 string with some utf8 chars \xC3\xB2 \xC3\x93", 
             "ISO-8859-1 string with some iso-8859-1 chars \xE0 \xE8", "..." ]

strings.each { |s| 
    s.force_encoding "utf-8"
    if s.valid_encoding?
        next
    else
        while s.valid_encoding? == false 
                    s.force_encoding "ISO-8859-1"
                    s.force_encoding "..."
                end
        s.encode!("utf-8")
    end
}

Je ne suis pas un "pro" Ruby de quelque manière que ce soit, alors veuillez pardonner si ma solution est fausse ou même un peu naïve ..

J'essaie juste de rendre ce que je peux, et c'est ce que je suis venu faire alors que je travaillais encore sur ce petit analyseur syntaxique pour des chaînes codées de manière arbitraire, ce que je suis en train de faire pour un projet d'étude.

Pendant que je publie ce message, je dois admettre que je ne l’ai même pas encore totalement testé. Je… viens d’obtenir quelques résultats «positifs», mais je me sentais tellement excité d’avoir trouvé ce que j’avais du mal à trouver ( et pendant tout le temps que j'ai passé à lire à ce sujet sur SO ..), j’ai ressenti le besoin de le partager le plus rapidement possible, en espérant que cela permettrait de gagner du temps pour ceux qui le recherchent depuis aussi longtemps que je le souhaite. 'ai été ... .. si cela fonctionne comme prévu :)

0
jj_

La seule chose à laquelle je peux penser est de transcoder en quelque chose de dos qui n'endommagera pas la corde dans l'aller-retour:

string.force_encoding("UTF-8").encode("UTF-32LE").encode("UTF-8")

Cela semble plutôt inutile.

0
Mark Reed

Ok, voici une façon vraiment stupide de le faire, Ruby, que j'ai moi-même découvert. Il effectue probablement pour la merde. qu'est-ce que c'est, Ruby? Ne sélectionnant pas ma propre réponse pour le moment, j'espère que quelqu'un d'autre se présentera et nous donnera quelque chose de mieux. 

 # Pass in a string, will raise an Encoding::InvalidByteSequenceError
 # if it contains an invalid byte for it's encoding; otherwise
 # returns an equivalent string.
 #
 # OR, like String#encode, pass in option `:invalid => :replace`
 # to replace invalid bytes with a replacement string in the
 # returned string.  Pass in the
 # char you'd like with option `:replace`, or will, like String#encode
 # use the unicode replacement char if it thinks it's a unicode encoding,
 # else ascii '?'.
 #
 # in any case, method will raise, or return a new string
 # that is #valid_encoding?
 def validate_encoding(str, options = {})
   str.chars.collect do |c|
     if c.valid_encoding?
       c
     else
       unless options[:invalid] == :replace
         # it ought to be filled out with all the metadata
         # this exception usually has, but what a pain!
         raise  Encoding::InvalidByteSequenceError.new
       else
         options[:replace] || (
          # surely there's a better way to tell if
          # an encoding is a 'Unicode encoding form'
          # than this? What's wrong with you Ruby 1.9?
          str.encoding.name.start_with?('UTF') ?
             "\uFFFD" :
             "?" )
       end
     end 
   end.join
 end

De plus en plus sur http://bibwild.wordpress.com/2012/04/17/checkingfixing-bad-bytes-in-Ruby-1-9-char-encoding/

0
jrochkind