我正在为朋友的心理调查项目创建一个简单的 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)
所以我的问题是:
有更好的方法吗? 我愿意接受任何建议。
如何处理并非全部的公式 占位符已成功替换(例如,一个 问题没有得到解答)?例如:
{:Q2 => 2}
不是
在值哈希中?我的想法是拯救 eval() 但在这种情况下它不会失败,因为 (1 + + 2) / 2
仍然可以 eval() 编辑...有什么想法吗?如何得到正确的结果?应该是 2.5,但由于整数运算,它会截断为 2。我不能指望提供正确公式(例如 / 2.0 )的人能够理解这个细微差别。
我不希望这样,但是如何 最好保护 eval() 免受滥用(例如 错误的公式,被操纵的值 进来)?示例:
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: ( -) = #<SyntaxError: (eval):1: syntax error, unexpected ')'>
使用Dentaku:
Dentaku 是数学和逻辑公式语言的解析器和评估器,允许在运行时将值绑定到公式中引用的变量。它的目的是在不打开安全漏洞的情况下安全地评估不受信任的表达式。
这可能不值得付出努力,但如果我要这样做,我会使用 Treetop 来定义解析语法。甚至还有使用像这样的 PEG 风格语法进行简单算术的示例,因此您已经完成了 90% 的语法,并且大部分完成了评估权重的工作。
您可以使用RubyParser来解释节点迭代的表达式e,以检查是否存在任何危险代码,例如函数调用。看:
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
Re 2) 即使这很丑陋,你也可以创建一个具有默认值的哈希,并确保在调用
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
Re 3) 只需确保您的计算中至少有一个浮点数。最简单的方法是在替换过程中将所有提供的值转换为浮点数:
>> result = f.gsub(/(Q\d+)/) {|m| values[$1.to_sym].to_f } #=> "(1.0 + 2.0 + 2.0) / 2"
>> eval result #=> 2.5
Re 4) 您可能想阅读
$SAFE
。 “Pickaxe”实际上包含一个关于 eval
ing 在网络表单中输入的内容的示例:
http://ruby-doc.org/docs/ProgrammingRuby/html/taint.html
如果您真的想走
eval
路线,请不要忽略本讨论中提供的替代方案。