Jak przekonwertować obiekt String na obiekt Hash?

136

Mam ciąg, który wygląda jak hash:

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

Jak uzyskać z tego hash? lubić:

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

Ciąg może mieć dowolną głębokość zagnieżdżenia. Ma wszystkie właściwości wpisywania prawidłowego skrótu w Rubim.

Waseem
źródło
Myślę, że eval coś tutaj zrobi. Najpierw przetestuję. Myślę, że opublikowałem to pytanie za wcześnie. :)
Waseem
O tak, po prostu przekaż to do eval. :)
Waseem

Odpowiedzi:

79

Ciąg utworzony przez wywołanie Hash#inspectmożna przekształcić z powrotem w hash, wywołując evalgo. Jednak wymaga to tego samego dla wszystkich obiektów w skrócie.

Jeśli zacznę od skrótu {:a => Object.new}, to jego reprezentacja w postaci ciągu to "{:a=>#<Object:0x7f66b65cf4d0>}"i nie mogę użyć go, evalaby zmienić go z powrotem w hash, ponieważ #<Object:0x7f66b65cf4d0>nie jest poprawna składnia Ruby.

Jeśli jednak wszystko, co znajduje się w haszu, to łańcuchy, symbole, liczby i tablice, powinno to działać, ponieważ mają one reprezentacje ciągów, które są poprawną składnią Rubiego.

Ken Bloom
źródło
„jeśli wszystko, co zawiera hash, to łańcuchy znaków, symbole i liczby”. To dużo mówi. Mogę więc sprawdzić poprawność ciągu, który ma być traktowany evaljako hash, upewniając się, że powyższa instrukcja jest ważna dla tego ciągu.
Waseem
1
Tak, ale aby to zrobić, potrzebujesz albo pełnego parsera Rubiego, albo musisz przede wszystkim wiedzieć, skąd pochodzi łańcuch i wiedzieć, że może on generować tylko łańcuchy, symbole i liczby. (Zobacz także odpowiedź Toma Mikossa na temat zaufania do zawartości ciągu.)
Ken Bloom,
13
Uważaj, gdzie tego używasz. Używanie evalw złym miejscu to ogromna luka w zabezpieczeniach. Wszystko, co znajduje się w ciągu, zostanie ocenione. Wyobraź sobie więc, że ktoś wstrzyknął w APIrm -fr
Pithikos
153

Dla innego ciągu możesz to zrobić bez użycia niebezpiecznej evalmetody:

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
źródło
2
Tę odpowiedź należy wybrać, aby uniknąć używania eval.
Michael_Zhang
4
należy również zastąpić nils, np.JSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke
136

Byłaby to szybka i brudna metoda

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

Ale ma to poważne konsekwencje dla bezpieczeństwa.
Wykonuje wszystko, co zostanie przekazane, musisz mieć 110% pewności (tak jak w przypadku, gdy nigdzie nie ma żadnych danych wejściowych użytkownika), że zawierałyby tylko poprawnie uformowane skróty lub nieoczekiwane błędy / straszne stworzenia z kosmosu mogą zacząć pojawiać się.

Toms Mikoss
źródło
16
Mam ze sobą świetlną szablę. Mogę zająć się tymi stworzeniami i owadami. :)
Waseem
12
Według mojego nauczyciela UŻYWANIE EVAL może być tutaj niebezpieczne. Eval pobiera dowolny kod ruby ​​i uruchamia go. Niebezpieczeństwo tutaj jest analogiczne do niebezpieczeństwa iniekcji SQL. Preferowany jest Gsub.
boulder_ruby
9
Przykładowy ciąg znaków pokazujący, dlaczego nauczyciel Davida jest poprawny: '{: surprise => "# {system \" rm -rf * \ "}"}'
A. Wilson
13
Nie mogę wystarczająco podkreślić NIEBEZPIECZEŃSTWA używania EVAL! Jest to absolutnie zabronione, jeśli dane wejściowe użytkownika mogą kiedykolwiek trafić do twojego łańcucha.
Dave Collins,
Nawet jeśli myślisz, że nigdy nie otworzysz tego bardziej publicznie, może to zrobić ktoś inny. Wszyscy (powinniśmy) wiedzieć, jak kod jest używany w sposób, którego nie spodziewałeś się. To tak, jakby umieszczać wyjątkowo ciężkie rzeczy na wysokiej półce, czyniąc je ciężkimi. Po prostu nigdy nie powinieneś stwarzać takiej formy zagrożenia.
Steve Sether
24

Może YAML.load?

cichy
źródło
(metoda ładowania obsługuje ciągi znaków)
cichy
5
Wymaga to zupełnie innej reprezentacji ciągu, ale jest dużo, dużo bezpieczniejsze. (A reprezentacja ciągu jest równie łatwa do wygenerowania - wystarczy zadzwonić na #to_yaml, zamiast #inspect)
Ken Bloom
Łał. Nie miałem pojęcia, że ​​tak łatwo jest analizować ciągi znaków w / yaml. Potrzeba mojego łańcucha poleceń linux bash, które generują dane i inteligentnie zamieniają je w ruby ​​Hash bez masowania formatu ciągów.
labirynt
To i to_yaml rozwiązuje mój problem, ponieważ mam pewną kontrolę nad sposobem generowania ciągu. Dzięki!
mlabarca
23

Ten krótki fragment wystarczy, ale nie widzę, żeby działał z zagnieżdżonym hashem. Myślę, że to całkiem urocze

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

Kroki 1. Eliminuję „{”, „}” i „:” 2. Dzielę łańcuch wszędzie tam, gdzie znajdzie „,” 3. Dzielę każdy z podciągów, które zostały utworzone za pomocą podziału, gdy tylko znajdzie a '=>'. Następnie tworzę hash z dwiema stronami haszyszu, które właśnie rozdzieliłem. 4. Zostaje mi tablica skrótów, które następnie łączę.

PRZYKŁADOWE WPROWADZENIE: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" WYNIKI WYNIKÓW: {"user_id" => "11", "blog_id" => "2", "comment_id" = > „1”}

hrdwdmrbl
źródło
1
To jeden chory oneliner! :) +1
blushrt
3
Czy nie spowoduje to również usunięcia {}:znaków z wartości wewnątrz ciągłego skrótu?
Vladimir Panteleev
@VladimirPanteleev Masz rację, tak. Dobry chwyt! Możesz przeglądać mój kod każdego dnia :)
hrdwdmrbl
20

Dotychczasowe rozwiązania obejmują niektóre przypadki, ale niektóre pomijają (patrz poniżej). Oto moja próba dokładniejszej (bezpieczniejszej) konwersji. Znam jeden przypadek narożny, którego to rozwiązanie nie obsługuje, czyli symbole jednoznakowe złożone z dziwnych, ale dozwolonych znaków. Na przykład{:> => :<} jest to poprawny hash ruby.

Umieściłem ten kod również na githubie . Ten kod zaczyna się od ciągu testowego, aby wykonać wszystkie konwersje

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)

Oto kilka uwag na temat innych rozwiązań tutaj

gene_wood
źródło
Bardzo fajne rozwiązanie. Można dodać gsub wszystkim :nilaby :nulldo uchwytu danym tajemniczości.
SteveTurczyn
1
Zaletą tego rozwiązania jest również rekurencyjna praca z wielopoziomowymi skrótami, ponieważ wykorzystuje analizę JSON #. Miałem problemy z zagnieżdżaniem się na innych rozwiązaniach.
Patrick Przeczytaj
17

Miałem ten sam problem. Przechowywałem haszysz w Redis. Podczas pobierania tego skrótu był to ciąg. Nie chciałem dzwonić eval(str)ze względów bezpieczeństwa. Moim rozwiązaniem było zapisanie skrótu jako łańcucha json zamiast ruby. Jeśli masz taką opcję, używanie json jest łatwiejsze.

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

TL; DR: użyj to_jsoniJSON.parse

Jared Menard
źródło
1
To zdecydowanie najlepsza odpowiedź. to_jsoniJSON.parse
ardochhigh
3
Ktokolwiek mnie zlekceważył. Czemu? Miałem ten sam problem, próbując przekonwertować ciąg znaków reprezentujący hash ruby ​​na rzeczywisty obiekt skrótu. Zdałem sobie sprawę, że próbuję rozwiązać niewłaściwy problem. Zdałem sobie sprawę, że rozwiązanie zadanego tutaj pytania było podatne na błędy i niepewne. Zdałem sobie sprawę, że muszę inaczej przechowywać dane i używać formatu, który jest przeznaczony do bezpiecznej serializacji i deserializacji obiektów. TL; DR: Miałem to samo pytanie co OP i zdałem sobie sprawę, że odpowiedzią było zadanie innego pytania. Ponadto, jeśli głosujesz na mnie negatywnie, prześlij opinię, abyśmy mogli razem uczyć się.
Jared Menard
3
Głosowanie w dół bez komentarza wyjaśniającego to rak przepełnienia stosu.
ardochhigh
1
tak, głosowanie przeciw powinno wymagać wyjaśnienia i pokazać, kto głosuje przeciw.
Nick Res
2
Aby uczynić tę odpowiedź jeszcze bardziej stosowną do pytania OP, jeśli twoja reprezentacja ciągu hash nazywa się „strungout”, powinieneś być w stanie zrobić hashit = JSON.parse (strungout.to_json), a następnie wybrać elementy wewnątrz hashitu za pomocą hashit [ „nazwa klucza”] jak zwykle.
cixelsyd
11

Wolę nadużywać ActiveSupport :: JSON. Ich podejście polega na zamianie haszyszu na yaml, a następnie załadowaniu go. Niestety konwersja do yaml nie jest prosta i prawdopodobnie chciałbyś pożyczyć ją od AS, jeśli nie masz jeszcze AS w swoim projekcie.

Musimy także przekonwertować wszystkie symbole na zwykłe klucze łańcuchowe, ponieważ symbole nie są odpowiednie w JSON.

Jednak nie jest w stanie obsłużyć skrótów, które zawierają ciąg daty (nasze ciągi daty nie są otoczone ciągami, co jest przyczyną dużego problemu):

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

Spowoduje to błąd nieprawidłowego ciągu JSON podczas próby przeanalizowania wartości daty.

Chciałbym mieć jakiekolwiek sugestie, jak sobie z tym poradzić

c.apolzon
źródło
2
Dzięki za wskaźnik do .decode, działało świetnie. Musiałem przekonwertować odpowiedź JSON, aby ją przetestować. Oto kod, którego użyłem:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips,
9

działa w railsach 4.1 i obsługuje symbole bez cudzysłowów {: a => 'b'}

po prostu dodaj to do folderu inicjalizatorów:

class String
  def to_hash_object
    JSON.parse(self.gsub(/:([a-zA-z]+)/,'"\\1"').gsub('=>', ': ')).symbolize_keys
  end
end
Eugene
źródło
Prace na linii poleceń, ale pojawia się „do poziomu stos głęboki”, kiedy kładę to w intializer ...
Alex Edelstein
2

Zbudowałem gem hash_parser, który najpierw sprawdza, czy hash jest bezpieczny, czy nie używa ruby_parsergem. Dopiero wtedy stosuje eval.

Możesz go używać jako

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)

Testy na https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb dają więcej przykładów rzeczy, które przetestowałem, aby upewnić się, że eval jest bezpieczny.

bibstha
źródło
2

Prosimy o rozważenie tego rozwiązania. Biblioteka + specyfikacja:

Plik 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

Plik 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
źródło
1
it "generally works" ale niekoniecznie? Byłbym bardziej rozwlekły w tych testach. it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex,
Hej @Lex, jaka metoda działa, jest opisana w komentarzu RubyDoc. Test lepiej nie powtarzać tego, utworzy niepotrzebne szczegóły jako tekst pasywny. Tak więc „ogólnie działa” to fajny wzór na stwierdzenie, że rzeczy, no cóż, ogólnie działają. Twoje zdrowie!
Alex Fortuna,
Tak, na koniec dnia cokolwiek zadziała. Jakiekolwiek testy są lepsze niż brak testów. Osobiście jestem fanem jednoznacznych opisów, ale to tylko preferencja.
Lex,
1

Doszedłem do tego pytania po napisaniu w tym celu jednej linijki, więc udostępniam swój kod na wypadek, gdyby to komuś pomogło. Działa dla łańcucha z tylko jedną głębokością poziomu i możliwymi wartościami pustymi (ale nie zerowymi), na przykład:

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

Kod to:

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
źródło
0

Natrafiłem na podobny problem, który wymagał użycia funkcji eval ().

W mojej sytuacji pobierałem pewne dane z API i zapisywałem je lokalnie do pliku. Następnie można pobrać dane z pliku i użyć skrótu.

Użyłem IO.read () do wczytania zawartości pliku do zmiennej. W tym przypadku IO.read () tworzy go jako ciąg.

Następnie użył eval () do konwersji ciągu znaków na 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

Wystarczy wspomnieć, że IO jest przodkiem File. Możesz więc zamiast tego użyć File.read, jeśli chcesz.

TomG
źródło