Ruby: Parsing a string representation of nested ar

2020-01-29 08:10发布

问题:

Let's say I had the string

"[1,2,[3,4,[5,6]],7]"

How would I parse that into the array

[1,2,[3,4,[5,6]],7]

?

Nesting structures and patterns are completely arbitrary in my usage case.

My current ad-hoc solution involves adding a space after every period and using YAML.load, but I'd like to have a cleaner one if possible.

(One that does not require external libraries if possible)

回答1:

That particular example is being parsed correctly using JSON:

s = "[1,2,[3,4,[5,6]],7]"
#=> "[1,2,[3,4,[5,6]],7]"
require 'json'
#=> true
JSON.parse s
#=> [1, 2, [3, 4, [5, 6]], 7]

If that doesn't work, you can try running the string through eval, but you have to ensure that no actual ruby code has been passed, as eval could be used as injection vulnerability.

Edit: Here is a simple recursive, regex based parser, no validation, not tested, not for production use etc:

def my_scan s
  res = []
  s.scan(/((\d+)|(\[(.+)\]))/) do |match|
    if match[1]
      res << match[1].to_i
    elsif match[3]
      res << my_scan(match[3])
    end
  end
  res
end

s = "[1,2,[3,4,[5,6]],7]"
p my_scan(s).first #=> [1, 2, [3, 4, [5, 6]], 7]


回答2:

The same can be done using Ruby standard libaray YAML as below :

require 'yaml'
s = "[1,2,[3,4,[5,6]],7]"
YAML.load(s)
# => [1, 2, [3, 4, [5, 6]], 7]


回答3:

"Obviously" the best solution is to write your own parser. [ If you like writing parsers, have never done it before and want to learn something new, or want control over the exact grammar ]

require 'parslet'

class Parser < Parslet::Parser
  rule(:space)       { str(' ') }
  rule(:space?)      { space.repeat(0) }
  rule(:openbrace_)  { str('[').as(:op) >> space? }
  rule(:closebrace_) { str(']').as(:cl) >> space? }
  rule(:comma_)      { str(',') >> space?  }
  rule(:integer)     { match['0-9'].repeat(1).as(:int) }
  rule(:value)       { (array | integer) >> space? }
  rule(:list)        { value >> ( comma_ >> value ).repeat(0) }
  rule(:array)       { (openbrace_ >> list.maybe.as(:list) >> closebrace_ )}
  rule(:nest)        { space? >> array.maybe }
  root(:nest)
end

class Arr
  def initialize(args)
    @val = args
  end
  def val
    @val.map{|v| v.is_a?(Arr) ? v.val : v}
  end
end


class MyTransform < Parslet::Transform
  rule(:int => simple(:x))      { Integer(x) }
  rule(:op => '[', :cl => ']')  { Arr.new([]) }
  rule(:op => '[', :list => simple(:x), :cl => ']')   {  Arr.new([x]) }
  rule(:op => '[', :list => sequence(:x), :cl => ']')   { Arr.new(x) }
end

def parse(s)
  MyTransform.new.apply(Parser.new.parse(s)).val
end

parse " [   1  ,   2  ,  [  3  ,  4  ,  [  5   ,  6  , [ ]]   ]  ,  7  ]  "

Parslet transforms will match a single value as "simple" but if that value returns an array, you soon get arrays of arrays, then you have to start using subtree. returning objects however are fine as they represent a single value when transforming the layer above... so sequence will match fine.

Couple the trouble with returning bare arrays, with the problem that Array([x]) and Array(x) give you the same thing... and you get very confusing results.

To avoid this I made a helper class called Arr which represents an array of items. I could then dictate what I pass into it. Then I can get the parser to keep all the brackets even if you have the example that @MateuszFryc called out :) (thanks @MateuszFryc)



回答4:

Use eval

array = eval("[1,2,[3,4,[5,6]],7]")