2008-09-25 21 views
14

Los analizadores léxicos son bastante fáciles de escribir cuando tiene expresiones regulares. Hoy quería escribir un analizador simple y general en Python, y se acercó con:Empareje eficazmente múltiples expresiones regulares en 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 

Funciona muy bien, pero estoy un poco preocupado de que sea demasiado ineficiente. ¿Hay algún truco de expresiones regulares que me permita escribirlo de una manera más eficiente/elegante?

Específicamente, ¿hay alguna forma de evitar alternar linealmente todas las reglas de expresiones regulares para encontrar una que encaje?

Respuesta

7

Puede fusionar todas sus expresiones regulares en una utilizando el "|" operador y deje que la biblioteca de expresiones regulares haga el trabajo de discernir entre tokens. Se debe tener cuidado para garantizar la preferencia de los tokens (por ejemplo, para evitar que coincida una palabra clave como un identificador).

+1

¿Cómo hago que devuelva el tipo correcto para cada una de las opciones? –

+0

Usa grupos de captura. Encerrar una parte de una expresión regular entre paréntesis la convierte en un grupo de captura que se puede recuperar del objeto de coincidencia, por ejemplo re.match ("(a) | (b)", "b"). Groups() = (Ninguno, "segundo"). El primer grupo no coincidía, el segundo correspondía con "b". –

+0

Pero aún tendré que caminar linealmente sobre los grupos de captura? –

3

re.match está anclado. Se le puede dar un argumento de posición:

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

Tener una mirada para Pygments que se incluye un chingo de lexers de resaltado de sintaxis con propósitos diferentes implementaciones, la mayoría basadas en expresiones regulares.

+0

¿Cómo funciona esto? –

+0

¿Cómo funciona qué ayuda? ¿Anclaje? No es necesario cortar el texto. –

+0

Ya veo. ¿Así que supongo que podré guardar el tiempo de rodaje? –

1

Esto no es exactamente una respuesta directa a su pregunta, pero es posible que desee consultar ANTLR. De acuerdo con el documento this, el objetivo de generación de código python debe estar actualizado.

En cuanto a sus expresiones regulares, hay realmente dos formas de acelerar el proceso si se atiene a las expresiones regulares. La primera sería ordenar sus expresiones regulares en el orden de la probabilidad de encontrarlas en un texto predeterminado. Podría figurar agregar un generador de perfiles simple al código que recopila los recuentos de tokens para cada tipo de token y ejecutar el lexer en un cuerpo de trabajo. La otra solución sería agrupar ordenadamente sus expresiones regulares (dado que su espacio clave, que es un carácter, es relativamente pequeño) y luego usar una matriz o diccionario para realizar las expresiones regulares necesarias después de realizar una única discriminación en el primer carácter.

Sin embargo, creo que si vas a seguir esta ruta, deberías probar algo como ANTLR que será más fácil de mantener, más rápido y con menos probabilidades de tener errores.

3

Es posible que la combinación de las expresiones regulares token funcione, pero tendría que compararla. Algo así como:

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] 

El filtro es donde se tiene que hacer una evaluación comparativa ...

Actualización: He probado esto, y parece que si se combinan todas las fichas como se dijo y escribir una función como:

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

Usted obtendrá más o menos la misma velocidad (tal vez un teensy más rápido) para esto. Creo que la aceleración debe estar en el número de llamadas para que coincida, pero el ciclo para la discriminación de token todavía está allí, lo que por supuesto lo mata.

+0

Puede usar 'a.lastgroup' para obtener el nombre del último grupo coincidente, y' a.group (a.lastgroup) 'para obtener la cadena correspondiente para ese grupo. No es necesario construir todo el diccionario y encontrar la entrada que no es 'Ninguna'. –

0

estos no son tan simples, pero puede ser vale la pena mirar ...

módulo de Python pyparsing (pyparsing.wikispaces.com) permite especificar la gramática - a continuación, utilizarlo para analizar el texto.Douglas, gracias por la publicación sobre ANTLR No he oído hablar de ella. También está PLY - implementación compatible con python2 y python3 de lex/yacc.

He escrito un analizador basado en expresiones regulares ad hoc mismo principio, pero luego se dio cuenta de que podría beneficiarse del uso de alguna herramienta de análisis madura y conceptos de la gramática independiente contexto de aprendizaje, etc.

La ventaja de usar La gramática para el análisis es que puede modificar fácilmente las reglas y formalizar una sintaxis bastante compleja para lo que sea que esté analizando.

11

Sugiero usar la clase re.Scanner, no está documentada en la biblioteca estándar, pero vale la pena usarla. Aquí hay un ejemplo:

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] 
+1

Creo que los tokens deberían ser una lista de pares (de texto, etiqueta). Devolver la secuencia de valores coincidentes no sería muy útil para el análisis posterior. – Meow

+0

También es desafortunado que esto no se haya implementado como generador. Analiza todo de una vez y devuelve una lista. –

6

Encontré this en el documento python. Es simple y elegante.

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) 

El truco aquí es la línea:

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

Aquí (?P<ID>PATTERN) marcará el resultado coincide con un nombre especificado por ID.

Cuestiones relacionadas