2008-09-25 6 views
14

Les analyseurs lexicaux sont assez faciles à écrire lorsque vous avez des expressions rationnelles. Aujourd'hui, je voulais écrire un analyseur simple, général en Python, et est venu avec:Correspondance efficace de plusieurs expressions rationnelles 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 

Il fonctionne très bien, mais je suis un peu inquiet qu'il est trop inefficace. Existe-t-il des astuces regex qui me permettront de l'écrire de manière plus efficace/élégante? Plus précisément, existe-t-il un moyen d'éviter de faire une boucle linéaire sur toutes les règles regex pour trouver celle qui convient?

Répondre

7

Vous pouvez fusionner toutes vos expressions rationnelles en une en utilisant le "|" opérateur et laissez la bibliothèque regex faire le travail de discernement entre les jetons. Des précautions doivent être prises pour s'assurer de la préférence des jetons (par exemple pour éviter de faire correspondre un mot-clé comme identifiant).

+1

Comment puis-je le faire retourner le bon type pour chacun des choix? –

+0

Utilisez des groupes de capture. L'inclusion d'une partie d'une regex entre parenthèses en fait un groupe de capture qui peut être extrait de l'objet de correspondance, par exemple re.match ("(a) | (b)", "b"). Groups() = (Aucun, "b"). Le premier groupe ne correspond pas, le second correspond à "b". –

+0

Mais je devrai encore marcher linéairement sur les groupes de capture? –

3

re.match est ancré. Vous pouvez lui donner un argument de position:

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

Jetez un oeil pour pygments qui est livré un shitload de lexers pour la coloration syntaxique avec des fins différentes implémentations, la plupart basées sur des expressions régulières.

+0

Comment cela vous aide-t-il? –

+0

Comment ça aide? Ancrage? Pas besoin de découper le texte. –

+0

Je vois. Donc, je sais que je serai en mesure de sauver le temps de tranchage prend? –

1

Ce n'est pas exactement une réponse directe à votre question, mais vous voudrez peut-être regarder ANTLR. Selon le document this, la cible de génération de code python doit être à jour.

En ce qui concerne vos expressions rationnelles, il y a vraiment deux façons de l'accélérer si vous vous en tenez aux expressions régulières. Le premier serait de commander vos regexes dans l'ordre de la probabilité de les trouver dans un texte par défaut. Vous pourriez figurer l'ajout d'un profileur simple au code qui a collecté les comptes de jetons pour chaque type de jeton et l'exécution du lexer sur un corps de travail. L'autre solution consisterait à trier les expressions rationnelles (puisque votre espace clé, étant un personnage, est relativement petit), puis à utiliser un tableau ou un dictionnaire pour effectuer les regexes nécessaires après avoir effectué une seule discrimination sur le premier caractère.

Cependant, je pense que si vous allez suivre cette route, vous devriez vraiment essayer quelque chose comme ANTLR qui sera plus facile à maintenir, plus rapide, et moins susceptible d'avoir des bugs.

3

Il est possible que la combinaison des expressions régulières du jeton fonctionne, mais vous devrez le référencer. Quelque chose comme:

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] 

Le filtre est l'endroit où vous devrez faire des analyses comparatives ...

Mise à jour: J'ai testé cela, et il semble que si vous combinez tous les jetons comme indiqué et écrire une fonction comme:

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

Vous obtiendrez à peu près la même vitesse (peut-être un teensy plus rapide) pour cela. Je crois que l'accélération doit être dans le nombre d'appels à faire correspondre, mais la boucle pour la discrimination de jetons est toujours là, ce qui bien sûr le tue.

+0

Vous pouvez utiliser 'a.lastgroup' pour obtenir le nom du dernier groupe correspondant, et' a.group (a.lastgroup) 'pour obtenir la chaîne correspondante pour ce groupe. Pas besoin de construire le dictionnaire entier et de trouver l'entrée qui n'est pas 'None'. –

0

ceux-ci ne sont pas si simples, mais peut-être intéressant de regarder ...

module python pyparsing (pyparsing.wikispaces.com) permet de spécifier la grammaire - puis l'utiliser pour analyser le texte.Douglas, merci pour l'article sur ANTLR Je n'en ai pas entendu parler. Il y a également PLY - implémentation compatible python2 et python3 de lex/yacc.

J'ai écrit un ad-hoc moi-même analyseur à base d'expressions régulières en premier lieu, mais réalisé plus tard que je pourrais bénéficier d'utiliser certains outils d'analyse syntaxique mature et l'apprentissage des concepts de contexte grammaire indépendante, etc.

L'avantage d'utiliser grammaire pour l'analyse est que vous pouvez facilement modifier les règles et formaliser une syntaxe assez complexe pour tout ce que vous êtes en train d'analyser.

11

Je suggère d'utiliser la classe re.Scanner, ce n'est pas documenté dans la bibliothèque standard, mais cela vaut la peine d'être utilisé. Voici un exemple:

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

Je pense que les jetons devraient être une liste de paires (texte, étiquette). Renvoyer juste la séquence des valeurs correspondantes ne serait pas très utile pour l'analyse ultérieure. – Meow

+0

Il est également regrettable que cela ne soit pas implémenté en tant que générateur. Il analyse le tout en une fois et renvoie une liste. –

6

J'ai trouvé this dans un document python. C'est juste simple et élégant.

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) 

L'astuce ici est la ligne:

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

Ici (?P<ID>PATTERN) marquera le résultat assorti d'un nom spécifié par ID.

Questions connexes