Ruby: 如何将字符串表示的嵌套数组解析为数组?

18

假设我有一个字符串

"[1,2,[3,4,[5,6]],7]"
我该如何将其解析成数组?
[1,2,[3,4,[5,6]],7]

?

在我的使用情况中,嵌套结构和模式是完全任意的。

我目前的临时解决方法是在每个句号后面加一个空格并使用YAML.load,但如果可能的话,我想有一个更清晰的解决方案。

(如果可能的话不需要外部库)

4个回答

44

使用 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]

如果那个方法不起作用,你可以尝试通过eval运行字符串,但是你必须确保没有传递实际的Ruby代码,因为eval可能被用作注入漏洞。

编辑:这是一个简单的递归,基于正则表达式的解析器,没有验证,没有测试,不适合生产使用等:

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]

我想使用这个,但是我无法使json在我的电脑上正常运行,而且无论如何,它也不会比yaml解决方案更干净。有没有一种手动编码这个解析的方法? - Justin L.
不确定您所说的“不干净”是什么意思,因为它只是一个方法调用来解析它。当然,您可以编写自己的简单基于正则表达式的解析器,或者使用专用工具,例如http://treetop.rubyforge.org/,但在我看来,这两种方法都不如`JSON.parse`简单。 - Mladen Jablanović
哦,而且 JSON 是 Ruby 核心库的一部分,至少在1.9.x版本中是这样的。 - Mladen Jablanović
如果您有一个多类型数组,例如s = "['hello', 2, 'test', 5.0]",JSON将无法解析并显示一般错误unexpected token at ...。然而,YAML可以正常工作,如@Arup的答案所示:YAML.load(s) => ["hello", 2, "test", 5.0] - Chris Cirefice
@ChrisCirefice:这是因为单引号字符串不是有效的 JSON。 - Mladen Jablanović
@MladenJablanović 嗯,那很有道理;我想对于我的用例来说,YAML仅仅因为这个原因更好! - Chris Cirefice

18
可以使用Ruby标准库YAML来完成相同的操作,示例如下:
require 'yaml'
s = "[1,2,[3,4,[5,6]],7]"
YAML.load(s)
# => [1, 2, [3, 4, [5, 6]], 7]

+1;YAML 成功加载多类型数组,例如 "['hello', 2, 'test', 5.0]",而 JSON 无法解析。 - Chris Cirefice
这种方法的优点是,如果存在nil元素,它不会抛出错误,但它会将nil输出为“nil”,因此仍然需要将其转换为nil。 - Obromios

6
“显然”,最好的解决方案是编写自己的解析器。[如果您喜欢编写解析器,从未尝试过并想学习新知识,或想要对确切的语法进行控制]
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转换会将单个值匹配为"简单",但如果该值返回数组,则很快就会得到数组的数组,然后您必须开始使用子树。但是,返回对象是可以的,因为它们代表上层转换时的单个值...所以序列匹配很好。
将返回裸数组的问题与Array([x])和Array(x)给出相同结果的问题结合起来,您会得到非常令人困惑的结果。
为了避免这种情况,我创建了一个名为Arr的辅助类,表示项目的数组。然后我可以指定我要传递给它的内容。然后,即使您有@MateuszFryc提出的示例(感谢@MateuszFryc),我也可以让解析器保留所有括号。

2
我认为它不一定是“显然最佳”的解决方案,因为它取决于输入及其格式以及如何生成它。但是,它是最灵活的解决方案之一。此外,完整可工作的parslet示例是非常难得的,因此给你点赞! - Mark Thomas
怎么样,[[[1],[2,3]]]? - Mateusz Fryc
@MateuszFryc - puts (parse "[[[1],[2,3]]]").inspect => [[1], [2, 3]] - 对我来说似乎是有效的 :)...哦,我明白了...我们丢失了外部括号? - Nigel Thorne
@NigelThorne - 感谢您的更新,但似乎您的解析器/转换器仍存在一些差异。例如,请看 "[]" 数组,它会产生 [nil]。看起来规则 rule(:op => '[', :cl => ']') { Arr.new([]) } 没有被匹配到?就像您认为的那样。这实际上是由 rule(:op => '[', :list => simple(:x), :cl => ']') { Arr.new([x]) } 匹配的,因此出现了问题。 - Mateusz Fryc
你可以简单地将 rule(:op => '[', :list => simple(:x), :cl => ']') { Arr.new([x].compact) } 规则添加 compact,以及你认为会被使用的规则 rule(:op => '[', :cl => ']') { Arr.new([]) } 删除。 - Mateusz Fryc
顺便说一下,也许你可以看一下我解析类JSON结构的问题,我也在处理嵌套数组 ;) https://stackoverflow.com/questions/56749529/how-to-transform-nested-arrays-string-in-json-like-string-to-structured-object-u 无论如何,我都需要重新考虑你的方法,因为它看起来可能是解决我的问题的关键 ;) - Mateusz Fryc

2

使用 eval

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

抱歉,这不是我的应用程序中我觉得可以让注入攻击漏洞存在的部分。 - Justin L.
@Justin L.,“清洁室”+“沙盒”将保护您免受eval的危害:https://dev59.com/hXI-5IYBdhLWcg3wBjrl#2046076。现在唯一剩下的保护工作是防范长时间运行的代码;Timeout可以解决这个问题。 - Wayne Conrad
请在您的回答中添加有关安全风险的注释,以免被踩。一些有关如何减轻风险的建议也将非常有价值。 - Nigel Thorne
我同意需要添加有关安全性的警告,因此我对本来不错的解决方案进行了反对投票。 - gorn
不良实践:/ - Darlan Dieterich

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接