Разбор строки с escape-символами надежным способом - PullRequest
2 голосов
/ 05 марта 2020

Я полагаю, что этот вопрос достаточно базовый c, чтобы ответ уже существовал, но мои навыки google-fu должны отсутствовать.

Мне нужно проанализировать строки в следующем формате: upper:lower cc ; ! comment , Символ % используется для экранирования специальных символов %:; !. Символ : отделяет upper от lower. Символ ; завершает строку. Символ пробела используется для разделения элемента cc. Комментарии вводятся с помощью !. Следующие строки должны быть проанализированы, как показано:

a:b c ;        upper="a"   lower="b" cc="c" comment=""
a%::b c ;      upper="a:"  lower="b" cc="c" comment=""
a%%:b c ; ! x  upper="a%"  lower="b" cc="c" comment=" x"
a%!:b c ; ! x  upper="a!"  lower="b" cc="c" comment=" x"
a%%%::b c ;    upper="a%:" lower="b" cc="c" comment=""

Какой самый питон c (то есть простой, читаемый, элегантный) и надежный способ решения этой задачи в python? Подходят ли регулярные выражения?

Я пытался написать регулярное выражение, которое использовало отрицательный взгляд назад, чтобы обнаружить нечетное число % с до :, но, очевидно, взгляды назад не могут иметь переменную длину.

Ответы [ 3 ]

3 голосов
/ 05 марта 2020

Я не думаю, что регулярные выражения могут надежно зафиксировать выходящее состояние. Вот синтаксический анализатор конечного автомата.

def parse_line(s):
    fields = [""]
    in_escape = False
    for i, c in enumerate(s):
        if not in_escape:
            if c == "%":  # Start of escape
                in_escape = True
                continue
            if (len(fields) == 1 and c == ":") or (len(fields) == 2 and c == " "):  # Next field
                fields.append("")
                continue
            if c == ";":  # End-of-line
                break
        fields[-1] += c  # Regular or escaped character
        in_escape = False
    return (fields, s[i + 1:])



print(parse_line("a:b c ;"))
print(parse_line("a%::b c ;"))
print(parse_line("a%%:b c ; ! x"))
print(parse_line("a%!:b c ; ! x"))
print(parse_line("a%%%::b c defgh:!:heh;"))
print(parse_line("a%;"))
print(parse_line("a%;:b!unterminated-line"))

выводит

(['a', 'b', 'c '], '')
(['a:', 'b', 'c '], '')
(['a%', 'b', 'c '], ' ! x')
(['a!', 'b', 'c '], ' ! x')
(['a%:', 'b', 'c defgh:!:heh'], '')
(['a;'], '')
(['a;', 'b!unterminated-line'], '')

то есть retval представляет собой 2 кортежа проанализированных полей, а остальная часть строки после ; маркер (который может содержать или не содержать комментарий).

1 голос
/ 05 марта 2020

Аналогично ответу от AKX, но я уже был готов, когда увидел его. Кроме того, подход немного отличается (его легче адаптировать к другому формату), и результат может быть немного чище.

def parse(line):
    parts = [""]
    delims = ":  ; !"
    escape = False
    for c in line:
        if escape:
            parts[-1] += c
            escape = False
        elif c == "%":
            escape = True
        elif c == delims[:1]:
            parts += [""]
            delims = delims[1:]
        else:
            parts[-1] += c
    return [p for p in parts if p] if ";" not in delims else None


lines = ["a:b c ;","a%::b c ;","a%%:b c ; ! x","a%!:b c ; ! x","a%%%::b c ;","a:b incomplete"]
for line in lines:
    print(line, "\t", parse(line))

По сути, это выполняет итерацию строки за символом, отслеживая «escape» mode "и проверяет текущий символ с помощью следующего ожидаемого разделителя.

Вывод:

a:b c ;        ['a', 'b', 'c']
a%::b c ;      ['a:', 'b', 'c']
a%%:b c ; ! x  ['a%', 'b', 'c', ' x']
a%!:b c ; ! x  ['a!', 'b', 'c', ' x']
a%%%::b c ;    ['a%:', 'b', 'c']
a:b incomplete None
0 голосов
/ 07 марта 2020

Основываясь на комментарии @MichaelButscher, я написал следующее решение с использованием регулярных выражений:

def parse_line(line):
    parsed = re.match(r'''( (?: %. | [^:] )+ )     # capture upper
                          (?: :                    # colon delimiter
                              ( (?: %. | [^ ] )+ ) # capture lower
                          )?                       # :lower is optional
                          \ +                      # space delimiter(s)
                          ( (?: %. | [^ ;] )+ )    # capture cont class
                          \ +;                     # space delimiter(s)
                          ( .* ) \s* $                 # capture comment''',
                      line, re.X)
    groups = parsed.groups(default='')
    groups = [re.sub('%(.)', r'\1', elem) for elem in groups]  # unescape
    return groups

Это дает следующие результаты:

>>> print(parse_line("a:b c ;"))
['a', 'b', 'c', '']
>>> print(parse_line("a%::b c ;"))
['a:', 'b', 'c', '']
>>> print(parse_line("a%%:b c ; ! x"))
['a%', 'b', 'c', ' ! x']
>>> print(parse_line("a%!:b c ; ! x"))
['a!', 'b', 'c', ' ! x']

Возвращенные искаженные записи возвращают NoneType объект.

...