Як перетворити об'єкт 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' } }

Рядок може мати будь-яку глибину гніздування. Він має всі властивості того, як в Ruby набирається дійсний хеш.


Я думаю, що eval щось тут зробить. Дозвольте спробувати спочатку. Я поставив це питання занадто рано, я думаю. :)
Waseem

О так, просто передай це на eval. :)
Waseem

Відповіді:


79

Рядок, створений викликом, Hash#inspectможе бути повернутий у хеш, зателефонувавши evalна нього. Однак для цього потрібно те саме, що стосується всіх об'єктів у хеші.

Якщо я розпочну з хеша {:a => Object.new}, то його представлення рядків є "{:a=>#<Object:0x7f66b65cf4d0>}", і я не можу використовувати його, evalщоб повернути його в хеш, тому що #<Object:0x7f66b65cf4d0>недійсний синтаксис Ruby.

Однак якщо все, що є в хеші, це рядки, символи, числа та масиви, воно повинно працювати, тому що в них є рядкові представлення, які є дійсним синтаксисом Ruby.


"якщо все, що є в хеші, - це рядки, символи та числа". Це багато говорить. Тож я можу перевірити дійсність рядка, який буде використовуватися evalяк хеш, переконавшись, що наведене вище твердження є дійсним для цього рядка.
Waseem

1
Так, але для цього вам або потрібен повний аналізатор Ruby, або вам потрібно знати в першу чергу, звідки походить рядок, і знати, що він може генерувати лише рядки, символи та цифри. (Дивіться також відповідь Томса Мікосса про довіру до вмісту рядка.)
Кен Блум

13
Будьте уважні, де ви цим користуєтеся. Використання evalв неправильному місці - це величезна безпека. Все, що знаходиться всередині рядка, буде оцінено. Тож уявіть, якби в API хтось rm -fr
зробив

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
ви також повинні замінити нулі, феJSON.parse(hash_as_string.gsub("=>", ":").gsub(":nil,", ":null,"))
Йо Людке

136

Швидкий і брудний метод був би

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

Але це має серйозні наслідки для безпеки.
Він виконує все, що він передається, ви повинні бути впевнені на 110% (як, наприклад, принаймні жодного вводу користувача ніде на шляху), він би містив лише правильно сформовані хеші, або несподівані помилки / жахливі істоти з космосу можуть почати спливати.


16
У мене світла шабля. Я можу подбати про тих істот і клопів. :)
Waseem

12
ВИКОРИСТАННЯ EVAL може бути тут небезпечним, на думку мого вчителя. Евал бере будь-який рубіновий код і запускає його. Тут небезпека аналогічна небезпеці введення SQL. Гсуб є кращим.
boulder_ruby

9
Приклад рядка, що показує, чому вчитель Девіда правильний: '{: iznenaня => "# {система \" rm -rf * \ "}"}'
А. Вілсон,

13
Я не можу підкреслити НЕБЕЗПЕЧЕННЯ використання EVAL тут досить! Це абсолютно заборонено, якщо введення користувача коли-небудь зможе потрапити у ваш рядок.
Дейв Коллінз

Навіть якщо ви думаєте, що ніколи не відкриєте це більш публічно, хтось ще може. Ми всі (повинні) знаємо, як використовується код таким способом, якого ви не очікували. Це як покласти надзвичайно важкі речі на високу полицю, зробивши її зверху важкою. Вам ніколи не слід створювати цю форму небезпеки.
Стів Сетер

24

Можливо, YAML.load?


(метод завантаження підтримує рядки)
беззвучний

5
Це вимагає зовсім іншого представлення рядків, але це набагато, набагато безпечніше. (І представлення рядків генерується так само просто - просто зателефонуйте #to_yaml, а не #inspect)
Кен Блум

Ого. Я не мав уявлення, що так легко розбирати рядки w / yaml. Це займає мою ланцюжок команд Linux bash, які генерують дані, та інтелектуально перетворює їх на рубіновий хеш без будь-якого масового формату струнного формату.
лабіринт

Це і 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
рум'янець

3
Хіба це також не позбавить {}:символів від значень всередині строгого хеша?
Володимир Пантелеев

@VladimirPanteleev Ти маєш рацію. Приємний улов! Ви можете робити огляди мого коду в будь-який день :)
hrdwdmrbl

20

Поки що рішення охоплюють деякі випадки, але деякі пропускають (див. Нижче). Ось моя спроба більш ретельного (безпечного) перетворення. Я знаю один кутовий випадок, який не вирішує це рішення, це символи, що складаються з одних символів, що складаються з непарних, але дозволених символів. Наприклад {:> => :<}, допустимий хебі-рубін.

Я також ставлю цей код на 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 # синтаксичний аналіз. У мене були проблеми з вкладенням на інші рішення.
Патрік

17

У мене була така ж проблема. Я зберігав хеш в Редісі. Коли ви отримували цей хеш, це був рядок. Мені не хотілося телефонувати eval(str)через проблеми безпеки. Моє рішення полягало в тому, щоб зберегти хеш як рядок json замість рубінової хеш-рядки. Якщо у вас є можливість, використовувати 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: У мене було те саме питання, що і в ОП, і я зрозумів, що відповідь - це задати інше питання. Крім того, якщо ви проголосуєте за мене, будь ласка, надайте відгуки, щоб ми могли разом вчитися.
Джаред Менар

3
Відхилення без пояснювальних коментарів є раком Стек Переповнення.
ardochhigh

1
так, заборона голосування повинна вимагати пояснення та показувати, хто виступає проти.
Нік Рез

2
Щоб зробити цю відповідь ще більш застосовною до питання ОП, якщо ваше рядкове представлення хеша називається "strungout", ви повинні мати змогу зробити hashit = JSON.parse (strungout.to_json), а потім вибрати предмети всередині хешиту через hashit [ 'keyname'] як звичайне.
cixelsyd

11

Я вважаю за краще зловживати ActiveSupport :: JSON. Їх підхід полягає в тому, щоб перетворити хеш в ямл і потім завантажити його. На жаль, перетворення на 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
Дякую за вказівник на .decode, він працював для мене чудово. Мені потрібно було перетворити відповідь JSON, щоб перевірити її. Ось код, який я використав:ActiveSupport::JSON.decode(response.body, symbolize_keys: true)
Ендрю Філіпс

9

працює в рейках 4.1 і підтримує символи без лапок {: a => 'b'}

просто додайте це до папки ініціалізаторів:

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

Працює в командному рядку, але я отримую "рівень стека на глибину", коли я поміщаю це в інструмент ...
Alex 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, що робить метод, описано в коментарі RubyDoc. Тест краще не повторювати, він створить непотрібні деталі як пасивний текст. Таким чином, "загалом працює" - це приємна формула, яка стверджує, що матеріал, ну, як правило, працює. Ура!
Алекс Фортуна

Так, наприкінці дня все, що працює. Будь-які тести краще, ніж тести. Особисто я прихильник явних описів, але це лише вподобання.
Лекс

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 і записував їх у файл локально. Потім зможете витягнути дані з файлу і скористатися Hash.

Я використовував IO.read (), щоб прочитати вміст файлу в змінну. У цьому випадку IO.read () створює його як String.

Потім використовується eval () для перетворення рядка в хеш.

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. Тому ви також можете використовувати File.read замість цього, якщо хочете.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.