Как преобразовать объект String в объект Hash?

138

У меня есть строка, похожая на хеш:

"{ :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
О, да, просто передай это на оценку. :)
Waseem

Ответы:

80

Строку, созданную при вызове, Hash#inspectможно снова превратить в хэш, вызвав evalее. Однако для этого требуется, чтобы то же самое было верно для всех объектов в хэше.

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

Однако, если все, что находится в хэше, - это строки, символы, числа и массивы, он должен работать, потому что они имеют строковые представления, соответствующие синтаксису Ruby.

Кен Блум
источник
«если в хеше есть только строки, символы и числа». Это говорит о многом. Поэтому я могу проверить правильность строки, которая будет использоваться evalкак хэш, убедившись, что приведенное выше утверждение действительно для этой строки.
Waseem
1
Да, но для этого вам либо нужен полноценный синтаксический анализатор Ruby, либо вам нужно знать, откуда взялась строка, и знать, что он может генерировать только строки, символы и числа. (См. Также ответ Томса Микосса о доверии к содержимому строки.)
Кен Блум
13
Будьте осторожны, когда используете это. Использование evalв неправильном месте - огромная дыра в безопасности. Все, что находится внутри строки, будет оцениваться. Так что представьте, если бы в API кто-то ввелrm -fr
Питикос
156

Для другой строки это можно сделать без использования опасного 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
вам также следует заменить nils, 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 берет любой код Ruby и запускает его. Опасность здесь аналогична опасности SQL-инъекций. Gsub предпочтительнее.
boulder_ruby
9
Пример строки, показывающей, почему учитель Дэвида прав: '{: Surprise => "# {system \" rm -rf * \ "}"}'
А. Уилсон
13
Я не могу здесь достаточно подчеркнуть ОПАСНОСТЬ использования EVAL! Это абсолютно запрещено, если пользовательский ввод может когда-либо попасть в вашу строку.
Дэйв Коллинз
Даже если вы думаете, что никогда не раскроете это более публично, это может сделать кто-то другой. Мы все (должны) знать, как код используется так, как вы не ожидали. Это все равно, что ставить очень тяжелые вещи на высокую полку, делая ее сверху тяжелой. Вы просто никогда не должны создавать такую ​​опасность.
Стив Сетер
24

Может быть, YAML.load?

тихий
источник
(метод загрузки поддерживает строки)
silent
5
Это требует совершенно другого строкового представления, но намного безопаснее. (И строковое представление так же легко сгенерировать - просто вызовите #to_yaml, а не #inspect)
Кен Блум
Вау. Я понятия не имел, что так просто разбирать строки с yaml. Он берет мою цепочку команд linux bash, которые генерируют данные, и интеллектуально превращает их в рубиновый хэш без обработки любого строкового формата.
лабиринт
Это и to_yaml решают мою проблему, поскольку у меня есть некоторый контроль над способом генерации строки. Благодарность!
mlabarca
23

Этот небольшой фрагмент сделает это, но я не вижу, чтобы он работал с вложенным хешем. Я думаю это довольно мило

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

Шаги 1. Я удаляю '{', '}' и ':' 2. Я разделяю строку везде, где она находит ',' 3. Я разделяю каждую из подстрок, которые были созданы с помощью разделения, всякий раз, когда она находит а '=>'. Затем я создаю хеш с двумя сторонами хеша, которые я только что разделил. 4. У меня остается массив хешей, которые я затем объединяю.

ПРИМЕР ВВОДА: "{: user_id => 11,: blog_id => 2,: comment_id => 1}" ВЫВОД РЕЗУЛЬТАТА: {"user_id" => "11", "blog_id" => "2", "comment_id" = > "1"}

hrdwdmrbl
источник
1
Это одна больная штука! :) +1
blushrt
3
Не удалит ли это также {}:символы из значений внутри строкового хеша?
Владимир Пантелеев
@VladimirPanteleev Вы правы, было бы. Хорошо поймал! Вы можете сделать мой код ревью в любой день :)
hrdwdmrbl
22

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

Я также разместил этот код на 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)

Вот несколько заметок о других решениях здесь

gene_wood
источник
Очень крутое решение. Вы можете добавить GSUB всех :nilк :nullк ручке конкретной странности.
SteveTurczyn
1
Это решение также имеет преимущество рекурсивной работы с многоуровневыми хешами, поскольку оно использует синтаксический анализ JSON #. У меня были проблемы с вложением в другие решения.
Патрик Рид
20

У меня такая же проблема. Я хранил хеш в Redis. При получении этого хеша это была строка. Я не хотел звонить eval(str)из соображений безопасности. Мое решение заключалось в том, чтобы сохранить хэш как строку json вместо строки хеша рубина. Если у вас есть возможность, использовать json проще.

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

TL; DR: использовать to_jsonиJSON.parse

Джаред Менард
источник
1
На сегодняшний день это лучший ответ. to_jsonandJSON.parse
ardochhigh 02
3
Тем, кто проголосовал против меня. Зачем? У меня была такая же проблема, когда я пытался преобразовать строковое представление рубинового хэша в реальный хеш-объект. Я понял, что пытался решить не ту проблему. Я понял, что решение заданного здесь вопроса чревато ошибками и небезопасно. Я понял, что мне нужно хранить данные по-другому и использовать формат, предназначенный для безопасной сериализации и десериализации объектов. TL; DR: У меня был тот же вопрос, что и у OP, и я понял, что ответ - задать другой вопрос. Кроме того, если вы проголосуете против, оставьте отзыв, чтобы мы все вместе учились.
Джаред Менард,
3
Голосование против без пояснительного комментария - это рак Stack Overflow.
ardochhigh
1
да, голосование против должно требовать объяснения и показывать, кто голосует против.
Nick Res
2
Чтобы сделать этот ответ еще более применимым к вопросу OP, если ваше строковое представление хэша называется 'Strungout', вы должны иметь возможность сделать hashit = JSON.parse (strungout.to_json), а затем выбрать свои элементы внутри хеша с помощью hashit [ 'keyname'] как обычно.
cixelsyd
12

Я предпочитаю злоупотреблять ActiveSupport :: JSON. Их подход состоит в том, чтобы преобразовать хеш в yaml, а затем загрузить его. К сожалению, преобразование в yaml непросто, и вы, вероятно, захотите позаимствовать его у AS, если у вас еще нет AS в вашем проекте.

Мы также должны преобразовать любые символы в обычные строковые ключи, поскольку символы не подходят в JSON.

Однако он не может обрабатывать хэши, в которых есть строка даты (наши строки даты в конечном итоге не окружены строками, и здесь возникает большая проблема):

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

При попытке проанализировать значение даты приведет к ошибке неверной строки JSON.

Хотел бы получить предложения о том, как справиться с этим делом

c.apolzon
источник
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
Евгений
источник
Работает в командной строке, но я получаю "уровень стека до глубокого", когда помещаю это в инициализатор ...
Алекс Эдельштейн
3

Пожалуйста, рассмотрите это решение. Библиотека + спецификация:

Файл 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
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 безопасен.

Bibstha
источник
1

Я пришел к этому вопросу после того, как написал для этой цели однострочник, поэтому я делюсь своим кодом на случай, если он кому-то поможет. Работает для строки только с одной глубиной уровня и возможными пустыми значениями (но не с nil), например:

"{ :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 () создает его как строку.

Затем использовал 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, если хотите.

TomG
источник