Эффективно сопоставить несколько регулярных выражений в Python - PullRequest
14 голосов
/ 25 сентября 2008

Лексические анализаторы довольно легко написать, когда у вас есть регулярные выражения. Сегодня я хотел написать простой общий анализатор на Python и придумал:

import re
import sys

class Token(object):
    """ A simple Token structure.
        Contains the token type, value and position. 
    """
    def __init__(self, type, val, pos):
        self.type = type
        self.val = val
        self.pos = pos

    def __str__(self):
        return '%s(%s) at %s' % (self.type, self.val, self.pos)


class LexerError(Exception):
    """ Lexer error exception.

        pos:
            Position in the input line where the error occurred.
    """
    def __init__(self, pos):
        self.pos = pos


class Lexer(object):
    """ A simple regex-based lexer/tokenizer.

        See below for an example of usage.
    """
    def __init__(self, rules, skip_whitespace=True):
        """ Create a lexer.

            rules:
                A list of rules. Each rule is a `regex, type`
                pair, where `regex` is the regular expression used
                to recognize the token and `type` is the type
                of the token to return when it's recognized.

            skip_whitespace:
                If True, whitespace (\s+) will be skipped and not
                reported by the lexer. Otherwise, you have to 
                specify your rules for whitespace, or it will be
                flagged as an error.
        """
        self.rules = []

        for regex, type in rules:
            self.rules.append((re.compile(regex), type))

        self.skip_whitespace = skip_whitespace
        self.re_ws_skip = re.compile('\S')

    def input(self, buf):
        """ Initialize the lexer with a buffer as input.
        """
        self.buf = buf
        self.pos = 0

    def token(self):
        """ Return the next token (a Token object) found in the 
            input buffer. None is returned if the end of the 
            buffer was reached. 
            In case of a lexing error (the current chunk of the
            buffer matches no rule), a LexerError is raised with
            the position of the error.
        """
        if self.pos >= len(self.buf):
            return None
        else:
            if self.skip_whitespace:
                m = self.re_ws_skip.search(self.buf[self.pos:])

                if m:
                    self.pos += m.start()
                else:
                    return None

            for token_regex, token_type in self.rules:
                m = token_regex.match(self.buf[self.pos:])

                if m:
                    value = self.buf[self.pos + m.start():self.pos + m.end()]
                    tok = Token(token_type, value, self.pos)
                    self.pos += m.end()
                    return tok

            # if we're here, no rule matched
            raise LexerError(self.pos)

    def tokens(self):
        """ Returns an iterator to the tokens found in the buffer.
        """
        while 1:
            tok = self.token()
            if tok is None: break
            yield tok


if __name__ == '__main__':
    rules = [
        ('\d+',             'NUMBER'),
        ('[a-zA-Z_]\w+',    'IDENTIFIER'),
        ('\+',              'PLUS'),
        ('\-',              'MINUS'),
        ('\*',              'MULTIPLY'),
        ('\/',              'DIVIDE'),
        ('\(',              'LP'),
        ('\)',              'RP'),
        ('=',               'EQUALS'),
    ]

    lx = Lexer(rules, skip_whitespace=True)
    lx.input('erw = _abc + 12*(R4-623902)  ')

    try:
        for tok in lx.tokens():
            print tok
    except LexerError, err:
        print 'LexerError at position', err.pos

Это работает просто отлично, но я немного волнуюсь, что это слишком неэффективно. Есть ли какие-нибудь трюки с регулярными выражениями, которые позволят мне написать это более эффективным / элегантным способом?

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

Ответы [ 6 ]

11 голосов
/ 09 ноября 2010

Я предлагаю использовать класс re.Scanner, он не задокументирован в стандартной библиотеке, но его стоит использовать. Вот пример:

import re

scanner = re.Scanner([
    (r"-?[0-9]+\.[0-9]+([eE]-?[0-9]+)?", lambda scanner, token: float(token)),
    (r"-?[0-9]+", lambda scanner, token: int(token)),
    (r" +", lambda scanner, token: None),
])

>>> scanner.scan("0 -1 4.5 7.8e3")[0]
[0, -1, 4.5, 7800.0]
7 голосов
/ 25 сентября 2008

Вы можете объединить все свои регулярные выражения в один, используя "|" оператор и пусть библиотека регулярных выражений делает работу по распознаванию между токенами. Необходимо соблюдать осторожность, чтобы обеспечить предпочтение токенов (например, чтобы избежать совпадения ключевого слова в качестве идентификатора).

6 голосов
/ 17 февраля 2013

Я нашел этот в документе Python. Это просто и элегантно.

import collections
import re

Token = collections.namedtuple('Token', ['typ', 'value', 'line', 'column'])

def tokenize(s):
    keywords = {'IF', 'THEN', 'ENDIF', 'FOR', 'NEXT', 'GOSUB', 'RETURN'}
    token_specification = [
        ('NUMBER',  r'\d+(\.\d*)?'), # Integer or decimal number
        ('ASSIGN',  r':='),          # Assignment operator
        ('END',     r';'),           # Statement terminator
        ('ID',      r'[A-Za-z]+'),   # Identifiers
        ('OP',      r'[+*\/\-]'),    # Arithmetic operators
        ('NEWLINE', r'\n'),          # Line endings
        ('SKIP',    r'[ \t]'),       # Skip over spaces and tabs
    ]
    tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)
    get_token = re.compile(tok_regex).match
    line = 1
    pos = line_start = 0
    mo = get_token(s)
    while mo is not None:
        typ = mo.lastgroup
        if typ == 'NEWLINE':
            line_start = pos
            line += 1
        elif typ != 'SKIP':
            val = mo.group(typ)
            if typ == 'ID' and val in keywords:
                typ = val
            yield Token(typ, val, line, mo.start()-line_start)
        pos = mo.end()
        mo = get_token(s, pos)
    if pos != len(s):
        raise RuntimeError('Unexpected character %r on line %d' %(s[pos], line))

statements = '''
    IF quantity THEN
        total := total + price * quantity;
        tax := price * 0.05;
    ENDIF;
'''

for token in tokenize(statements):
    print(token)

Хитрость здесь в следующем:

tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification)

Здесь (?P<ID>PATTERN) помечает сопоставленный результат именем, указанным ID.

3 голосов
/ 25 сентября 2008

Возможно, что объединение регулярных выражений токена сработает, но вам придется его сравнить. Что-то вроде:

x = re.compile('(?P<NUMBER>[0-9]+)|(?P<VAR>[a-z]+)')
a = x.match('9999').groupdict() # => {'VAR': None, 'NUMBER': '9999'}
if a:
    token = [a for a in a.items() if a[1] != None][0]

Фильтр - это то место, где вам нужно будет провести сравнительный анализ ...

Обновление: Я проверил это, и похоже, что вы объединили все токены, как указано, и написали функцию вроде:

def find_token(lst):
    for tok in lst:
        if tok[1] != None: return tok
    raise Exception

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

3 голосов
/ 25 сентября 2008

re.match закреплено. Вы можете указать аргумент позиции:

pos = 0
end = len(text)
while pos < end:
    match = regexp.match(text, pos)
    # do something with your match
    pos = match.end()

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

1 голос
/ 25 сентября 2008

Это не совсем прямой ответ на ваш вопрос, но вы можете посмотреть на ANTLR . Согласно этому документу цель генерации кода Python должна быть актуальной.

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

Тем не менее, я думаю, что если вы собираетесь пойти по этому пути, вам стоит попробовать что-то вроде ANTLR , которое будет легче поддерживать, быстрее и с меньшей вероятностью иметь ошибки.

...