Преобразование пар ключ = значение обратно в Python dicts - PullRequest
0 голосов
/ 19 октября 2018

Есть лог-файл с текстом в виде разделенных пробелами пар key=value, и каждая строка была первоначально сериализована из данных в Python dict, что-то вроде:

' '.join([f'{k}={v!r}' for k,v in d.items()])

Ключи всегда простостроки.Значения могут быть любыми, которые ast.literal_eval может успешно проанализировать, не больше, не меньше.

Как обработать этот файл журнала и превратить строки обратно в Python dicts? Пример:

>>> to_dict("key='hello world'")
{'key': 'hello world'}

>>> to_dict("k1='v1' k2='v2'")
{'k1': 'v1', 'k2': 'v2'}

>>> to_dict("s='1234' n=1234")
{'s': '1234', 'n': 1234}

>>> to_dict("""k4='k5="hello"' k5={'k6': ['potato']}""")
{'k4': 'k5="hello"', 'k5': {'k6': ['potato']}}

Вот еще один дополнительный контекст о данных:

  • Ключи действительные имена
  • Входные строки правильно сформированы (например, без висячих скобок)
  • Данные являются доверенными (небезопасные функции, такие как * 1024)*eval, exec, yaml.load можно использовать)
  • Заказ не важен.Производительность не важна.Правильность важна.

Редактировать: Как и просили в комментариях, вот MCVE и пример кода, который работал неправильно

>>> def to_dict(s):
...     s = s.replace(' ', ', ')
...     return eval(f"dict({s})")
... 
... 
>>> to_dict("k1='v1' k2='v2'")
{'k1': 'v1', 'k2': 'v2'}  # OK
>>> to_dict("s='1234' n=1234")
{'s': '1234', 'n': 1234}  # OK
>>> to_dict("key='hello world'")
{'key': 'hello, world'}  # Incorrect, the value was corrupted

Ответы [ 3 ]

0 голосов
/ 19 октября 2018

Вы можете найти все вхождения = символов, а затем найти максимальное количество символов, которые дают действительный результат ast.literal_eval.Затем эти символы могут быть проанализированы для значения, связанного с ключом, найденным срезом строки между последним успешным анализом и индексом текущего =:

import ast, typing
def is_valid(_str:str) -> bool:  
  try:
     _ = ast.literal_eval(_str)
  except:
    return False
  else:
    return True

def parse_line(_d:str) -> typing.Generator[typing.Tuple, None, None]:
  _eq, last = [i for i, a in enumerate(_d) if a == '='], 0
  for _loc in _eq:
     if _loc >= last:
       _key = _d[last:_loc]
       _inner, seen, _running, _worked = _loc+1, '', _loc+2, []
       while True:
         try:
            val = ast.literal_eval(_d[_inner:_running])
         except:
            _running += 1
         else:
            _max = max([i for i in range(len(_d[_inner:])) if is_valid(_d[_inner:_running+i])])
            yield (_key, ast.literal_eval(_d[_inner:_running+_max]))
            last = _running+_max
            break


def to_dict(_d:str) -> dict:
  return dict(parse_line(_d))

print([to_dict("key='hello world'"), 
       to_dict("k1='v1' k2='v2'"), 
       to_dict("s='1234' n=1234"), 
       to_dict("""k4='k5="hello"' k5={'k6': ['potato']}"""),
       to_dict("val=['100', 100, 300]"),
       to_dict("val=[{'t':{32:45}, 'stuff':100, 'extra':[]}, 100, 300]")
   ]

)

Вывод:

{'key': 'hello world'}
{'k1': 'v1', 'k2': 'v2'}
{'s': '1234', 'n': 1234}
{'k4': 'k5="hello"', 'k5': {'k6': ['potato']}}
{'val': ['100', 100, 300]}
{'val': [{'t': {32: 45}, 'stuff': 100, 'extra': []}, 100, 300]}

Отказ от ответственности:

Это решение не такое элегантное, как у @ Jean-FrançoisFabre, и я не уверен, сможет ли оно проанализировать 100% того, что передано to_dict,но это может дать вам вдохновение для вашей собственной версии.

0 голосов
/ 20 октября 2018

Ваш ввод не может быть удобно проанализирован чем-то вроде ast.literal_eval, но он может быть токенизированным как серия токенов Python.Это делает вещи немного проще, чем они могли бы быть в противном случае.

Единственное место, где = токены могут появляться в вашем вводе - это разделители ключ-значение;по крайней мере, на данный момент ast.literal_eval ничего не принимает с токенами =.Мы можем использовать токены =, чтобы определить, где начинаются и заканчиваются пары ключ-значение, а большая часть остальной работы может быть обработана с помощью ast.literal_eval.Использование модуля tokenize также позволяет избежать проблем с = или экранированием обратной косой черты в строковых литералах.

import ast
import io
import tokenize

def todict(logstring):
    # tokenize.tokenize wants an argument that acts like the readline method of a binary
    # file-like object, so we have to do some work to give it that.
    input_as_file = io.BytesIO(logstring.encode('utf8'))
    tokens = list(tokenize.tokenize(input_as_file.readline))

    eqsign_locations = [i for i, token in enumerate(tokens) if token[1] == '=']

    names = [tokens[i-1][1] for i in eqsign_locations]

    # Values are harder than keys.
    val_starts = [i+1 for i in eqsign_locations]
    val_ends = [i-1 for i in eqsign_locations[1:]] + [len(tokens)]

    # tokenize.untokenize likes to add extra whitespace that ast.literal_eval
    # doesn't like. Removing the row/column information from the token records
    # seems to prevent extra leading whitespace, but the documentation doesn't
    # make enough promises for me to be comfortable with that, so we call
    # strip() as well.
    val_strings = [tokenize.untokenize(tok[:2] for tok in tokens[start:end]).strip()
                   for start, end in zip(val_starts, val_ends)]
    vals = [ast.literal_eval(val_string) for val_string in val_strings]

    return dict(zip(names, vals))

Это работает правильно на вводимых вами примерах, а также на примере с обратными слешами:

>>> todict("key='hello world'")
{'key': 'hello world'}
>>> todict("k1='v1' k2='v2'")
{'k1': 'v1', 'k2': 'v2'}
>>> todict("s='1234' n=1234")
{'s': '1234', 'n': 1234}
>>> todict("""k4='k5="hello"' k5={'k6': ['potato']}""")
{'k4': 'k5="hello"', 'k5': {'k6': ['potato']}}
>>> s=input()
a='=' b='"\'' c=3
>>> todict(s)
{'a': '=', 'b': '"\'', 'c': 3}

Кстати, мы, вероятно, могли бы искать токены типа NAME вместо = токенов, но это сломается, если они когда-нибудь добавят поддержку set() к literal_eval.Поиск = может также сломаться в будущем, но вряд ли он сломается, как поиск NAME токенов.

0 голосов
/ 19 октября 2018

Функции замены регулярных выражений на помощь

Я не переписываю аст-подобный синтаксический анализатор для вас, но одна хитрость, которая работает довольно хорошо - это использоватьрегулярные выражения для замены строк в кавычках и их замены на «переменные» (я выбрал __token(number)__), немного похоже на то, что вы не используете какой-то код.

Запишите заменяемые строки(это должно заботиться о пробелах), замените пробел запятой (защита от символов до того, как : позволяет пройти последний тест) и замените на строки снова.

import re,itertools

def to_dict(s):
    rep_dict = {}
    cnt = itertools.count()
    def rep_func(m):
        rval = "__token{}__".format(next(cnt))
        rep_dict[rval] = m.group(0)
        return rval

    # replaces single/double quoted strings by token variable-like idents
    # going on a limb to support escaped quotes in the string and double escapes at the end of the string
    s = re.sub(r"(['\"]).*?([^\\]|\\\\)\1",rep_func,s)
    # replaces spaces that follow a letter/digit/underscore by comma
    s = re.sub("(\w)\s+",r"\1,",s)
    #print("debug",s)   # uncomment to see temp string
    # put back the original strings
    s = re.sub("__token\d+__",lambda m : rep_dict[m.group(0)],s)

    return eval("dict({s})".format(s=s))

print(to_dict("k1='v1' k2='v2'"))
print(to_dict("s='1234' n=1234"))
print(to_dict(r"key='hello world'"))
print(to_dict('key="hello world"'))
print(to_dict("""k4='k5="hello"' k5={'k6': ['potato']}"""))
# extreme string test
print(to_dict(r"key='hello \'world\\'"))

печатает:

{'k2': 'v2', 'k1': 'v1'}
{'n': 1234, 's': '1234'}
{'key': 'hello world'}
{'key': 'hello world'}
{'k5': {'k6': ['potato']}, 'k4': 'k5="hello"'}
{'key': "hello 'world\\"}

Ключ заключается в том, чтобы извлечь строки (заключенные в кавычки / двойные кавычки), используя не жадные регулярные выражения, и заменить их не строковыми (например, если это были строковые переменные не литералы) в выражении.Регулярное выражение настроено таким образом, что оно может принимать экранированные кавычки и двойной экранирование в конце строки (пользовательское решение)

Функция замены является внутренней функцией, поэтому она может использовать нелокальный словарь и счетчик и отслеживатьтекст заменен, поэтому его можно восстановить после того, как пробелы были учтены.

При замене пробелов запятыми следует соблюдать осторожность, чтобы не делать это после двоеточия (последний тест) или всех рассмотренных вопросовпосле алфавитного / нижнего подчеркивания (следовательно, защита \w в регулярном выражении замены для запятой)

Если мы раскомментируем код отладочной печати непосредственно перед тем, как будут возвращены исходные строки, на которых будет напечатано:

debug k1=__token0__,k2=__token1__
debug s=__token0__,n=1234
debug key=__token0__
debug k4=__token0__,k5={__token1__: [__token2__]}
debug key=__token0__

Строки были пробиты, и замена пробелов сработала правильно.Если приложить больше усилий, вероятно, можно будет заключить в кавычки ключи и заменить k1= на "k1":, чтобы вместо eval можно было использовать ast.literal_eval (более рискованно, и здесь не требуется)

Я уверен, что некоторые сверхсложные выражения могут нарушить мой код (я даже слышал, что существует очень мало парсеров json, способных проанализировать 100% допустимых файлов json), но для отправленных вами тестов это сработает (конечно, если какой-нибудь забавный парень попытается вставить __tokenxx__ идентификаторов в исходные строки, это потерпит неудачу, возможно, его можно будет заменить на некоторые другие недопустимые переменные-заполнители).Некоторое время назад я создал лексер Ada, используя эту технику, чтобы избежать пробелов в строках, и это сработало довольно хорошо.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...