записать в строку все, что не является токеном - PullRequest
3 голосов
/ 02 апреля 2020

Контекст: Я имею дело со смесью логических и арифметических выражений c, которые могут выглядеть в следующем примере:

b_1 /\ (0 <= x_1) /\ (x_2 <= 2 \/ (b_3 /\ ((/ 1 3) <= x_4))))

Я хочу сопоставить и извлечь любое ограничение формы A <= B содержится в формуле, которая всегда должна быть истинной. В приведенном выше примере только 0 <= x_1 будет удовлетворять такому критерию.

Текущая цель: Моя идея состоит в том, чтобы построить простое дерево разбора входной формулы, фокусируясь только на следующих токенах: и (/\) или (\/), левая скобка (() и правая скобка ()). Учитывая приведенную выше формулу, я хотел бы сгенерировать следующий AST:

/\
|_ "b_1"
|_ /\
   |_ "0 <= x_1"
   |_ \/
      |_ "x_2 <= 2"
      |_ /\
         |_ "b_3"
         |_ "(/ 1 3) <= x_4"

Затем я могу просто пройти через AST и отбросить любое поддерево с корнем в \/.

Моя попытка:

Глядя на эту документацию , я определяю грамматику для лексера следующим образом:

import ply.lex as lex

tokens = (
    "LPAREN",
    "RPAREN",
    "AND",
    "OR",
    "STRING",
)

t_AND    = r'\/\\'
t_OR     = r'\\\/'
t_LPAREN = r'\('
t_RPAREN = r'\)'

t_ignore = ' \t\n'

def t_error(t):
    print(t)
    print("Illegal character '{}'".format(t.value[0]))
    t.lexer.skip(1)

def t_STRING(t):
    r'^(?!\)|\(| |\t|\n|\\\/|\/\\)'
    t.value = t
    return t

data = "b_1 /\ (x_2 <= 2 \/ (b_3 /\ ((/ 1 3) <= x_4))"

lexer = lex.lex()

lexer.input(data)

while True:
    tok = lexer.token()
    if not tok:
        break
    print(tok.type, tok.value, tok.lineno, tok.lexpos)

Однако я получаю следующий вывод:

~$ python3 lex.py
LexToken(error,'b_1 /\\ (x_2 <= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,0)
Illegal character 'b'
LexToken(error,'_1 /\\ (x_2 <= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,1)
Illegal character '_'
LexToken(error,'1 /\\ (x_2 <= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,2)
Illegal character '1'
AND /\ 1 4
LPAREN ( 1 7
LexToken(error,'x_2 <= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,8)
Illegal character 'x'
LexToken(error,'_2 <= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,9)
Illegal character '_'
LexToken(error,'2 <= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,10)
Illegal character '2'
LexToken(error,'<= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,12)
Illegal character '<'
LexToken(error,'= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,13)
Illegal character '='
LexToken(error,'2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))',1,15)
Illegal character '2'
OR \/ 1 17
LPAREN ( 1 20
LexToken(error,'b_3 /\\ ((/ 1 3) <= x_4))',1,21)
Illegal character 'b'
LexToken(error,'_3 /\\ ((/ 1 3) <= x_4))',1,22)
Illegal character '_'
LexToken(error,'3 /\\ ((/ 1 3) <= x_4))',1,23)
Illegal character '3'
AND /\ 1 25
LPAREN ( 1 28
LPAREN ( 1 29
LexToken(error,'/ 1 3) <= x_4))',1,30)
Illegal character '/'
LexToken(error,'1 3) <= x_4))',1,32)
Illegal character '1'
LexToken(error,'3) <= x_4))',1,34)
Illegal character '3'
RPAREN ) 1 35
LexToken(error,'<= x_4))',1,37)
Illegal character '<'
LexToken(error,'= x_4))',1,38)
Illegal character '='
LexToken(error,'x_4))',1,40)
Illegal character 'x'
LexToken(error,'_4))',1,41)
Illegal character '_'
LexToken(error,'4))',1,42)
Illegal character '4'
RPAREN ) 1 43
RPAREN ) 1 44

Токен t_STRING распознан неправильно, как и должно быть.

Вопрос: как установить перехватить все регулярное выражение для t_STRING, чтобы получить работающий токенизатор?

Ответы [ 2 ]

3 голосов
/ 03 апреля 2020

Ваше регулярное выражение для T_STRING определенно не делает то, что вы хотите. Что он делает, ответить немного сложнее.

В принципе, он состоит только из двух утверждений нулевой длины: ^, что верно только в начале строки (если вы не предоставите re.MULTILINE флаг, который вы не указали), и длинное отрицательное утверждение с прогнозом.

Шаблон, который состоит только из утверждений нулевой длины, может соответствовать только пустой строке, если она вообще соответствует чему-либо. Но шаблоны лексера не могут совпадать с пустой строкой. Лексеры делят ввод на серию токенов, так что каждый символ на входе принадлежит некоторому токену. Каждое совпадение - а это все совпадения, а не поиски - начинается именно в конце предыдущего совпадения. Таким образом, если шаблон может соответствовать пустой строке, лексер попытается найти следующее совпадение в том же месте с тем же результатом, который будет бесконечным l oop.

Некоторые генераторы лексеров решают эту проблему путем форсирование минимального совпадения одного символа с использованием встроенного универсального шаблона ошибок, но Ply просто отказывается генерировать лексер, если шаблон соответствует пустой строке. И все же Ply не жалуется на эту спецификацию лексера. Единственное возможное объяснение состоит в том, что шаблон не может соответствовать чему-либо.

Ключ заключается в том, что Ply компилирует все шаблоны, используя флаг re.VERBOSE, который позволяет разделять элементы в регулярных выражениях с пробелами, делая регулярные выражения немного меньше нечитаемым. Как указывается в документации Python:

Пробелы в шаблоне игнорируются, за исключением случаев, когда они находятся в классе символов, или когда им предшествует неэкранированная обратная коса sh или внутри токенов, таких как *?, (?: или (?P<...>.

Пробел включает в себя новые строки и даже комментарии (начиная с символа #), поэтому вы можете разбивать шаблоны на несколько строк и вставлять комментарии о каждой части.

Фактически, мы могли бы сделать это с вашим шаблоном:

def t_STRING(t):
    r'''^         # Anchor this match at the beginning of the input
        (?!       # Don't match if the next characters match:
           \)   | # Close parenthesis
           \(   | # Open parenthesis
           \    | # !!! HERE IS THE PROBLEM
           \t   | # Tab character
           \n   | # Newline character
           \\\/ | # \/ token
           \/\\   # /\ token
        )
     '''
    t.value = t
    return t

Поэтому, когда я добавил пробел и комментарии к вашему шаблону, я должен был заметить, что исходный шаблон пытался соответствовать пробелу в качестве альтернативы с | |. Но поскольку шаблон скомпилирован как re.VERBOSE, этот символ пробела игнорируется, оставляя пустую альтернативу, которая соответствует пустой строке. Эта альтернатива является частью отрицательного косвенного утверждения, что означает, что утверждение не будет выполнено, если строка для сопоставления в этой точке начинается с пустой строки. Конечно, каждая строка начинается с пустой строки, поэтому отрицательное прогнозное утверждение всегда терпит неудачу, объясняя, почему Ply не жаловался (и почему шаблон никогда ничего не соответствует).

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

Но вы почти наверняка не хотите совпадать только с одним символом. Предположительно, вы хотели сопоставить строку символов, которая не соответствует ни одному другому токену. Таким образом, это означает, что отрицательное прогнозное утверждение и следующие . повторяются. И помните, что это должен быть непустой повтор (+, а не *), потому что шаблоны не должны иметь пустых совпадений.

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

И прежде чем писать шаблон, давайте сделаем еще одну корректировку: в регулярном выражении Python, если вы можете заменить набор альтернатив классом символов, вы должны сделать это, потому что он намного эффективнее. Это верно, даже если только некоторые из альтернатив могут быть заменены.

Так что получается следующее:

def t_STRING(t):
    r'''(
         (?!            # Don't match if the next characters match:
            [() \t\n] |   # Parentheses or whitespace
            \\\/      |   # \/ token
            \/\\          # /\ token
         ) .            # If none of the above match, accept a character
        )+              # and repeat as many times as possible (at least once)
     '''
    return t

Я удалил t.value = t. t - это объект токена, а не строка, и значение должно быть строкой, с которой оно сопоставлено. Если вы перезаписываете значение круглой ссылкой, вы не сможете определить, какая строка соответствует.

Это работает, но не совсем так, как вы предполагали. Поскольку пробельные символы исключаются из T_STRING, вы не получите ни одного токена, представляющего (/ 1 3) <= x_4. Вместо этого вы получаете серию токенов:

STRING b_1 1 0
AND /\ 1 4
LPAREN ( 1 7
STRING x_2 1 8
STRING <= 1 12
STRING 2 1 15
OR \/ 1 17
LPAREN ( 1 20
STRING b_3 1 21
AND /\ 1 25
LPAREN ( 1 28
LPAREN ( 1 29
STRING / 1 30
STRING 1 1 32
STRING 3 1 34
RPAREN ) 1 35
STRING <= 1 37
STRING x_4 1 40
RPAREN ) 1 43
RPAREN ) 1 44

Но я думаю, что это разумно. Как лексер мог бы сказать, что круглые скобки в (x_2 <= 2 и (b_3 являются токенами скобок, в то время как круглые скобки в (/ 1 3) <= x_4 являются частью T_STRING? Это определение нужно будет выполнить в вашем парсере.

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

2 голосов
/ 03 апреля 2020

На основании превосходного ответа от @ rici , указывающего на проблему с t_STRING, это мой последний вариант примера, который вносит меньшие изменения в предложенный @ rici .

Код

##############
# TOKENIZING #
##############

tokens = (
    "LPAREN",
    "RPAREN",
    "AND",
    "OR",
    "STRING",
)

def t_AND(t):
    r'[ ]*\/\\[ ]*'
    t.value = "/\\"
    return t

def t_OR(t):
    r'[ ]*\\\/[ ]*'
    t.value = "\\/"
    return t

def t_LPAREN(t):
    r'[ ]*\([ ]*'
    t.value = "("
    return t

def t_RPAREN(t):
    r'[ ]*\)[ ]*'
    t.value = ")"
    return t

def t_STRING(t):
    r'''(
         (?!            # Don't match if the next characters match:
            [()\t\n] |   # Parentheses or whitespace
            \\\/     |   # \/ token
           \/\\          # /\ token
         ) .            # If none of the above match, accept a character
        )+              # and repeat as many times as possible (at least once)
     '''
    return t

def t_error(t):
    print("error: " + str(t.value[0]))
    t.lexer.skip(1)

import ply.lex as lex
lexer = lex.lex()

data = "b_b /\\ (ccc <= 2 \\/ (b_3 /\\ ((/ 1 3) <= x_4))"
lexer.input(data)

while True:
    tok = lexer.token()
    if not tok:
        break
    print("{0}: `{1}`".format(tok.type, tok.value))

Выход

STRING: `b_b `
AND: `/\`
LPAREN: `(`
STRING: `ccc <= 2 `
OR: `\/`
LPAREN: `(`
STRING: `b_3 `
AND: `/\`
LPAREN: `(`
LPAREN: `(`
STRING: `/ 1 3`
RPAREN: `)`
STRING: `<= x_4`
RPAREN: `)`
RPAREN: `)`
...