Bagaimana cara mengubah objek String menjadi objek Hash?

138

Saya memiliki string yang terlihat seperti hash:

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

Bagaimana cara mendapatkan Hash darinya? Suka:

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

String bisa memiliki kedalaman bersarang. Ia memiliki semua properti bagaimana Hash yang valid diketik di Ruby.

Waseem
sumber
Saya pikir eval akan melakukan sesuatu di sini. Biar saya uji dulu. Sepertinya saya memposting pertanyaan terlalu dini. :)
Waseem
Ohh ya teruskan saja ke eval. :)
Waseem

Jawaban:

80

String yang dibuat dengan memanggil Hash#inspectdapat diubah kembali menjadi hash dengan memanggilnya eval. Namun, ini membutuhkan hal yang sama untuk menjadi benar untuk semua objek dalam hash.

Jika saya mulai dengan hash {:a => Object.new}, maka representasi stringnya adalah "{:a=>#<Object:0x7f66b65cf4d0>}", dan saya tidak dapat menggunakannya evaluntuk mengubahnya kembali menjadi hash karena #<Object:0x7f66b65cf4d0>bukan sintaks Ruby yang valid.

Namun, jika semua yang ada di hash adalah string, simbol, angka, dan array, itu harus berfungsi, karena mereka memiliki representasi string yang merupakan sintaks Ruby yang valid.

Ken Bloom
sumber
"jika yang ada di hash hanyalah string, simbol, dan angka,". Ini mengatakan banyak. Jadi saya dapat memeriksa validitas string yang akan di evaluated sebagai hash dengan memastikan bahwa pernyataan di atas valid untuk string itu.
Waseem
1
Ya, tetapi untuk melakukan itu Anda memerlukan parser Ruby lengkap, atau Anda perlu tahu dari mana asal string itu dan tahu bahwa itu hanya dapat menghasilkan string, simbol, dan angka. (Lihat juga jawaban Toms Mikoss tentang mempercayai isi string.)
Ken Bloom
13
Berhati-hatilah di mana Anda menggunakan ini. Menggunakan evaldi tempat yang salah adalah lubang keamanan yang sangat besar. Apa pun yang ada di dalam string, akan dievaluasi. Jadi bayangkan jika dalam API seseorang disuntikkanrm -fr
Pithikos
156

Untuk string yang berbeda, Anda dapat melakukannya tanpa menggunakan evalmetode berbahaya :

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
sumber
2
Jawaban ini harus dipilih untuk menghindari penggunaan eval.
Michael_Zhang
4
Anda juga harus mengganti nils, feJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke
136

Metode cepat dan kotor akan

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

Tapi itu memiliki implikasi keamanan yang parah.
Itu mengeksekusi apa pun yang diteruskan, Anda harus yakin 110% (seperti, setidaknya tidak ada input pengguna di mana pun di sepanjang jalan) itu hanya akan berisi hash yang terbentuk dengan benar atau bug yang tidak terduga / makhluk mengerikan dari luar angkasa mungkin mulai bermunculan.

Toms Mikoss
sumber
16
Saya memiliki pedang cahaya dengan saya. Saya bisa menjaga makhluk dan serangga itu. :)
Waseem
12
MENGGUNAKAN EVAL bisa berbahaya di sini, menurut guru saya. Eval mengambil kode ruby ​​dan menjalankannya. Bahaya di sini analog dengan bahaya injeksi SQL. Gsub lebih disukai.
boulder_ruby
9
Contoh string yang menunjukkan mengapa guru David benar: '{: surprise => "# {system \" rm -rf * \ "}"}'
A. Wilson
13
Saya tidak bisa cukup menekankan BAHAYA menggunakan EVAL di sini! Ini benar-benar dilarang jika input pengguna dapat masuk ke string Anda.
Dave Collins
Bahkan jika Anda pikir Anda tidak akan pernah membukanya secara lebih terbuka, orang lain mungkin. Kita semua (harus) tahu bagaimana kode digunakan dengan cara yang tidak Anda duga. Ini seperti meletakkan barang-barang yang sangat berat di rak yang tinggi, membuatnya menjadi berat. Anda seharusnya tidak pernah menciptakan bahaya seperti ini.
Steve Sether
24

Mungkin YAML.load?

diam
sumber
(metode beban mendukung string)
diam
5
Itu membutuhkan representasi string yang sama sekali berbeda, tetapi itu jauh lebih aman. (Dan representasi string juga mudah dibuat - cukup panggil #to_yaml, bukan #inspect)
Ken Bloom
Wow. Saya tidak tahu itu sangat mudah untuk mengurai string dengan yaml. Dibutuhkan rantai perintah linux bash saya yang menghasilkan data dan dengan cerdas mengubahnya menjadi hash ruby ​​tanpa memijat format string apa pun.
labirin
Ini dan to_yaml memecahkan masalah saya karena saya memiliki kendali atas cara string dihasilkan. Terima kasih!
mlabarca
23

Potongan kecil pendek ini akan melakukannya, tetapi saya tidak dapat melihatnya berfungsi dengan hash bersarang. Saya pikir itu cukup lucu

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

Langkah 1. Saya menghilangkan '{', '}' dan ':' 2. Saya membagi string di mana pun ia menemukan ',' 3. Saya membagi setiap substring yang dibuat dengan split, setiap kali ditemukan a '=>'. Kemudian, saya membuat hash dengan kedua sisi hash yang baru saja saya pisahkan. 4. Saya memiliki array hashes yang kemudian saya gabungkan.

CONTOH INPUT: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" HASIL KELUARAN: {"user_id" => "11", "blog_id" => "2", "comment_id" = > "1"}

hrdwdmrbl
sumber
1
Itu satu perjalanan yang sakit! :) +1
blushrt
3
Tidakkah ini juga akan menghapus {}:karakter dari nilai di dalam hash yang dirangkai?
Vladimir Panteleev
@VladimirPanteleev Anda benar, itu akan. Tangkapan bagus! Anda dapat melakukan tinjauan kode saya kapan saja :)
hrdwdmrbl
22

Solusi sejauh ini mencakup beberapa kasus tetapi melewatkan beberapa (lihat di bawah). Inilah upaya saya pada konversi yang lebih menyeluruh (aman). Saya tahu satu kasus sudut yang tidak ditangani oleh solusi ini yang merupakan simbol karakter tunggal yang terdiri dari karakter ganjil, tetapi diperbolehkan. Misalnya {:> => :<}adalah hash ruby ​​yang valid.

Saya memasang kode ini di github juga . Kode ini dimulai dengan string pengujian untuk menjalankan semua konversi

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)

Berikut beberapa catatan tentang solusi lain di sini

gene_wood
sumber
Solusi yang sangat keren. Anda bisa menambahkan gsub semua :niluntuk :nullke pegangan yang keanehan tertentu.
SteveTurczyn
1
Solusi ini juga memiliki bonus untuk mengerjakan hash multi-level secara rekursif, karena memanfaatkan JSON # parse. Saya mengalami masalah dalam mencari solusi lain.
Patrick Membaca
20

Saya memiliki masalah yang sama. Saya menyimpan hash di Redis. Saat mengambil hash itu, itu adalah string. Saya tidak ingin menelepon eval(str)karena masalah keamanan. Solusi saya adalah menyimpan hash sebagai string json, bukan string hash ruby. Jika Anda memiliki opsi, menggunakan json lebih mudah.

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

TL; DR: gunakan to_jsondanJSON.parse

Jared Menard
sumber
1
Sejauh ini, ini adalah jawaban terbaik. to_jsondanJSON.parse
ardochhigh
3
Untuk siapa pun yang merendahkan saya. Mengapa? Saya memiliki masalah yang sama, mencoba mengubah representasi string dari hash ruby ​​menjadi objek hash yang sebenarnya. Saya menyadari bahwa saya mencoba memecahkan masalah yang salah. Saya menyadari bahwa menyelesaikan pertanyaan yang diajukan di sini rawan kesalahan dan tidak aman. Saya menyadari bahwa saya perlu menyimpan data saya secara berbeda dan menggunakan format yang dirancang untuk membuat serial dan deserialisasi objek dengan aman. TL; DR: Saya memiliki pertanyaan yang sama dengan OP, dan menyadari bahwa jawabannya adalah menanyakan pertanyaan yang berbeda. Selain itu, jika Anda menolak saya, berikan masukan agar kita semua bisa belajar bersama.
Jared Menard
3
Downvoting tanpa komentar penjelasan adalah kanker dari Stack Overflow.
ardochhigh
1
yes downvoting harus membutuhkan penjelasan dan menunjukkan siapa yang downvoting.
Nick Res
2
Untuk membuat jawaban ini lebih dapat diterapkan pada pertanyaan OP, jika representasi string Anda dari hash disebut 'strungout', Anda harus dapat melakukan hashit = JSON.parse (strungout.to_json) dan kemudian memilih item Anda di dalam hashit melalui hashit [ 'keyname'] seperti biasa.
cixelsyd
12

Saya lebih suka menyalahgunakan ActiveSupport :: JSON. Pendekatan mereka adalah dengan mengubah hash ke yaml dan kemudian memuatnya. Sayangnya konversi ke yaml tidak sederhana dan Anda mungkin ingin meminjamnya dari AS jika Anda belum memiliki AS dalam proyek Anda.

Kami juga harus mengubah simbol apa pun menjadi kunci string biasa karena simbol tidak sesuai di JSON.

Namun, itu tidak dapat menangani hash yang memiliki string tanggal di dalamnya (string tanggal kami akhirnya tidak dikelilingi oleh string, di situlah masalah besar masuk):

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

Akan menghasilkan kesalahan string JSON yang tidak valid ketika mencoba mengurai nilai tanggal.

Akan sangat senang jika ada saran tentang cara menangani kasus ini

c. apolzon
sumber
2
Terima kasih atas penunjuk ke .decode, ini berfungsi dengan baik untuk saya. Saya perlu mengonversi respons JSON untuk mengujinya. Berikut kode yang saya gunakan:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips
9

berfungsi di rel 4.1 dan mendukung simbol tanpa tanda kutip {: a => 'b'}

cukup tambahkan ini ke folder penginisialisasi:

class String
  def to_hash_object
    JSON.parse(self.gsub(/:([a-zA-z]+)/,'"\\1"').gsub('=>', ': ')).symbolize_keys
  end
end
Eugene
sumber
Berfungsi pada baris perintah, tetapi saya mendapatkan "level tumpukan ke dalam" ketika saya memasukkan ini ke dalam penginisialisasi ...
Alex Edelstein
3

Harap pertimbangkan solusi ini. Perpustakaan + spesifikasi:

Berkas 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

Berkas 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
sumber
1
it "generally works" tapi belum tentu? Saya akan lebih bertele-tele dalam tes tersebut. it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex
Hai @Lex, metode apa yang dijelaskan dalam komentar RubyDoc-nya. Tes lebih baik tidak menyatakannya kembali, itu akan membuat detail yang tidak perlu sebagai teks pasif. Jadi, "secara umum berhasil" adalah rumus yang bagus untuk menyatakan bahwa hal-hal, secara umum berhasil. Bersulang!
Alex Fortuna
Ya, pada akhirnya apapun yang berhasil. Tes apa pun lebih baik daripada tidak ada tes. Secara pribadi saya penggemar deskripsi eksplisit, tapi itu hanya preferensi.
Lex
2

Saya membangun gem hash_parser yang pertama kali memeriksa apakah hash aman atau tidak digunakanruby_parser gem. Hanya kemudian, itu berlaku eval.

Anda dapat menggunakannya sebagai

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)

Pengujian di https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb memberi Anda lebih banyak contoh hal-hal yang telah saya uji untuk memastikan eval aman.

bibstha
sumber
1

Saya sampai pada pertanyaan ini setelah menulis satu baris untuk tujuan ini, jadi saya membagikan kode saya seandainya itu membantu seseorang. Berfungsi untuk string dengan hanya satu level kedalaman dan kemungkinan nilai kosong (tapi tidak nihil), seperti:

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

Kodenya adalah:

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

Menemukan masalah serupa yang perlu menggunakan eval ().

Situasi saya, saya menarik beberapa data dari API dan menulisnya ke file secara lokal. Kemudian bisa menarik data dari file dan menggunakan Hash.

Saya menggunakan IO.read () untuk membaca konten file menjadi variabel. Dalam hal ini IO.read () membuatnya sebagai String.

Kemudian digunakan eval () untuk mengubah string menjadi 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

Juga hanya untuk menyebutkan bahwa IO adalah nenek moyang File. Jadi Anda juga bisa menggunakan File.read jika Anda mau.

TomG
sumber