Bir String nesnesini Hash nesnesine nasıl dönüştürebilirim?


137

Bir karma gibi görünüyor bir dize var:

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

Nasıl bir karma elde edebilirim? sevmek:

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

Dize herhangi bir yuvalama derinliğine sahip olabilir. Ruby'de geçerli bir Hash'in nasıl yazıldığı tüm özelliklere sahiptir.


Bence eval burada bir şeyler yapacak. Önce test edeyim. Soruyu çok erken yayınladım sanırım. :)
Waseem

Ohh evet sadece eval'e ilet. :)
Waseem

Yanıtlar:


79

Çağırarak oluşturulan dize, çağırarak Hash#inspectbir karmaya dönüştürülebilir eval. Ancak bu, karmadaki tüm nesneler için aynı şeyi gerektirir.

Karma ile başlarsam {:a => Object.new}, dize temsili olur "{:a=>#<Object:0x7f66b65cf4d0>}"ve geçerli Ruby sözdizimi olmadığı evaliçin tekrar karma haline dönüştürmek için #<Object:0x7f66b65cf4d0>kullanamam.

Bununla birlikte, karmadaki her şey dizeler, semboller, sayılar ve dizilerse işe yaramalıdır, çünkü bunların geçerli Ruby sözdizimi olan dize gösterimleri vardır.


msgstr "karmadaki tüm karakter dizgiler, semboller ve sayılarsa". Bu çok şey söylüyor. Bu nedenle eval, yukarıdaki ifadenin bu dize için geçerli olduğundan emin olarak karma olarak işlenecek bir dizenin geçerliliğini kontrol edebilirim .
Waseem

1
Evet, ancak bunu yapmak için ya tam bir Ruby ayrıştırıcısına ihtiyacınız var ya da dizenin nereden geldiğini bilmeniz ve sadece dizeler, semboller ve sayılar üretebileceğini bilmeniz gerekiyor. (Ayrıca bkz. Toms Mikoss'un dizenin içeriğine güvenme konusundaki cevabı.)
Ken Bloom

13
Bunu kullandığınız yerde dikkatli olun. evalYanlış yerde kullanmak büyük bir güvenlik deliğidir. Dize içindeki her şey değerlendirilir. Bir API'de birisinin enjekte edildiğini düşününrm -fr
Pithikos

153

Farklı dize için, tehlikeli evalyöntemi kullanmadan yapabilirsiniz :

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('=>', ':')

2
Bu cevap eval kullanmaktan kaçınmak için seçilmelidir.
Michael_Zhang

4
Ayrıca nils, feJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke

136

Hızlı ve kirli yöntem olurdu

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

Ancak ciddi güvenlik etkileri vardır.
Geçtiği her şeyi yürütür,% 110 emin olmalısınız (olduğu gibi, yol boyunca hiçbir yerde en az kullanıcı girişi yok), sadece düzgün bir şekilde oluşturulmuş karmaları veya dış alandan beklenmedik böcekleri / korkunç yaratıkların ortaya çıkmaya başlayabileceğinden emin olmalısınız.


16
Yanımda bir ışın kılıcım var. O yaratıklara ve böceklere bakabilirim. :)
Waseem

12
Öğretmenime göre burada EVAL kullanmak tehlikeli olabilir. Eval herhangi bir yakut kodu alır ve çalıştırır. Buradaki tehlike SQL enjeksiyon tehlikesine benzer. Gsub tercih edilir.
boulder_ruby

9
David'in öğretmeninin neden doğru olduğunu gösteren örnek dize: '{: surprise => "# {system \" rm -rf * \ "}"}'
A. Wilson

13
EVAL'ı burada yeterince kullanmanın TEHLİKESİNİ vurgulayamıyorum! Kullanıcı girişi dizginize girebiliyorsa, bu kesinlikle yasaktır.
Dave Collins

Bunu asla daha halka açmayacağınızı düşünseniz bile, başka biri açabilir. Hepimiz kodun beklediğiniz gibi nasıl kullanılacağını bilmeliyiz. Yüksek bir rafa aşırı ağır şeyler koymak, onu en ağır yapmak gibi. Asla bu tür bir tehlike yaratmamalısınız.
Steve Sether

24

Belki de YAML.load?


(yükleme yöntemi dizeleri destekler)
sessiz

5
Bu tamamen farklı bir dize temsili gerektirir, ancak çok, çok daha güvenli. (Ve dize temsili oluşturmak da kolaydır - sadece #inspect yerine #to_yaml çağırın)
Ken Bloom

Vay. Ben yaml w / dizeleri ayrıştırmak çok kolay olduğu hakkında hiçbir fikrim yoktu. Veri üreten linux bash komutları zincirimi alır ve akıllıca herhangi bir dize formatı masajı olmadan yakut bir Hash'e dönüştürür.
labirent

Bu ve to_yaml, dizenin oluşturulma şeklini biraz kontrol ettiğim için sorunumu çözdü. Teşekkürler!
mlabarca

23

Bu kısa küçük snippet bunu yapacak, ancak iç içe bir karma ile çalıştığını göremiyorum. Bence oldukça sevimli

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

Adımlar 1. '{', '}' ve ':' 2'yi elimine ediyorum. 2. ',' bulduğu yerde dizgiye ayrıldım. 3. Bölme ile oluşturulan alt dizelerin her birini, a '=>'. Sonra, sadece ayırdığım karma iki tarafı ile bir karma oluşturun. 4. Sonra bir araya karma bir dizi kaldı.

ÖRNEK GİRİŞ: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" SONUÇ ÇIKIŞI: {"user_id" => "11", "blog_id" => "2", "comment_id" = > "1"}


1
Bu bir hasta oneliner! :) +1
allık

3
Bu, {}:karakterleri dize edilmiş karma içindeki değerlerden ayırmaz mı?
Vladimir Panteleev

@VladimirPanteleev Haklısın, olur. İyi yakaladın! Kod incelemelerimi her gün yapabilirsiniz :)
hrdwdmrbl

20

Şimdiye kadarki çözümler bazı vakaları kapsamakta ancak bazılarını kaçırmaktadır (aşağıya bakınız). İşte daha kapsamlı (güvenli) dönüşüm denemem. Garip ama izin verilen karakterlerden oluşan tek karakter sembolleri olan bu çözümün işlemediği bir köşe durumunu biliyorum. Örneğin {:> => :<}geçerli bir yakut karmasıdır.

Bu kodu da github üzerine koydum . Bu kod, tüm dönüşümleri uygulamak için bir test dizesiyle başlar

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)

İşte buradaki diğer çözümlerle ilgili bazı notlar


Çok güzel bir çözüm. Bu tuhaflığı ele :nilalmak :nulliçin hepsinden bir gsub ekleyebilirsiniz .
SteveTurczyn

1
Bu çözüm, JSON # ayrıştırmasını kullandığından, çok düzeyli karmalar üzerinde tekrarlama çalışması bonusu da vardır. Diğer çözümlerle iç içe geçme konusunda sorun yaşadım.
Patrick

17

Ben de aynı problemi yaşadım. Redis'te bir karma saklıyordum. Bu karmayı alırken, bir dize idi. eval(str)Güvenlik kaygısı yüzünden aramak istemedim . Benim çözüm hash bir yakut karma dizesi yerine json dizesi olarak kaydetmek oldu. Seçeneğiniz varsa, json kullanmak daha kolaydır.

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

TL; DR: kullanın to_jsonveJSON.parse


1
Bu en iyi cevap. to_jsonveJSON.parse
ardochhigh

3
Beni kim düşürdüyse. Neden? Aynı sorun vardı, bir yakut karma bir dize temsilini gerçek bir karma nesneye dönüştürmek için çalışıyor. Yanlış problemi çözmeye çalıştığımı fark ettim. Burada sorulan sorunun çözülmesinin hataya açık ve güvensiz olduğunu fark ettim. Verilerimi farklı bir şekilde saklamam gerektiğini ve nesneleri güvenli bir şekilde serileştirmek ve serileştirmeyi kaldırmak için tasarlanmış bir format kullanmam gerektiğini fark ettim. TL; DR: OP ile aynı soruyu sordum ve cevabın farklı bir soru sormak olduğunu fark ettim. Ayrıca, eğer bana oy vermezseniz, hep birlikte öğrenebilmemiz için lütfen geri bildirim sağlayın.
Jared Menard

3
Açıklayıcı bir yorum olmadan aşağı oy, Stack Overflow kanseridir.
ardochhigh

1
evet downvoting bir açıklama gerektirmeli ve kimin downvotes olduğunu göstermelidir.
Nick Res

2
Bu cevabı OP'nin sorusuna daha da uygulanabilir hale getirmek için, bir karma dizesinin temsiline 'strungout' adı veriliyorsa hashit = JSON.parse (strungout.to_json) yapmalı ve sonra hashit içindeki öğelerinizi hashit [ 'keyname'] normal olarak.
cixelsyd

11

ActiveSupport :: JSON'u kötüye kullanmayı tercih ederim. Yaklaşımları hash'i yaml'ye dönüştürmek ve sonra yüklemek. Ne yazık ki yaml'ye dönüştürme basit değildir ve projenizde AS yoksa AS'den ödünç almak istersiniz.

Ayrıca JSON'da semboller uygun olmadığından herhangi bir sembolü normal dize anahtarlarına dönüştürmeliyiz.

Ancak, içinde bir tarih dizesi olan karmaları işleyemiyor (tarih dizelerimiz, büyük sorunun geldiği dizelerle çevrili değil):

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

Tarih değerini ayrıştırmaya çalıştığında geçersiz bir JSON dizesi hatasıyla sonuçlanır.

Bu davanın nasıl ele alınacağına dair herhangi bir öneri isterim


2
İşaretçiye .decode için teşekkürler, benim için harika çalıştı. Test etmek için bir JSON yanıtı dönüştürmek gerekiyordu. İşte kullandığım kod:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips

9

raylar 4.1'de çalışır ve tırnak işaretleri olmadan destek sembolleri {: a => 'b'}

sadece başlatıcılar klasörüne ekleyin:

class String
  def to_hash_object
    JSON.parse(self.gsub(/:([a-zA-z]+)/,'"\\1"').gsub('=>', ': ')).symbolize_keys
  end
end

Komut satırında İşleri, ama ben bir intializer ... bu koyduğunuzda "derin için yığın düzeyi" olsun
Alex Edelstein

2

İlk önce bir karma güvenli olup olmadığını kontrol eden bir mücevher hash_parser inşa etti ruby_parser. Ancak o zaman eval.

Olarak kullanabilirsiniz

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)

Https://github.com/bibstha/ruby_hash_parser/blob/master/test/test_hash_parser.rb'deki testler eval'ün güvenli olduğundan emin olmak için test ettiğim şeylere daha fazla örnek veriyor.


2

Lütfen bu çözümü düşünün. Kütüphane + Spec:

Dosya 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

Dosya 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

1
it "generally works" ama ille de değil mi? Bu testlerde daha ayrıntılı olurdum. it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex

Hey @Lex, hangi yöntemin RubyDoc yorumunda açıklanmıştır. Test tekrar belirtmese iyi olur, pasif metin olarak gereksiz ayrıntılar oluşturur. Bu nedenle, "genellikle işe yarar", genel olarak işe yarayan şeyleri belirtmek için güzel bir formüldür. Şerefe!
Alex Fortuna

Evet, günün sonunda ne işe yarıyorsa. Herhangi bir test, hiçbir testten daha iyidir. Şahsen ben açık açıklamaların hayranıyım, ama bu sadece bir tercih.
Lex

1

Bu amaçla bir astar yazdıktan sonra bu soruya geldim, bu yüzden birine yardım etmesi durumunda kodumu paylaşıyorum. Yalnızca bir düzey derinliğine ve olası boş değerlere (nil değil) sahip bir dize için çalışır, örneğin:

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

Kod:

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]}

0

Eval () kullanmak için gereken benzer bir sorun koştu.

Benim durumum, bir API bazı veri çekmek ve yerel olarak bir dosyaya yazıyordu. Sonra verileri dosyadan alıp Karma'yı kullanabilme.

Dosyanın içeriğini bir değişkene okumak için IO.read () kullandım. Bu durumda IO.read () yöntemi bir String olarak oluşturur.

Sonra dizeyi Hash'e dönüştürmek için eval () kullanılır.

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

Ayrıca, IO'nun Dosya'nın atası olduğunu da belirtmek gerekir. İsterseniz bunun yerine File.read'i de kullanabilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.