Comment convertir un objet String en objet Hash?

137

J'ai une chaîne qui ressemble à un hachage:

"{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }"

Comment puis-je en obtenir un Hash? comme:

{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }

La chaîne peut avoir n'importe quelle profondeur d'imbrication. Il possède toutes les propriétés permettant de saisir un hachage valide dans Ruby.

Waseem
la source
Je pense qu'eval fera quelque chose ici. Laissez-moi tester d'abord. J'ai posté la question trop tôt, je pense. :)
Waseem
Ohh ouais, passez-le simplement à l'évaluation. :)
Waseem

Réponses:

79

La chaîne créée par appel Hash#inspectpeut être reconvertie en hachage en appelanteval . Cependant, cela nécessite que la même chose soit vraie pour tous les objets du hachage.

Si je commence par le hachage {:a => Object.new}, alors sa représentation sous forme de chaîne est "{:a=>#<Object:0x7f66b65cf4d0>}", et je ne peux pas l'utiliser evalpour le transformer en hachage car la #<Object:0x7f66b65cf4d0>syntaxe Ruby n'est pas valide.

Cependant, si tout ce qui est dans le hachage est constitué de chaînes, de symboles, de nombres et de tableaux, cela devrait fonctionner, car ceux-ci ont des représentations de chaîne qui sont une syntaxe Ruby valide.

Ken Bloom
la source
"si tout ce qui est dans le hachage est des chaînes, des symboles et des nombres,". Cela en dit long. Je peux donc vérifier la validité d'une chaîne à utiliser evalcomme hachage en m'assurant que l'instruction ci-dessus est valide pour cette chaîne.
Waseem
1
Oui, mais pour ce faire, vous avez soit besoin d'un analyseur Ruby complet, soit vous devez savoir d'où vient la chaîne et savoir qu'elle ne peut générer que des chaînes, des symboles et des nombres. (Voir aussi la réponse de Toms Mikoss sur la confiance dans le contenu de la chaîne.)
Ken Bloom
13
Soyez prudent lorsque vous l'utilisez. Utiliser evalau mauvais endroit est une énorme faille de sécurité. Tout ce qui se trouve à l'intérieur de la chaîne sera évalué. Alors imaginez si quelqu'un a injecté dans une APIrm -fr
Pithikos
153

Pour une chaîne différente, vous pouvez le faire sans utiliser de evalméthode dangereuse :

hash_as_string = "{\"0\"=>{\"answer\"=>\"1\", \"value\"=>\"No\"}, \"1\"=>{\"answer\"=>\"2\", \"value\"=>\"Yes\"}, \"2\"=>{\"answer\"=>\"3\", \"value\"=>\"No\"}, \"3\"=>{\"answer\"=>\"4\", \"value\"=>\"1\"}, \"4\"=>{\"value\"=>\"2\"}, \"5\"=>{\"value\"=>\"3\"}, \"6\"=>{\"value\"=>\"4\"}}"
JSON.parse hash_as_string.gsub('=>', ':')
Zolter
la source
2
Cette réponse doit être sélectionnée pour éviter d'utiliser eval.
Michael_Zhang
4
vous devriez également remplacer nils, feJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke
136

Une méthode rapide et sale serait

eval("{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, :key_b => { :key_1b => 'value_1b' } }") 

Mais cela a de graves implications pour la sécurité.
Il exécute tout ce qu'il est passé, vous devez être sûr à 110% (comme dans, au moins aucune entrée utilisateur n'importe où en cours de route), il ne contiendrait que des hachages correctement formés ou des bugs inattendus / horribles créatures de l'espace pourraient commencer à apparaître.

Toms Mikoss
la source
16
J'ai un sabre laser avec moi. Je peux m'occuper de ces créatures et de ces insectes. :)
Waseem
12
UTILISER EVAL peut être dangereux ici, selon mon professeur. Eval prend n'importe quel code ruby ​​et l'exécute. Le danger ici est analogue au danger d'injection SQL. Gsub est préférable.
boulder_ruby
9
Exemple de chaîne montrant pourquoi le professeur de David est correct: '{: surprise => "# {system \" rm -rf * \ "}"}'
A. Wilson
13
Je ne saurais trop insister sur le DANGER d'utiliser EVAL! Ceci est absolument interdit si l'entrée de l'utilisateur peut se frayer un chemin dans votre chaîne.
Dave Collins
Même si vous pensez que vous n'ouvrirez jamais cela plus publiquement, quelqu'un d'autre pourrait le faire. Nous (devrions) tous savoir comment le code est utilisé d'une manière que vous n'auriez pas attendue. C'est comme mettre des objets extrêmement lourds sur une étagère haute, ce qui la rend trop lourde. Vous ne devriez jamais créer cette forme de danger.
Steve Sether
24

Peut-être YAML.load?

silencieux
la source
(la méthode de chargement prend en charge les chaînes)
silencieux
5
Cela nécessite une représentation de chaîne totalement différente, mais c'est beaucoup, beaucoup plus sûr. (Et la représentation sous forme de chaîne est tout aussi facile à générer - appelez simplement #to_yaml, plutôt que #inspect)
Ken Bloom
Sensationnel. Je n'avais aucune idée qu'il était si facile d'analyser les chaînes avec yaml. Il prend ma chaîne de commandes linux bash qui génèrent des données et les transforme intelligemment en un Ruby Hash sans aucun format de chaîne de massage.
labyrinthe
Ceci et to_yaml résout mon problème car j'ai un certain contrôle sur la façon dont la chaîne est générée. Merci!
mlabarca le
23

Ce petit extrait de code le fera, mais je ne peux pas le voir fonctionner avec un hachage imbriqué. Je pense que c'est plutôt mignon

STRING.gsub(/[{}:]/,'').split(', ').map{|h| h1,h2 = h.split('=>'); {h1 => h2}}.reduce(:merge)

Étapes 1. J'élimine les '{', '}' et les ':' 2. Je divise sur la chaîne partout où elle trouve un ',' 3. Je divise chacune des sous-chaînes qui ont été créées avec le fractionnement, chaque fois qu'il trouve a '=>'. Ensuite, je crée un hachage avec les deux côtés du hachage que je viens de séparer. 4. Il me reste un tableau de hachages que je fusionne ensuite.

EXEMPLE D'ENTREE: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" RESULT OUTPUT: {"user_id" => "11", "blog_id" => "2", "comment_id" = > "1"}

hrdwdmrbl
la source
1
C'est un oneliner malade! :) +1
blushrt
3
Cela ne supprimera-t-il pas également les {}:caractères des valeurs à l'intérieur du hachage stringifié?
Vladimir Panteleev
@VladimirPanteleev Vous avez raison, ce serait le cas. Belle prise! Vous pouvez faire mes révisions de code n'importe quel jour :)
hrdwdmrbl
20

Les solutions jusqu'à présent couvrent certains cas, mais en manquent (voir ci-dessous). Voici ma tentative de conversion plus approfondie (sûre). Je connais un cas d'angle que cette solution ne gère pas, à savoir les symboles à un seul caractère composés de caractères impairs, mais autorisés. Par exemple, {:> => :<}un hachage ruby ​​valide.

J'ai également mis ce code sur github . Ce code commence par une chaîne de test pour exercer toutes les conversions

require 'json'

# Example ruby hash string which exercises all of the permutations of position and type
# See http://json.org/
ruby_hash_text='{"alpha"=>{"first second > third"=>"first second > third", "after comma > foo"=>:symbolvalue, "another after comma > foo"=>10}, "bravo"=>{:symbol=>:symbolvalue, :aftercomma=>10, :anotheraftercomma=>"first second > third"}, "charlie"=>{1=>10, 2=>"first second > third", 3=>:symbolvalue}, "delta"=>["first second > third", "after comma > foo"], "echo"=>[:symbol, :aftercomma], "foxtrot"=>[1, 2]}'

puts ruby_hash_text

# Transform object string symbols to quoted strings
ruby_hash_text.gsub!(/([{,]\s*):([^>\s]+)\s*=>/, '\1"\2"=>')

# Transform object string numbers to quoted strings
ruby_hash_text.gsub!(/([{,]\s*)([0-9]+\.?[0-9]*)\s*=>/, '\1"\2"=>')

# Transform object value symbols to quotes strings
ruby_hash_text.gsub!(/([{,]\s*)(".+?"|[0-9]+\.?[0-9]*)\s*=>\s*:([^,}\s]+\s*)/, '\1\2=>"\3"')

# Transform array value symbols to quotes strings
ruby_hash_text.gsub!(/([\[,]\s*):([^,\]\s]+)/, '\1"\2"')

# Transform object string object value delimiter to colon delimiter
ruby_hash_text.gsub!(/([{,]\s*)(".+?"|[0-9]+\.?[0-9]*)\s*=>/, '\1\2:')

puts ruby_hash_text

puts JSON.parse(ruby_hash_text)

Voici quelques notes sur les autres solutions ici

gene_wood
la source
Solution très cool. Vous pouvez ajouter un gsub de tout :nilà :nullpour gérer cette bizarrerie particulière.
SteveTurczyn
1
Cette solution a également l'avantage de travailler sur des hachages multi-niveaux de manière récursive, car elle exploite l'analyse JSON #. J'ai eu quelques problèmes avec l'imbrication sur d'autres solutions.
Patrick Read
17

J'ai eu le même problème. Je stockais un hash dans Redis. Lors de la récupération de ce hachage, il s'agissait d'une chaîne. Je ne voulais pas appeler pour des eval(str)raisons de sécurité. Ma solution était d'enregistrer le hachage sous forme de chaîne json au lieu d'une chaîne de hachage ruby. Si vous en avez la possibilité, utiliser json est plus simple.

  redis.set(key, ruby_hash.to_json)
  JSON.parse(redis.get(key))

TL; DR: utiliser to_jsonetJSON.parse

Jared Ménard
la source
1
C'est de loin la meilleure réponse. to_jsonetJSON.parse
ardochhigh
3
À quiconque m'a refusé. Pourquoi? J'ai eu le même problème, en essayant de convertir une représentation sous forme de chaîne d'un hachage ruby ​​en un objet de hachage réel. J'ai réalisé que j'essayais de résoudre le mauvais problème. J'ai réalisé que la résolution de la question posée ici était sujette aux erreurs et peu sûre. J'ai réalisé que je devais stocker mes données différemment et utiliser un format conçu pour sérialiser et désérialiser des objets en toute sécurité. TL; DR: J'avais la même question que OP, et j'ai réalisé que la réponse était de poser une question différente. De plus, si vous me rejetez, veuillez fournir vos commentaires afin que nous puissions tous apprendre ensemble.
Jared Menard
3
Le vote à la baisse sans commentaire explicatif est le cancer de Stack Overflow.
ardochhigh
1
oui, le vote défavorable doit exiger une explication et montrer qui vote contre.
Nick Res
2
Pour rendre cette réponse encore plus applicable à la question de l'OP, si votre représentation sous forme de chaîne d'un hachage s'appelle 'strungout', vous devriez être en mesure de faire hashit = JSON.parse (strungout.to_json), puis sélectionnez vos éléments à l'intérieur de hashit via hashit [ 'keyname'] comme d'habitude.
cixelsyd
11

Je préfère abuser d'ActiveSupport :: JSON. Leur approche consiste à convertir le hachage en yaml, puis à le charger. Malheureusement, la conversion en yaml n'est pas simple et vous voudrez probablement l'emprunter à AS si vous n'avez pas déjà AS dans votre projet.

Nous devons également convertir tous les symboles en clés de chaîne régulières car les symboles ne sont pas appropriés dans JSON.

Cependant, il est incapable de gérer les hachages contenant une chaîne de date (nos chaînes de date finissent par ne pas être entourées de chaînes, c'est là que le gros problème entre en jeu):

string = '{' last_request_at ': 28/12/2011 23:00:00 UTC}' ActiveSupport::JSON.decode(string.gsub(/:([a-zA-z])/,'\\1').gsub('=>', ' : '))

Se traduirait par une erreur de chaîne JSON non valide lorsqu'il essaie d'analyser la valeur de date.

J'adorerais des suggestions sur la façon de gérer cette affaire

c.apolzon
la source
2
Merci pour le pointeur vers .decode, cela a très bien fonctionné pour moi. J'avais besoin de convertir une réponse JSON pour la tester. Voici le code que j'ai utilisé:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips
9

fonctionne dans les rails 4.1 et prend en charge les symboles sans guillemets {: a => 'b'}

ajoutez simplement ceci au dossier des initialiseurs:

class String
  def to_hash_object
    JSON.parse(self.gsub(/:([a-zA-z]+)/,'"\\1"').gsub('=>', ': ')).symbolize_keys
  end
end
Eugène
la source
Fonctionne sur la ligne de commande, mais j'obtiens "le niveau de la pile à la profondeur" quand je mets cela dans un initiateur ...
Alex Edelstein
2

J'ai construit un hash_parser gem qui vérifie d'abord si un hachage est sûr ou n'utilise pas de ruby_parsergem. Seulement alors, il applique le eval.

Vous pouvez l'utiliser comme

require 'hash_parser'

# this executes successfully
a = "{ :key_a => { :key_1a => 'value_1a', :key_2a => 'value_2a' }, 
       :key_b => { :key_1b => 'value_1b' } }"
p HashParser.new.safe_load(a)

# this throws a HashParser::BadHash exception
a = "{ :key_a => system('ls') }"
p HashParser.new.safe_load(a)

Les tests de https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb vous donnent plus d'exemples des choses que j'ai testées pour m'assurer que eval est sûr.

bibstha
la source
2

Veuillez considérer cette solution. Bibliothèque + spécifications:

Fichier lib/ext/hash/from_string.rb::

require "json"

module Ext
  module Hash
    module ClassMethods
      # Build a new object from string representation.
      #
      #   from_string('{"name"=>"Joe"}')
      #
      # @param s [String]
      # @return [Hash]
      def from_string(s)
        s.gsub!(/(?<!\\)"=>nil/, '":null')
        s.gsub!(/(?<!\\)"=>/, '":')
        JSON.parse(s)
      end
    end
  end
end

class Hash    #:nodoc:
  extend Ext::Hash::ClassMethods
end

Fichier spec/lib/ext/hash/from_string_spec.rb::

require "ext/hash/from_string"

describe "Hash.from_string" do
  it "generally works" do
    [
      # Basic cases.
      ['{"x"=>"y"}', {"x" => "y"}],
      ['{"is"=>true}', {"is" => true}],
      ['{"is"=>false}', {"is" => false}],
      ['{"is"=>nil}', {"is" => nil}],
      ['{"a"=>{"b"=>"c","ar":[1,2]}}', {"a" => {"b" => "c", "ar" => [1, 2]}}],
      ['{"id"=>34030, "users"=>[14105]}', {"id" => 34030, "users" => [14105]}],

      # Tricky cases.
      ['{"data"=>"{\"x\"=>\"y\"}"}', {"data" => "{\"x\"=>\"y\"}"}],   # Value is a `Hash#inspect` string which must be preserved.
    ].each do |input, expected|
      output = Hash.from_string(input)
      expect([input, output]).to eq [input, expected]
    end
  end # it
end
Alex Fortuna
la source
1
it "generally works" mais pas nécessairement? Je serais plus verbeux dans ces tests. it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex
Hey @Lex, ce que fait la méthode est décrit dans son commentaire RubyDoc. Le test vaut mieux ne pas le reformuler, il créera des détails inutiles sous forme de texte passif. Ainsi, «fonctionne généralement» est une bonne formule pour dire que les choses fonctionnent généralement. À votre santé!
Alex Fortuna le
Ouais, à la fin de la journée, tout ce qui fonctionne. Tous les tests valent mieux qu'aucun test. Personnellement, je suis fan de descriptions explicites, mais ce n'est qu'une préférence.
Lex
1

Je suis venu à cette question après avoir écrit une ligne à cet effet, donc je partage mon code au cas où cela aiderait quelqu'un. Fonctionne pour une chaîne avec un seul niveau de profondeur et des valeurs vides possibles (mais pas nulles), comme:

"{ :key_a => 'value_a', :key_b => 'value_b', :key_c => '' }"

Le code est:

the_string = '...'
the_hash = Hash.new
the_string[1..-2].split(/, /).each {|entry| entryMap=entry.split(/=>/); value_str = entryMap[1]; the_hash[entryMap[0].strip[1..-1].to_sym] = value_str.nil? ? "" : value_str.strip[1..-2]}
Pablo
la source
0

Ran sur un problème similaire nécessitant l'utilisation de eval ().

Ma situation, je tirais des données d'une API et les écrivais dans un fichier localement. Puis être en mesure d'extraire les données du fichier et d'utiliser le Hash.

J'ai utilisé IO.read () pour lire le contenu du fichier dans une variable. Dans ce cas, IO.read () le crée en tant que String.

Puis utilisé eval () pour convertir la chaîne en un Hash.

read_handler = IO.read("Path/To/File.json")

puts read_handler.kind_of?(String) # Returns TRUE

a = eval(read_handler)

puts a.kind_of?(Hash) # Returns TRUE

puts a["Enter Hash Here"] # Returns Key => Values

puts a["Enter Hash Here"].length # Returns number of key value pairs

puts a["Enter Hash Here"]["Enter Key Here"] # Returns associated value

Aussi juste pour mentionner que IO est un ancêtre de File. Vous pouvez donc également utiliser File.read à la place si vous le souhaitez.

TomG
la source