2008-09-25 171 views
14

當你有正則表達式時,詞法分析器很容易編寫。今天我想用Python語言編寫一個簡單的一般分析,並與上前:在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 

它工作得很好,但我有點擔心它的效率太低。是否有任何正則表達式技巧可以讓我以更高效/優雅的方式編寫它?

特別是,有沒有辦法避免循環遍歷所有正則表達式規則線性查找一個適合?

回答

7

您可以使用「|」將所有正則表達式合併爲一個正則表達式。運算符,並讓正則表達式庫在令牌之間進行辨別。應該注意確保令牌的偏好(例如避免將關鍵字作爲標識符)。

3

re.match被錨定。你可以給它一個位置參數:

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

看一看針對船舶詞法分析器的語法高亮顯示的目的有不同的實現一個shitload,大多數基於正則表達式Pygments來做。

+0

這有什麼用? – 2008-09-25 15:52:28

+0

如何幫助?錨定?無需分割文本。 – 2008-09-25 19:36:05

+0

我明白了。所以我覺得我可以節省時間片需要? – 2008-09-26 04:52:20

1

這不完全是直接回答你的問題,但你可能想看看ANTLR。根據this文檔,python代碼生成目標應該是最新的。

至於你的正則表達式,如果你堅持正則表達式,真的有兩種方法可以加快速度。第一種方法是按照默認文本中查找正則表達式的順序排列正則表達式。您可以將一個簡單的分析器添加到爲每個標記類型收集標記計數並在工作體上運行詞法分析器的代碼中。另一種解決方案是對你的正則表達式進行排序(因爲你的密鑰空間是一個字符,相對較小),然後在對第一個字符進行單一區分後使用數組或字典來執行所需的正則表達式。

但是,我認爲如果你要走這條路線,你應該嘗試像ANTLR這樣的東西,這樣會更容易維護,更快,並且不太可能有錯誤。

3

將令牌正則表達式組合起來可能會起作用,但您必須對其進行基準測試。喜歡的東西:

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 

你會得到大致相同的速度(也許更快)。我相信加速必須在匹配的呼叫數量上,但是令牌歧視的循環仍然存在,這當然會殺死它。

0

這些都不是那麼簡單,但可能是值得考慮的......

Python模塊pyparsing(pyparsing.wikispaces.com)允許指定語法 - 然後用它來解析文本。道格拉斯,感謝有關ANTLR我沒有聽說過它。還有PLY - python2和python3兼容lex/yacc的實現。

我已經寫了一個特設的基於正則表達式解析器自己放在第一位,但後來意識到,我可能會使用一些成熟的分析工具和學習上下文無關文法的概念中受益等

使用的優點解析語法是,您可以輕鬆修改規則,併爲解析任何內容形成相當複雜的語法。

11

我建議使用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] 
6

我在Python文檔中發現this。它只是簡單而優雅。

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指定名稱匹配的結果。