ฉันจะแปลงวัตถุ String เป็นวัตถุ Hash ได้อย่างไร


136

ฉันมีสตริงที่ดูเหมือนแฮช:

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

ฉันจะนำแฮชออกมาได้อย่างไร ชอบ:

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

สตริงสามารถมีความลึกของการซ้อน มันมีคุณสมบัติทั้งหมดที่วิธีการพิมพ์ Hash ที่ถูกต้องใน Ruby


ฉันคิดว่า eval จะทำอะไรที่นี่ ให้ฉันทดสอบก่อน ฉันโพสต์คำถามเร็วเกินไปที่ฉันคิด :)
Waseem

โอ้ใช่เพียงแค่ส่งผ่านไปยัง Eval :)
Waseem

คำตอบ:


79

สตริงที่สร้างขึ้นโดยการโทรHash#inspectสามารถเปลี่ยนกลับเป็นแฮชได้โดยการเรียกevalมัน อย่างไรก็ตามสิ่งนี้ต้องการสิ่งเดียวกันที่เป็นจริงของวัตถุทั้งหมดในแฮช

หากฉันเริ่มต้นด้วยแฮช{:a => Object.new}การแสดงสตริงของมันคือ"{:a=>#<Object:0x7f66b65cf4d0>}"และฉันไม่สามารถใช้evalเพื่อแปลงกลับเป็นแฮชได้เนื่องจาก#<Object:0x7f66b65cf4d0>ไวยากรณ์ของ Ruby ไม่ถูกต้อง

อย่างไรก็ตามหากทั้งหมดที่อยู่ในแฮชคือสตริงสัญลักษณ์ตัวเลขและอาร์เรย์มันควรใช้งานได้เพราะสิ่งเหล่านั้นมีการแทนค่าสตริงที่เป็นไวยากรณ์รูบีที่ถูกต้อง


"ถ้าทั้งหมดที่อยู่ในแฮชคือสตริงสัญลักษณ์และตัวเลข" เรื่องนี้พูดมาก ดังนั้นฉันสามารถตรวจสอบความถูกต้องของสตริงที่จะevaluated เป็นแฮโดยการทำให้แน่ใจว่าคำสั่งดังกล่าวถูกต้องสำหรับสตริงที่
Waseem

1
ใช่ แต่เพื่อให้คุณต้องใช้ตัวแยกวิเคราะห์ Ruby แบบเต็มหรือคุณต้องรู้ว่าสตริงมาจากที่ใดในตอนแรกและรู้ว่ามันสามารถสร้างสตริงสัญลักษณ์และตัวเลขได้เท่านั้น (โปรดดูคำตอบ Toms Mikoss ไว้วางใจเกี่ยวกับเนื้อหาของสตริง.)
เคนบลูม

13
ระวังที่คุณใช้สิ่งนี้ การใช้evalงานผิดที่เป็นช่องโหว่ขนาดใหญ่ สิ่งใดภายในสตริงจะถูกประเมิน ลองจินตนาการดูว่าถ้ามีใครบางคนฉีด APIrm -fr
Pithikos

153

สำหรับสตริงที่แตกต่างกันคุณสามารถทำได้โดยไม่ต้องใช้evalวิธีอันตราย:

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
ควรเลือกคำตอบนี้เพื่อหลีกเลี่ยงการใช้ eval
Michael_Zhang

4
คุณควรจะแทนที่นิลส์, feJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Yo Ludke

136

วิธีที่รวดเร็วและสกปรกจะเป็น

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

แต่มันมีผลกระทบด้านความปลอดภัยอย่างรุนแรง
มันดำเนินการสิ่งที่ผ่านคุณจะต้องแน่ใจว่า 110% (อย่างน้อยก็ไม่มีการป้อนข้อมูลของผู้ใช้) มันจะมีเพียงแฮ็คที่เกิดขึ้นอย่างถูกต้องหรือข้อบกพร่องที่ไม่คาดคิด / สิ่งมีชีวิตที่น่ากลัวจากอวกาศ


16
ฉันมีกระบี่แสงกับฉัน ฉันสามารถดูแลสิ่งมีชีวิตและแมลงเหล่านั้นได้ :)
Waseem

12
การใช้ EVAL อาจเป็นอันตรายได้ที่นี่ตามครูของฉัน Eval ใช้รหัสทับทิมใด ๆ และเรียกใช้ อันตรายที่นี่คล้ายกับอันตรายจากการฉีด SQL Gsub เป็นที่ต้องการ
boulder_ruby

9
ตัวอย่างสตริงแสดงสาเหตุที่อาจารย์ของดาวิดถูกต้อง: '{: surprise => "# {system \" rm -rf * \ "}"}'
A. Wilson

13
ฉันไม่สามารถเน้นถึงอันตรายของการใช้ EVAL ได้ที่นี่พอ! สิ่งนี้เป็นสิ่งต้องห้ามหากข้อมูลที่ผู้ใช้ป้อนเข้ามาในสายของคุณ
เดฟคอลลินส์

แม้ว่าคุณจะคิดว่าคุณจะไม่เปิดเผยสิ่งนี้ต่อสาธารณะมากกว่าคนอื่นอาจทำได้ เราทุกคนควรรู้ว่าโค้ดได้รับการใช้ในวิธีที่คุณไม่คาดคิด มันเหมือนกับการวางของหนัก ๆ บนชั้นสูงทำให้มันหนักหน่วง คุณไม่ควรสร้างอันตรายแบบนี้
Steve Sether

24

อาจจะโหลด YAML?


(วิธีการโหลดรองรับสตริง)
เงียบ

5
ต้องใช้การแสดงสตริงที่แตกต่างกันโดยสิ้นเชิง แต่ก็ปลอดภัยกว่า (และการแสดงสตริงเป็นเพียงเป็นเรื่องง่ายที่จะสร้าง - #to_yaml โทรเพียงมากกว่า #inspect)
เคนบลูม

ว้าว. ฉันไม่รู้ว่ามันเป็นเรื่องง่ายมากที่จะแยกสตริงด้วย yaml ใช้โซ่คำสั่ง linux bash ของฉันที่สร้างข้อมูลและแปลงมันให้เป็น ruby ​​Hash โดยไม่ต้องนวดสตริงรูปแบบใด ๆ
เขาวงกต

สิ่งนี้และ to_yaml แก้ปัญหาของฉันเนื่องจากฉันมีการควบคุมวิธีสร้างสตริง ขอบคุณ!
mlabarca

23

ตัวอย่างข้อมูลสั้น ๆ นี้จะทำ แต่ฉันไม่เห็นว่ามันทำงานกับแฮชซ้อน ฉันคิดว่ามันน่ารักดี

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

ขั้นตอนที่ 1. ฉันกำจัด '{', '}' และ ':' 2. ฉันแยกสตริงที่ใดก็ตามที่พบ ',' 3. ฉันแยกสตริงย่อยที่สร้างด้วยการแยกเมื่อใดก็ตามที่พบ a '=>' จากนั้นฉันสร้างแฮชที่มีสองด้านของแฮชที่เพิ่งแยกออก 4. ฉันถูกทิ้งให้อยู่กับชุดของแฮชซึ่งฉันจะรวมเข้าด้วยกัน

ตัวอย่างอินพุต: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" ผลลัพธ์ของผลลัพธ์: {"user_id" => "11", "blog_id" => "2", "comment_id" = > "1"}


1
นั่นเป็นหนึ่งในผู้ป่วยไข้คนเดียว! :) +1
blushrt

3
สิ่งนี้จะไม่ตัด{}:อักขระออกจากค่าภายในแฮชที่ทำให้เป็นสตริงหรือไม่
Vladimir Panteleev

@VladimirPanteleev คุณพูดถูก รับได้สวย! คุณสามารถทำรีวิวรหัสของฉันได้ทุกวัน :)
hrdwdmrbl

20

การแก้ปัญหาจนถึงบางกรณี แต่พลาดบางอย่าง (ดูด้านล่าง) นี่คือความพยายามของฉันในการแปลงที่ปลอดภัยมากขึ้น ฉันรู้กรณีมุมหนึ่งซึ่งโซลูชันนี้ไม่จัดการซึ่งเป็นสัญลักษณ์อักขระเดียวที่ประกอบขึ้นจากแปลก แต่ได้รับอนุญาตอักขระ ตัวอย่างเช่น{:> => :<}แฮช ruby ​​ที่ถูกต้อง

ฉันใส่นี้รหัสขึ้นบน GitHub เช่นกัน รหัสนี้เริ่มต้นด้วยสตริงทดสอบเพื่อฝึกแปลงทั้งหมด

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)

นี่คือหมายเหตุเกี่ยวกับโซลูชันอื่น ๆ ที่นี่


ทางออกที่ยอดเยี่ยมมาก คุณสามารถเพิ่ม gsub ของทุกคน:nilที่จะ:nullไปจับที่แปลกประหลาดโดยเฉพาะอย่างยิ่ง
SteveTurczyn

1
โซลูชันนี้ยังมีโบนัสของการทำงานกับแฮชหลายระดับแบบเรียกซ้ำเนื่องจากใช้ประโยชน์จาก JSON # parse ฉันมีปัญหากับการทำรังในโซลูชันอื่น ๆ
Patrick อ่าน

17

ผมมีปัญหาเหมือนกัน. ฉันกำลังเก็บแฮชใน Redis เมื่อดึงข้อมูลแฮชนั่นเป็นสตริง ฉันไม่ต้องการโทรeval(str)เพราะกังวลเรื่องความปลอดภัย ทางออกของฉันคือการบันทึกแฮชเป็นสตริง json แทนที่จะเป็นสตริง ruby ​​hash หากคุณมีตัวเลือกการใช้ json นั้นง่ายกว่า

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

TL; DR: ใช้to_jsonและJSON.parse


1
นี่คือคำตอบที่ดีที่สุด to_jsonและJSON.parse
ardochhigh

3
ใครก็ตามที่ลงคะแนนฉัน ทำไม? ฉันมีปัญหาเดียวกันพยายามแปลงสตริงที่เป็นตัวแทนของแฮชทับทิมเป็นวัตถุแฮชจริง ฉันรู้ว่าฉันพยายามแก้ไขปัญหาที่ผิด ฉันตระหนักว่าการแก้คำถามที่ถามที่นี่เป็นข้อผิดพลาดและไม่ปลอดภัย ฉันรู้ว่าฉันต้องการจัดเก็บข้อมูลของฉันแตกต่างกันและใช้รูปแบบที่ออกแบบมาเพื่อทำให้เป็นอันดับและปลอดภัยต่อวัตถุ TL: DR: ฉันมีคำถามเดียวกันกับ OP และตระหนักว่าคำตอบคือถามคำถามอื่น นอกจากนี้หากคุณลงคะแนนให้ฉันโปรดให้ข้อเสนอแนะเพื่อให้เราทุกคนสามารถเรียนรู้ร่วมกัน
Jared Menard

3
Downvoting โดยไม่มีความคิดเห็นอธิบายเป็นมะเร็งของ Stack Overflow
ardochhigh

1
ใช่ downvoting ควรจะต้องมีคำอธิบายและแสดงว่าใคร downvotes
Nick Res

2
เพื่อให้คำตอบนี้ใช้ได้กับคำถามของ OP มากขึ้นหากการแสดงสตริงของคุณของแฮชเรียกว่า 'strungout' คุณควรจะ hashit = JSON.parse (strungout.to_json) จากนั้นเลือกรายการของคุณใน hashit ผ่าน hashit [ 'keyname'] ตามปกติ
cixelsyd

11

ฉันชอบที่จะละเมิด ActiveSupport :: JSON วิธีการของพวกเขาคือแปลงแฮชเป็น yaml แล้วโหลดมัน น่าเสียดายที่การแปลงเป็น yaml นั้นไม่ใช่เรื่องง่ายและคุณอาจต้องการยืมมันจาก AS หากคุณไม่มี AS ในโครงการของคุณอยู่แล้ว

นอกจากนี้เรายังต้องแปลงสัญลักษณ์ใด ๆ ให้เป็นสตริงแบบปกติเนื่องจากสัญลักษณ์ไม่เหมาะสมใน JSON

อย่างไรก็ตามมันไม่สามารถจัดการกับแฮชที่มีสตริงวันที่ได้ (สตริงวันที่ของเราสิ้นสุดลงไม่ได้ถูกล้อมรอบด้วยสตริงซึ่งเป็นที่ที่ปัญหาใหญ่เข้ามา):

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

จะส่งผลให้เกิดข้อผิดพลาดสตริง JSON ที่ไม่ถูกต้องเมื่อพยายามวิเคราะห์ค่าวันที่

จะรักคำแนะนำใด ๆ เกี่ยวกับวิธีการจัดการกรณีนี้


2
ขอบคุณสำหรับตัวชี้ไปที่. รหัสมันใช้งานได้ดีสำหรับฉัน ฉันต้องการแปลงการตอบสนอง JSON เพื่อทดสอบ นี่คือรหัสที่ฉันใช้:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Andrew Philips

9

ทำงานในราง 4.1 และสัญลักษณ์สนับสนุนโดยไม่มีเครื่องหมายคำพูด {: a => 'b'}

เพียงเพิ่มลงในโฟลเดอร์ initializers:

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

ผลงานในบรรทัดคำสั่ง แต่ฉันได้รับ "สแต็คระดับลึก" เมื่อฉันใส่นี้ในการ intializer ...
อเล็กซ์ Edelstein

2

ฉันสร้าง gem hash_parserเพื่อตรวจสอบว่าแฮชนั้นปลอดภัยหรือไม่ใช้ruby_parsergem evalเท่านั้นจากนั้นก็นำไปใช้

คุณสามารถใช้มันเป็น

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ให้ตัวอย่างเพิ่มเติมของสิ่งที่ฉันทดสอบเพื่อให้แน่ใจว่า eval ปลอดภัย


2

โปรดพิจารณาวิธีนี้ + ข้อมูลจำเพาะห้องสมุด

ไฟล์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

ไฟล์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" แต่ไม่จำเป็นต้อง? ฉันจะพูดอย่างละเอียดมากขึ้นในการทดสอบเหล่านั้น it "converts strings to object" { expect('...').to eql ... } it "supports nested objects" { expect('...').to eql ... }
Lex

เฮ้ @Lex วิธีใดที่อธิบายไว้ในความคิดเห็น RubyDoc การทดสอบที่ดีกว่าไม่ระบุอีกครั้งมันจะสร้างรายละเอียดที่ไม่จำเป็นเป็นข้อความแฝง ดังนั้น "โดยทั่วไปทำงาน" เป็นสูตรที่ดีในการระบุสิ่งที่ดีโดยทั่วไปได้ผล ไชโย!
Alex Fortuna

ใช่ในตอนท้ายของวันทำงานอะไรก็ได้ การทดสอบใด ๆ นั้นดีกว่าการไม่ทดสอบ โดยส่วนตัวฉันเป็นแฟนของคำอธิบายที่ชัดเจน แต่นั่นเป็นเพียงการตั้งค่า
Lex

1

ฉันมาถึงคำถามนี้หลังจากเขียนหนึ่งซับเพื่อจุดประสงค์นี้ดังนั้นฉันแบ่งปันรหัสของฉันในกรณีที่ช่วยใครบางคน ใช้งานได้กับสตริงที่มีความลึกระดับเดียวและค่าว่างที่เป็นไปได้ (แต่ไม่ใช่ศูนย์) เช่น:

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

รหัสคือ:

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 ()

สถานการณ์ของฉันฉันกำลังดึงข้อมูลบางส่วนจาก API และเขียนลงในไฟล์ในเครื่อง จากนั้นความสามารถในการดึงข้อมูลจากไฟล์และใช้แฮช

ฉันใช้ IO.read () เพื่ออ่านเนื้อหาของไฟล์ลงในตัวแปร ในกรณีนี้ IO.read () สร้างเป็นสตริง

จากนั้นใช้ eval () เพื่อแปลงสตริงเป็น 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

นอกจากนี้ยังพูดถึงว่า IO เป็นบรรพบุรุษของไฟล์ ดังนั้นคุณสามารถใช้ File.read แทนหากคุณต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.