Wie konvertiere ich ein String-Objekt in ein Hash-Objekt?

136

Ich habe eine Zeichenfolge, die wie ein Hash aussieht:

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

Wie bekomme ich einen Hash daraus? mögen:

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

Die Zeichenfolge kann eine beliebige Verschachtelungstiefe aufweisen. Es hat alle Eigenschaften, wie ein gültiger Hash in Ruby eingegeben wird.

Waseem
quelle
Ich denke, eval wird hier etwas bewirken. Lassen Sie mich zuerst testen. Ich habe die Frage zu früh gestellt, denke ich. :)
Waseem
Ohh ja, gib es einfach an die Bewertung weiter. :)
Waseem

Antworten:

79

Die durch Aufrufen erstellte Zeichenfolge kann durch Aufrufen Hash#inspectwieder in einen Hash umgewandelt werden eval. Dies setzt jedoch voraus, dass dies für alle Objekte im Hash gilt.

Wenn ich mit dem Hash beginne {:a => Object.new}, ist seine Zeichenfolgendarstellung "{:a=>#<Object:0x7f66b65cf4d0>}"und ich kann ihn nicht evalwieder in einen Hash umwandeln, da #<Object:0x7f66b65cf4d0>die Ruby-Syntax nicht gültig ist.

Wenn der Hash jedoch nur Zeichenfolgen, Symbole, Zahlen und Arrays enthält, sollte dies funktionieren, da diese Zeichenfolgendarstellungen mit gültiger Ruby-Syntax enthalten.

Ken Bloom
quelle
"Wenn alles, was im Hash enthalten ist, Zeichenfolgen, Symbole und Zahlen sind". Das sagt viel aus. So kann ich die Gültigkeit einer Zeichenfolge überprüfen, die evalals Hash verwendet werden soll, indem ich sicherstelle, dass die obige Anweisung für diese Zeichenfolge gültig ist.
Waseem
1
Ja, aber dazu benötigen Sie entweder einen vollständigen Ruby-Parser oder Sie müssen wissen, woher die Zeichenfolge stammt und dass nur Zeichenfolgen, Symbole und Zahlen generiert werden können. (Siehe auch Toms Mikoss 'Antwort über das Vertrauen in den Inhalt der Zeichenfolge.)
Ken Bloom
13
Seien Sie vorsichtig, wo Sie dies verwenden. Die Verwendung evalam falschen Ort ist eine große Sicherheitslücke. Alles innerhalb der Zeichenfolge wird ausgewertet. Stellen Sie sich also vor, in eine API würde jemand injizierenrm -fr
Pithikos
153

Für verschiedene Zeichenfolgen können Sie dies tun, ohne eine gefährliche evalMethode anzuwenden:

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
quelle
2
Diese Antwort sollte ausgewählt werden, um die Verwendung von eval zu vermeiden.
Michael_Zhang
4
Sie sollten auch nils, fe ersetzenJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke
136

Schnelle und schmutzige Methode wäre

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

Dies hat jedoch schwerwiegende Auswirkungen auf die Sicherheit.
Es führt alles aus, was übergeben wird. Sie müssen zu 110% sicher sein (wie in, zumindest keine Benutzereingaben irgendwo auf dem Weg), dass es nur richtig geformte Hashes enthält oder unerwartete Fehler / schreckliche Kreaturen aus dem Weltraum auftauchen könnten.

Toms Mikoss
quelle
16
Ich habe ein Lichtschwert dabei. Ich kann mich um diese Kreaturen und Käfer kümmern. :)
Waseem
12
Laut meinem Lehrer kann die Verwendung von EVAL hier gefährlich sein. Eval nimmt jeden Ruby-Code und führt ihn aus. Die Gefahr ist hier analog zur SQL-Injection-Gefahr. Gsub ist vorzuziehen.
boulder_ruby
9
Beispielzeichenfolge, die zeigt, warum Davids Lehrer korrekt ist: '{: überraschung => "# {system \" rm -rf * \ "}"}'
A. Wilson
13
Ich kann die GEFAHR, EVAL hier zu verwenden, nicht genug betonen! Dies ist absolut verboten, wenn Benutzereingaben jemals in Ihre Zeichenfolge gelangen können.
Dave Collins
Selbst wenn Sie denken, dass Sie dies niemals öffentlicher eröffnen werden, könnte es jemand anderes tun. Wir alle (sollten) wissen, wie Code auf eine Weise verwendet wird, die Sie nicht erwartet hätten. Es ist, als würde man extrem schwere Dinge auf ein hohes Regal stellen und es kopflastig machen. Sie sollten diese Form der Gefahr niemals schaffen.
Steve Sether
24

Vielleicht YAML.load?

Leise
quelle
(Lademethode unterstützt Zeichenfolgen)
Silent
5
Das erfordert eine völlig andere Zeichenfolgendarstellung, ist aber viel, viel sicherer. (Und die Zeichenfolgendarstellung ist genauso einfach zu generieren - rufen Sie einfach #to_yaml auf und nicht #inspect)
Ken Bloom
Beeindruckend. Ich hatte keine Ahnung, dass es so einfach ist, Strings mit Yaml zu analysieren. Es braucht meine Kette von Linux-Bash-Befehlen, die Daten generieren und sie auf intelligente Weise in einen Ruby-Hash ohne Massage im String-Format verwandeln.
Labyrinth
Dies und to_yaml lösen mein Problem, da ich die Steuerung der Zeichenfolge steuern kann. Vielen Dank!
mlabarca
23

Dieses kurze kleine Snippet wird es tun, aber ich kann nicht sehen, dass es mit einem verschachtelten Hash funktioniert. Ich finde es aber ziemlich süß

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

Schritte 1. Ich eliminiere das '{', '}' und das ':' 2. Ich teile die Zeichenfolge überall dort auf, wo sie ein ',' findet. 3. Ich teile jede der Teilzeichenfolgen, die mit der Teilung erstellt wurden, wann immer sie gefunden wird a '=>'. Dann erstelle ich einen Hash mit den beiden Seiten des Hash, die ich gerade getrennt habe. 4. Ich habe eine Reihe von Hashes, die ich dann zusammenführe.

BEISPIELEINGABE: "{: user_id => 11 ,: blog_id => 2 ,: comment_id => 1}" ERGEBNISAUSGABE: {"user_id" => "11", "blog_id" => "2", "comment_id" = > "1"}

hrdwdmrbl
quelle
1
Das ist ein kranker Oneliner! :) +1
blushrt
3
Wird dies nicht auch {}:Zeichen von Werten innerhalb des stringifizierten Hashs entfernen?
Vladimir Panteleev
@VladimirPanteleev Du hast recht, es würde. Schöner Fang! Sie können meine Code-Überprüfungen jeden Tag durchführen :)
hrdwdmrbl
20

Die bisherigen Lösungen decken einige Fälle ab, übersehen jedoch einige (siehe unten). Hier ist mein Versuch einer gründlicheren (sicheren) Konvertierung. Ich kenne einen Eckfall, den diese Lösung nicht behandelt, nämlich einzelne Zeichensymbole, die aus ungeraden, aber erlaubten Zeichen bestehen. Zum Beispiel {:> => :<}ist ein gültiger Ruby-Hash.

Ich habe diesen Code auch auf Github veröffentlicht . Dieser Code beginnt mit einer Testzeichenfolge, um alle Konvertierungen durchzuführen

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)

Hier einige Hinweise zu den anderen Lösungen

gene_wood
quelle
Sehr coole Lösung. Sie könnten ein gsub von allem hinzufügen :nil, :nullum mit dieser besonderen Verrücktheit umzugehen .
SteveTurczyn
1
Diese Lösung hat auch den Vorteil, dass sie rekursiv an mehrstufigen Hashes arbeitet, da sie JSON # parse nutzt. Ich hatte einige Probleme beim Verschachteln mit anderen Lösungen.
Patrick Read
17

Ich hatte das gleiche Problem. Ich habe einen Hash in Redis gespeichert. Beim Abrufen dieses Hashs war es eine Zeichenfolge. Ich wollte eval(str)aus Sicherheitsgründen nicht anrufen . Meine Lösung bestand darin, den Hash als JSON-String anstelle eines Ruby-Hash-Strings zu speichern. Wenn Sie die Option haben, ist die Verwendung von json einfacher.

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

TL; DR: verwenden to_jsonundJSON.parse

Jared Menard
quelle
1
Dies ist bei weitem die beste Antwort. to_jsonundJSON.parse
Ardochhigh
3
Zu wem auch immer mich herabgestimmt hat. Warum? Ich hatte das gleiche Problem beim Versuch, eine Zeichenfolgendarstellung eines Ruby-Hash in ein tatsächliches Hash-Objekt zu konvertieren. Mir wurde klar, dass ich versuchte, das falsche Problem zu lösen. Ich erkannte, dass das Lösen der hier gestellten Frage fehleranfällig und unsicher war. Mir wurde klar, dass ich meine Daten anders speichern und ein Format verwenden musste, mit dem Objekte sicher serialisiert und deserialisiert werden können. TL; DR: Ich hatte die gleiche Frage wie OP und erkannte, dass die Antwort darin bestand, eine andere Frage zu stellen. Wenn Sie mich ablehnen, geben Sie bitte Feedback, damit wir alle gemeinsam lernen können.
Jared Menard
3
Downvoting ohne erklärenden Kommentar ist der Krebs von Stack Overflow.
Ardochhigh
1
Ja, Downvoting sollte eine Erklärung erfordern und zeigen, wer Downvotes hat.
Nick Res
2
Um diese Antwort noch besser auf die Frage des OP anzuwenden, sollten Sie in der Lage sein, hashit = JSON.parse (strungout.to_json) auszuführen und dann Ihre Elemente innerhalb von Hashit über Hashit [auszuwählen 'keyname'] wie gewohnt.
Cixelsyd
11

Ich bevorzuge es, ActiveSupport :: JSON zu missbrauchen. Ihr Ansatz besteht darin, den Hash in Yaml umzuwandeln und ihn dann zu laden. Leider ist die Konvertierung in yaml nicht einfach und Sie möchten sie wahrscheinlich von AS ausleihen, wenn Sie AS noch nicht in Ihrem Projekt haben.

Wir müssen auch alle Symbole in reguläre String-Schlüssel konvertieren, da Symbole in JSON nicht geeignet sind.

Es ist jedoch nicht in der Lage, Hashes zu verarbeiten, die eine Datumszeichenfolge enthalten (unsere Datumszeichenfolgen werden letztendlich nicht von Zeichenfolgen umgeben, weshalb das große Problem auftritt):

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

Würde zu einem ungültigen JSON-Zeichenfolgenfehler führen, wenn versucht wird, den Datumswert zu analysieren.

Würde mich über Vorschläge zur Behandlung dieses Falls freuen

c.apolzon
quelle
2
Vielen Dank für den Zeiger auf .decode, es hat super für mich funktioniert. Ich musste eine JSON-Antwort konvertieren, um sie zu testen. Hier ist der Code, den ich verwendet habe:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips
9

funktioniert in Rails 4.1 und unterstützt Symbole ohne Anführungszeichen {: a => 'b'}

Fügen Sie dies einfach dem Initialisierungsordner hinzu:

class String
  def to_hash_object
    JSON.parse(self.gsub(/:([a-zA-z]+)/,'"\\1"').gsub('=>', ': ')).symbolize_keys
  end
end
Eugene
quelle
Funktioniert auf der Kommandozeile, aber ich bekomme "Stack Level to Deep", wenn ich dies in einen Intializer stecke ...
Alex Edelstein
2

Ich habe einen gem hash_parser erstellt , der zuerst prüft, ob ein Hash sicher ist oder nicht ruby_parsergem. Nur dann gilt das eval.

Sie können es als verwenden

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)

Die Tests in https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb geben Ihnen weitere Beispiele für die Dinge, die ich getestet habe, um sicherzustellen, dass die Bewertung sicher ist.

bibstha
quelle
2

Bitte beachten Sie diese Lösung. Bibliothek + Spezifikation:

Datei: 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

Datei: 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
quelle
1
it "generally works" aber nicht unbedingt? Ich würde in diesen Tests ausführlicher sein. it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex
Hey @Lex, welche Methode funktioniert, ist in ihrem RubyDoc-Kommentar beschrieben. Der Test sollte besser nicht erneut angegeben werden, da unnötige Details als passiver Text erstellt werden. Daher ist "im Allgemeinen funktioniert" eine nette Formel, um zu sagen, dass Zeug im Allgemeinen funktioniert. Prost!
Alex Fortuna
Ja, am Ende des Tages, was auch immer funktioniert. Alle Tests sind besser als keine Tests. Persönlich bin ich ein Fan von expliziten Beschreibungen, aber das ist nur eine Präferenz.
Lex
1

Ich bin auf diese Frage gekommen, nachdem ich zu diesem Zweck einen Einzeiler geschrieben habe, und teile meinen Code, falls er jemandem hilft. Funktioniert für einen String mit nur einer Ebenentiefe und möglichen leeren Werten (aber nicht Null) wie:

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

Der Code lautet:

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
quelle
0

Ist auf ein ähnliches Problem gestoßen, bei dem eval () verwendet werden musste.

In meiner Situation habe ich einige Daten aus einer API abgerufen und lokal in eine Datei geschrieben. Dann können Sie die Daten aus der Datei ziehen und den Hash verwenden.

Ich habe IO.read () verwendet, um den Inhalt der Datei in eine Variable einzulesen. In diesem Fall erstellt IO.read () es als String.

Verwenden Sie dann eval (), um den String in einen Hash zu konvertieren.

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

Auch nur um zu erwähnen, dass IO ein Vorfahr von File ist. Sie können also auch File.read verwenden, wenn Sie möchten.

TomG
quelle