Ruby: разбирать, заменять и оценивать строковую формулу - PullRequest
1 голос
/ 03 февраля 2011

Я создаю простое приложение для обследования 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. Iне ожидайте этого, но как лучше защитить eval () от злоупотреблений (например, неверная формула, манипулируемые значениями)?Пример: f = 'system("ruby -v"); (Q1 + (Q2 / 3) + Q3 + (Q4 * 2)) / 2 '

Спасибо!

Ответы [ 5 ]

5 голосов
/ 03 февраля 2011

ОК, теперь это абсолютно безопасно.Клянусь!

Я бы обычно клонировал переменную 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 ')'>
4 голосов
/ 03 февраля 2011

Возможно, это не стоило бы усилий, но если бы я это сделал, я бы использовал Treetop для определения грамматики синтаксического анализа.Существуют даже примеры использования таких грамматик в стиле PEG, как эта, для простой арифметики, так что вы получите 90% пути к грамматике и большую часть пути к оценке веса.

2 голосов
/ 23 сентября 2014

Использование Дентаку :

Dentaku - это синтаксический анализатор и оценщик для языка математических и логических формул, который позволяет связывать значения во время выполнения с переменными, на которые ссылаются формулы Он предназначен для безопасной оценки ненадежных выражений без открытия дыр в безопасности.

2 голосов
/ 03 февраля 2011

Вы можете использовать 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
0 голосов
/ 03 февраля 2011

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.«Кирка» на самом деле содержит пример о eval вводе чего-либо, введенного в веб-форму:

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

Это если вы действительно хотите пойти по маршруту eval, не игнорируйте альтернативы, представленные в этом обсуждении.

...