Ruby:解析,替换和评估字符串公式

我正在为朋友的心理调查项目创建一个简单的Ruby on Rails调查应用程序。 所以我们有调查,每个调查都有一堆问题,每个问题都有参与者可以选择的选项之一。 没什么好激动的。

其中一个有趣的方面是每个答案选项都有一个与之相关的分数值。 因此,对于每个调查,需要根据这些值计算总分。

现在,我的想法是,不是硬编码计算,而是允许用户添加一个公式,通过该公式计算总调查分数。 示例公式:

"Q1 + Q2 + Q3" "(Q1 + Q2 + Q3) / 3" "(10 - Q1) + Q2 + (Q3 * 2)" 

所以只是基本的数学(为了清晰起见,附加一些括号)。 我们的想法是保持公式非常简单,这样任何拥有基本数学的人都可以输入它们,而无需解析某些奇特的语法。

我的想法是采用任何给定的公式,并用基于参与者选择的分数值替换Q1,Q2等占位符。 然后eval()新形成的字符串。 像这样的东西:

 f = "(Q1 + Q2 + Q3) / 2" # some crazy formula for this survey values = {:Q1 => 1, :Q2 => 2, :Q3 => 2} # values for substitution result = f.gsub(/(Q\d+)/) {|m| values[$1.to_sym] } # string to be eval()-ed eval(result) 

所以我的问题是:

  1. 有一个更好的方法吗? 我愿意接受任何建议。

  2. 如何处理未成功替换所有占位符的公式(例如,一个问题没有得到解答)? 例如: {:Q2 => 2}不是值哈希? 我的想法是拯救eval(),但在这种情况下它不会失败coz (1 + + 2) / 2仍然可以是eval() – ed …任何想法?

  3. 如何获得正确的结果? 应该是2.5,但由于整数运算,它将截断为2.我不能指望提供正确公式(例如/ 2.0)的人理解这种细微差别。

  4. 我不指望这一点,但如何最好地保护eval()免受滥用(例如糟糕的公式,操纵值进入)? 示例: f = 'system("ruby -v"); (Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2 ' f = 'system("ruby -v"); (Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2 '

谢谢!

好的,现在它完全安全了。 我发誓!

我通常会克隆formula变量,但在这种情况下,因为你担心恶意用户我清理了变量:

 class Evaluator def self.formula(formula, values) # remove anything but Q's, numbers, ()'s, decimal points, and basic math operators formula.gsub!(/((?![qQ0-9\s\.\-\+\*\/\(\)]).)*/,'').upcase! begin formula.gsub!(/Q\d+/) { |match| ( values[match.to_sym] && values[match.to_sym].class.ancestors.include?(Numeric) ? values[match.to_sym].to_s : '0' )+'.0' } instance_eval(formula) rescue Exception => e e.inspect end end end f = '(q1 + (q2 / 3) + q3 + (q4 * 2))' # some crazy formula for this survey values = {:Q2 => 1, :Q4 => 2} # values for substitution puts "formula: #{f} = #{Evaluator.formula(f,values)}" => formula: (0.0 + (1.0 / 3) + 0.0 + (2.0 * 2)) = 4.333333333333333 f = '(Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2' # some crazy formula for this survey values = {:Q1 => 1, :Q3 => 2} # values for substitution puts "formula: #{f} = #{Evaluator.formula(f,values)}" => formula: (1.0 + (0.0 / 3) + 2.0 + (0.0 * 2)) / 2 = 1.5 f = '(Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2' # some crazy formula for this survey values = {:Q1 => 'delete your hard drive', :Q3 => 2} # values for substitution puts "formula: #{f} = #{Evaluator.formula(f,values)}" => formula: (0.0 + (0.0 / 3) + 2.0 + (0.0 * 2)) / 2 = 1.0 f = 'system("ruby -v")' # some crazy formula for this survey values = {:Q1 => 'delete your hard drive', :Q3 => 2} # values for substitution puts "formula: #{f} = #{Evaluator.formula(f,values)}" => formula: ( -) = # 

它可能不值得努力,但如果我这样做,我会使用Treetop来定义解析语法。 甚至有一些例子可以使用像这样的PEG式语法来进行简单的算术运算,因此你将90%的语法用于语法,并且大部分方法都用于评估加权。

您可以使用RubyParser来解释节点迭代的表达式,以检查是否存在任何危险代码,如函数调用。 看:

 require 'ruby_parser' def valid_formula?(str, consts=[]) !!valid_formula_node?(RubyParser.new.process(str), consts) rescue Racc::ParseError false end def valid_formula_node?(node, consts) case node.shift when :call node[1].to_s !~ /^[a-z_0-9]+$/i and valid_formula_node?(node[0], consts) and valid_formula_node?(node[2], consts) when :arglist node.all? {|inner| valid_formula_node?(inner, consts) } when :lit Numeric === node[0] when :const consts.include? node[0] end end 

这只允许运算符,数字和特定常量。

 valid_formula?("(Q1 + Q2 + Q3) / 2", [:Q1, :Q2, :Q3]) #=> true valid_formula?("exit!", [:Q1, :Q2, :Q3]) #=> false valid_formula?("!(%&$)%*", [:Q1, :Q2, :Q3]) #=> false 

使用Dentaku :

Dentaku是数学和逻辑公式语言的解析器和评估器,允许将值运行时绑定到公式中引用的变量。 它旨在安全地评估不受信任的表达式,而无需打开安全漏洞。

Re 2)即使这很难看,你也可以创建一个带有默认值的Hash,并确保在调用to_s时失败(我说这很难看,对吧?):

 >> class NaN ; def to_s; raise ArgumentError ; end; end #=> nil >> h = Hash.new { NaN.new } #=> {} >> h[:q1] = 12 #=> 12 >> h[:q1] #=> 12 >> h[:q2] ArgumentError: ArgumentError 

重新3)确保计算中至少有一个浮点数。 最简单的方法是在替换期间将所有提供的值转换为浮点数:

 >> result = f.gsub(/(Q\d+)/) {|m| values[$1.to_sym].to_f } #=> "(1.0 + 2.0 + 2.0) / 2" >> eval result #=> 2.5 

重新4)您可能想要阅读$SAFE 。 “Pickaxe”实际上包含一个关于eval在Web表单中输入内容的示例:

http://ruby-doc.org/docs/ProgrammingRuby/html/taint.html

如果您真的想沿着eval路线前进,请不要忽略本讨论中提供的替代方案。