Как выполнить логическую логику, хранящуюся в виде строки, предпочтительно без eval()?

#python #yaml #eval #pyyaml

Вопрос:

У меня есть приведенный ниже файл YAML, содержащий логику синтаксического анализа, соответствующую имени файла:

 rules_container = """
file_name:
  rule_1: {module: str, method: startswith, args: [s_]}
  rule_2: {module: str, method: endswith, args: [.log]}
  logic: rule_1 and rule_2
"""
 

Я загружаю его и получаю приведенное ниже отображение:

 >>> import yaml
>>> import pprint
 
 >>> d_yml = yaml.safe_load(rules_container)
>>> pprint.pprint(d_yml)
{'file_name': {'logic': 'rule_1 and rule_2',
               'rule_1': {'args': ['s_'],
                          'method': 'startswith',
                          'module': 'str'},
               'rule_2': {'args': ['.log'],
                          'method': 'endswith',
                          'module': 'str'}}}
 

Приведенные ниже функции помогают мне проанализировать вышеуказанные правила, и в конечном итоге я получаю:

  • словарь составленных правил
  • логика применения правил к строке (т. е. имени файла)
 def rule_compiler(module, method, args=None, kwargs=None):

    def str_parser(string):
        return getattr(string, method)(*args)
    
    return str_parser
 
 def rule_parser(container):
  
  compiled_rules = {
      k:rule_compiler(**v) for k,v in container.items() if k.startswith('rule')
  }
  
  logic = container.get('logic', None)
  
  return compiled_rules, logic
 

ПРИМЕЧАНИЕ: Я не использую module и kwargs здесь, но они используются в другом месте файла конфигурации для других правил.

Итак, если я обращусь rule_parser() к контейнеру правил, я получу это:

 >>> rp = rule_parser(d_yml['file_name'])
>>> print(rp)
({'rule_1': <function __main__.rule_compiler.<locals>.str_parser(string)>,
  'rule_2': <function __main__.rule_compiler.<locals>.str_parser(string)>},
 'rule_1 and rule_2')
 

Если я попытаюсь проанализировать строку с каждым отдельным правилом, они будут работать так, как ожидалось:

 >>> fname = 's_test.out'
 
 >>> rp[0]['rule_1'](fname)
True
>>> rp[0]['rule_2'](fname)
False
 

Я хочу применить логику, определенную «логикой», и получить это:

 >>> rp[0]['rule_1'](fname) and rp[0]['rule_2'](fname)
False
 

Я уже придумал, как это сделать, проанализировав строку, содержащую логическую логику, преобразовав ее во что-то похожее на описанное выше, а затем вызовите eval() ее.

Можете ли вы придумать какой-нибудь другой способ, не связанный eval() с этим ?

ПРИМЕЧАНИЕ: может быть более двух правил, поэтому замена логики простым «и/или», а затем использование какого if -либо оператора для определения того, какое из них применять, также не сработает.

ПРИМЕЧАНИЕ: использование регулярных выражений и полное устранение необходимости в нескольких правилах в данном конкретном случае не является вариантом.

Спасибо!

Комментарии:

1. Возможно, вам повезет с ast.literal_eval(...) (не проверено).

2. Правильный способ в долгосрочной перспективе-создать синтаксический анализатор, который читает интересующий вас язык, генерирует AST, а затем создает реализации узлов AST. Возьмите курс разработки компилятора университетского уровня, и к его концу вы будете готовы. Однако я не уверен, что это подходящая область для вопроса о переполнении стека. Любой приличный генератор синтаксического анализатора позволит вам настроить соответствующие правила приоритета и т. Д.

3. @Jan, это, конечно, не буквально. Я бы не ожидал literal_eval , что это сработает.

4. @Jan я получаю ValueError: malformed node or string «с ast.literal_eval() «. Тем не менее, спасибо за ваш вклад.

Ответ №1:

Вот идея, используя pandas.eval , не уверен, насколько она подходит вашему случаю.

 import pandas as pd

fname = 's_test.out'
rule, logic = rp

def apply_rule(rule_dict: dict, fname: str) -> dict:
    return {rule: func(fname) 
        for rule, func in rule_dict.items()}

print(pd.eval(logic, local_dict = apply_rule(rule, fname)))
 

Выход:

 False
 

Комментарии:

1. Кажется, это хорошо работает. Мне не нужно анализировать/изменять строку, предоставленную с помощью logic ключа, и беспокоиться о создании допустимого выражения для выполнения с помощью eval() . Более того, если я попытаюсь передать «опасную» строку через logic (например "__import__('os').listdir('.')" ) Я получаю исключение ValueError: "__import__" is not a supported function . Так что, кроме того факта , что мне приходится импортировать pandas , я не вижу никаких недостатков.

2. Я не смог заставить его работать с помощью boolean.py библиотеки, и я не собираюсь создавать синтаксический анализатор. Я понимаю, что при использовании все еще существуют некоторые риски pd.eval() . Хотя на данный момент это выглядит более безопасным — и более универсальным — чем eval() . Поэтому я собираюсь выбрать это в качестве решения своей проблемы.

Ответ №2:

Правильный ответ может быть где угодно, начиная с использования библиотеки, такой как boolean.py для анализа ваших строк (менее гибкий, вероятно, самый простой выбор, кроме eval ) для чтения лексеров и синтаксических анализаторов и реализации одного из них с нуля или с помощью программного обеспечения для создания синтаксического анализатора (наиболее гибкого, но сложного и трудоемкого). В этом случае я бы, вероятно, рекомендовал начать с библиотеки, специально созданной для вычисления логических выражений, и, если это не сработает, попробуйте что-нибудь более сложное.

Правка: Я должен отметить, что вы могли бы использовать eval , если у пользователей нет точек входа для внедрения вредоносного кода, вы правильно очистили строку оценки и т. Д. — Вероятно, это не стоит вашего времени, но стоит подумать, если другие варианты не соответствуют вашим потребностям.

Комментарии:

1. Мне нравится идея использовать boolean.py . Я пытался заставить это работать, но я всегда оказываюсь в ситуации, когда мне приходится динамически создавать переменные на основе имен моих правил. Итак, я вернулся к исходной точке, только с другой плохой практикой .

2. Не могли бы вы использовать словарь для динамического задания имен «переменных» или заранее создать несколько переменных и установить для них нужные значения?

3. Мое понимание использования этой библиотеки заключается в том , что мне нужно добраться до точки, где у меня есть что-то вроде rule_1, rule_2 = algebra.symbols(True, False) , где True и False являются результатами startswith() и endswith() методами соответственно. Чтобы попасть туда, мне нужно сделать имена правил, которые могут изменяться, частью пространства имен функции. Итак, я создаю переменные динамически. В противном случае строка, содержащаяся в logic , больше не будет работать как есть.

4. И даже если бы я смог добраться до сути, если бы имена правил стали частью локального пространства имен, я все равно не смог бы получить ожидаемый результат от этой библиотеки. На самом деле, если я это делаю algebra.parse(logic) == [True|False|TRUE|FALSE] , я всегда получаю FALSE .