4 Lex

lex.py是用来将输入字符串标记化。例如,假设你正在设计一个编程语言,用户的输入字符串如下:

  1. x = 3 + 42 * (s - t)

标记器将字符串分割成独立的标记:

  1. 'x','=', '3', '+', '42', '*', '(', 's', '-', 't', ')'

标记通常用一组名字来命名和表示:

  1. 'ID','EQUALS','NUMBER','PLUS','NUMBER','TIMES','LPAREN','ID','MINUS','ID','RPAREN'

将标记名和标记值本身组合起来:

  1. ('ID','x'), ('EQUALS','='), ('NUMBER','3'),('PLUS','+'), ('NUMBER','42), ('TIMES','*'),('LPAREN','('), ('ID','s'),('MINUS','-'),('ID','t'), ('RPAREN',')

正则表达式是描述标记规则的典型方法,下一节展示如何用 lex.py 实现。

4.1 Lex 的例子

下面的例子展示了如何使用 lex.py 对输入进行标记

  1. # ------------------------------------------------------------
  2. # calclex.py
  3. #
  4. # tokenizer for a simple expression evaluator for
  5. # numbers and +,-,*,/
  6. # ------------------------------------------------------------
  7. import ply.lex as lex
  8. # List of token names. This is always required
  9. tokens = (
  10. 'NUMBER',
  11. 'PLUS',
  12. 'MINUS',
  13. 'TIMES',
  14. 'DIVIDE',
  15. 'LPAREN',
  16. 'RPAREN',
  17. )
  18. # Regular expression rules for simple tokens
  19. t_PLUS = r'\+'
  20. t_MINUS = r'-'
  21. t_TIMES = r'\*'
  22. t_DIVIDE = r'/'
  23. t_LPAREN = r'\('
  24. t_RPAREN = r'\)'
  25. # A regular expression rule with some action code
  26. def t_NUMBER(t):
  27. r'\d+'
  28. t.value = int(t.value)
  29. return t
  30. # Define a rule so we can track line numbers
  31. def t_newline(t):
  32. r'\n+'
  33. t.lexer.lineno += len(t.value)
  34. # A string containing ignored characters (spaces and tabs)
  35. t_ignore = ' \t'
  36. # Error handling rule
  37. def t_error(t):
  38. print "Illegal character '%s'" % t.value[0]
  39. t.lexer.skip(1)
  40. # Build the lexer
  41. lexer = lex.lex()

为了使 lexer 工作,你需要给定一个输入,并传递给input()方法。然后,重复调用token()方法来获取标记序列,下面的代码展示了这种用法:

  1. # Test it out
  2. data = '''
  3. 3 + 4 * 10
  4. + -20 *2
  5. '''
  6. # Give the lexer some input
  7. lexer.input(data)
  8. # Tokenize
  9. while True:
  10. tok = lexer.token()
  11. if not tok: break # No more input
  12. print tok

程序执行,将给出如下输出:

  1. $ python example.py
  2. LexToken(NUMBER,3,2,1)
  3. LexToken(PLUS,'+',2,3)
  4. LexToken(NUMBER,4,2,5)
  5. LexToken(TIMES,'*',2,7)
  6. LexToken(NUMBER,10,2,10)
  7. LexToken(PLUS,'+',3,14)
  8. LexToken(MINUS,'-',3,16)
  9. LexToken(NUMBER,20,3,18)
  10. LexToken(TIMES,'*',3,20)
  11. LexToken(NUMBER,2,3,21)

Lexers 也同时支持迭代,你可以把上面的循环写成这样:

  1. for tok in lexer:
  2. print tok

由 lexer.token()方法返回的标记是 LexToken 类型的实例,拥有tok.type,tok.value,tok.linenotok.lexpos属性,下面的代码展示了如何访问这些属性:

  1. # Tokenize
  2. while True:
  3. tok = lexer.token()
  4. if not tok: break # No more input
  5. print tok.type, tok.value, tok.line, tok.lexpos

tok.typetok.value属性表示标记本身的类型和值。tok.linetok.lexpos属性包含了标记的位置信息,tok.lexpos表示标记相对于输入串起始位置的偏移。

4.2 标记列表

词法分析器必须提供一个标记的列表,这个列表将所有可能的标记告诉分析器,用来执行各种验证,同时也提供给 yacc.py 作为终结符。

在上面的例子中,是这样给定标记列表的:

  1. tokens = (
  2. 'NUMBER',
  3. 'PLUS',
  4. 'MINUS',
  5. 'TIMES',
  6. 'DIVIDE',
  7. 'LPAREN',
  8. 'RPAREN',
  9. )

4.3 标记的规则

每种标记用一个正则表达式规则来表示,每个规则需要以”t_”开头声明,表示该声明是对标记的规则定义。对于简单的标记,可以定义成这样(在 Python 中使用 raw string 能比较方便的书写正则表达式):

  1. t_PLUS = r'\+'

这里,紧跟在 t_ 后面的单词,必须跟标记列表中的某个标记名称对应。如果需要执行动作的话,规则可以写成一个方法。例如,下面的规则匹配数字字串,并且将匹配的字符串转化成 Python 的整型:

  1. def t_NUMBER(t):
  2. r'\d+'
  3. t.value = int(t.value)
  4. return t

如果使用方法的话,正则表达式写成方法的文档字符串。方法总是需要接受一个 LexToken 实例的参数,该实例有一个 t.type 的属性(字符串表示)来表示标记的类型名称,t.value 是标记值(匹配的实际的字符串),t.lineno 表示当前在源输入串中的作业行,t.lexpos 表示标记相对于输入串起始位置的偏移。默认情况下,t.type 是以t_开头的变量或方法的后面部分。方法可以在方法体里面修改这些属性。但是,如果这样做,应该返回结果 token,否则,标记将被丢弃。

在 lex 内部,lex.py 用re模块处理模式匹配,在构造最终的完整的正则式的时候,用户提供的规则按照下面的顺序加入:

  1. 所有由方法定义的标记规则,按照他们的出现顺序依次加入
  2. 由字符串变量定义的标记规则按照其正则式长度倒序后,依次加入(长的先入)
  3. 顺序的约定对于精确匹配是必要的。比如,如果你想区分‘=’和‘==’,你需要确保‘==’优先检查。如果用字符串来定义这样的表达式的话,通过将较长的正则式先加入,可以帮助解决这个问题。用方法定义标记,可以显示地控制哪个规则优先检查。

为了处理保留字,你应该写一个单一的规则来匹配这些标识,并在方法里面作特殊的查询:

  1. reserved = {
  2. 'if' : 'IF',
  3. 'then' : 'THEN',
  4. 'else' : 'ELSE',
  5. 'while' : 'WHILE',
  6. ...
  7. }
  8. tokens = ['LPAREN','RPAREN',...,'ID'] + list(reserved.values())
  9. def t_ID(t):
  10. r'[a-zA-Z_][a-zA-Z_0-9]*'
  11. t.type = reserved.get(t.value,'ID') # Check for reserved words
  12. return t

这样做可以大大减少正则式的个数,并稍稍加快处理速度。注意:你应该避免为保留字编写单独的规则,例如,如果你像下面这样写:

  1. t_FOR = r'for'
  2. t_PRINT = r'print'

但是,这些规则照样也能够匹配以这些字符开头的单词,比如’forget’或者’printed’,这通常不是你想要的。

4.4 标记的值

标记被 lex 返回后,它们的值被保存在value属性中。正常情况下,value 是匹配的实际文本。事实上,value 可以被赋为任何 Python 支持的类型。例如,当扫描到标识符的时候,你可能不仅需要返回标识符的名字,还需要返回其在符号表中的位置,可以像下面这样写:

  1. def t_ID(t):
  2. ...
  3. # Look up symbol table information and return a tuple
  4. t.value = (t.value, symbol_lookup(t.value))
  5. ...
  6. return t

需要注意的是,不推荐用其他属性来保存值,因为 yacc.py 模块只会暴露出标记的 value属 性,访问其他属性会变得不自然。如果想保存多种属性,可以将元组、字典、或者对象实例赋给 value。

4.5 丢弃标记

想丢弃像注释之类的标记,只要不返回 value 就行了,像这样:

  1. def t_COMMENT(t):
  2. r'\#.*'
  3. pass
  4. # No return value. Token discarded

为标记声明添加”ignore_”前缀同样可以达到目的:

  1. t_ignore_COMMENT = r'\#.*'

如果有多种文本需要丢弃,建议使用方法来定义规则,因为方法能够提供更精确的匹配优先级控制(方法根据出现的顺序,而字符串的正则表达式依据正则表达式的长度)

4.6 行号和位置信息

默认情况下,lex.py 对行号一无所知。因为 lex.py 根本不知道何为”行”的概念(换行符本身也作为文本的一部分)。不过,可以通过写一个特殊的规则来记录行号:

  1. # Define a rule so we can track line numbers
  2. def t_newline(t):
  3. r'\n+'
  4. t.lexer.lineno += len(t.value)

在这个规则中,当前 lexer 对象 t.lexer 的 lineno 属性被修改了,而且空行被简单的丢弃了,因为没有任何的返回。

lex.py 也不自动做列跟踪。但是,位置信息被记录在了每个标记对象的lexpos属性中,这样,就有可能来计算列信息了。例如:每当遇到新行的时候就重置列值:

  1. # Compute column.
  2. # input is the input text string
  3. # token is a token instance
  4. def find_column(input,token):
  5. last_cr = input.rfind('\n',0,token.lexpos)
  6. if last_cr < 0:
  7. last_cr = 0
  8. column = (token.lexpos - last_cr) + 1
  9. return column

通常,计算列的信息是为了指示上下文的错误位置,所以只在必要时有用。

4.7 忽略字符

t_ignore规则比较特殊,是lex.py所保留用来忽略字符的,通常用来跳过空白或者不需要的字符。虽然可以通过定义像t_newline()这样的规则来完成相同的事情,不过使用t_ignore能够提供较好的词法分析性能,因为相比普通的正则式,它被特殊化处理了。

4.8 字面字符

字面字符可以通过在词法模块中定义一个literals变量做到,例如:

  1. literals = [ '+','-','*','/' ]

或者

  1. literals = "+-*/"

字面字符是指单个字符,表示把字符本身作为标记,标记的typevalue都是字符本身。不过,字面字符是在其他正则式之后被检查的,因此如果有规则是以这些字符开头的,那么这些规则的优先级较高。

4.9 错误处理

最后,在词法分析中遇到非法字符时,t_error()用来处理这类错误。这种情况下,t.value包含了余下还未被处理的输入字串,在之前的例子中,错误处理方法是这样的:

  1. # Error handling rule
  2. def t_error(t):
  3. print "Illegal character '%s'" % t.value[0]
  4. t.lexer.skip(1)

这个例子中,我们只是简单的输出不合法的字符,并且通过调用t.lexer.skip(1)跳过一个字符。

4.10 构建和使用 lexer

函数lex.lex()使用 Python 的反射机制读取调用上下文中的正则表达式,来创建 lexer。lexer 一旦创建好,有两个方法可以用来控制 lexer 对象:

  • lexer.input(data) 重置 lexer 和输入字串
  • lexer.token() 返回下一个 LexToken 类型的标记实例,如果进行到输入字串的尾部时将返回None

推荐直接在 lex() 函数返回的 lexer 对象上调用上述接口,尽管也可以向下面这样用模块级别的 lex.input() 和 lex.token():

  1. lex.lex()
  2. lex.input(sometext)
  3. while 1:
  4. tok = lex.token()
  5. if not tok: break
  6. print tok

在这个例子中,lex.input() 和 lex.token() 是模块级别的方法,在 lex 模块中,input() 和 token() 方法绑定到最新创建的 lexer 对象的对应方法上。最好不要这样用,因为这种接口可能不知道在什么时候就失效(译者注:垃圾回收?)

4.11 @TOKEN 装饰器

在一些应用中,你可能需要定义一系列辅助的记号来构建复杂的正则表达式,例如:

  1. digit = r'([0-9])'
  2. nondigit = r'([_A-Za-z])'
  3. identifier = r'(' + nondigit + r'(' + digit + r'|' + nondigit + r')*)'
  4. def t_ID(t):
  5. # want docstring to be identifier above. ?????
  6. ...

在这个例子中,我们希望 ID 的规则引用上面的已有的变量。然而,使用文档字符串无法做到,为了解决这个问题,你可以使用@TOKEN装饰器:

  1. from ply.lex import TOKEN
  2. @TOKEN(identifier)
  3. def t_ID(t):
  4. ...

装饰器可以将 identifier 关联到 t_ID() 的文档字符串上以使 lex.py 正常工作,一种等价的做法是直接给文档字符串赋值:

  1. def t_ID(t):
  2. ...
  3. t_ID.__doc__ = identifier

注意:@TOKEN 装饰器需要 Python-2.4 以上的版本。如果你在意老版本Python的兼容性问题,使用上面的等价办法。

4.12 优化模式

为了提高性能,你可能希望使用 Python 的优化模式(比如,使用 -o 选项执行 Python)。然而,这样的话,Python 会忽略文档字串,这是 lex.py 的特殊问题,可以通过在创建 lexer 的时候使用 optimize 选项:

  1. lexer = lex.lex(optimize=1)

接着,用 Python 常规的模式运行,这样,lex.py 会在当前目录下创建一个 lextab.py 文件,这个文件会包含所有的正则表达式规则和词法分析阶段的分析表。然后,lextab.py 可以被导入用来构建 lexer。这种方法大大改善了词法分析程序的启动时间,而且可以在 Python 的优化模式下工作。

想要更改生成的文件名,使用如下参数:

  1. lexer = lex.lex(optimize=1,lextab="footab")

在优化模式下执行,需要注意的是 lex 会被禁用大多数的错误检查。因此,建议只在确保万事俱备准备发布最终代码时使用。

4.13 调试

如果想要调试,可以使 lex() 运行在调试模式:

  1. lexer = lex.lex(debug=1)

这将打出一些调试信息,包括添加的规则、最终的正则表达式和词法分析过程中得到的标记。

除此之外,lex.py 有一个简单的主函数,不但支持对命令行参数输入的字串进行扫描,还支持命令行参数指定的文件名:

  1. if __name__ == '__main__':
  2. lex.runmain()

想要了解高级调试的详情,请移步至最后的高级调试部分。

4.14 其他方式定义词法规则

上面的例子,词法分析器都是在单个的 Python 模块中指定的。如果你想将标记的规则放到不同的模块,使用 module 关键字参数。例如,你可能有一个专有的模块,包含了标记的规则:

  1. # module: tokrules.py
  2. # This module just contains the lexing rules
  3. # List of token names. This is always required
  4. tokens = (
  5. 'NUMBER',
  6. 'PLUS',
  7. 'MINUS',
  8. 'TIMES',
  9. 'DIVIDE',
  10. 'LPAREN',
  11. 'RPAREN',
  12. )
  13. # Regular expression rules for simple tokens
  14. t_PLUS = r'\+'
  15. t_MINUS = r'-'
  16. t_TIMES = r'\*'
  17. t_DIVIDE = r'/'
  18. t_LPAREN = r'\('
  19. t_RPAREN = r'\)'
  20. # A regular expression rule with some action code
  21. def t_NUMBER(t):
  22. r'\d+'
  23. t.value = int(t.value)
  24. return t
  25. # Define a rule so we can track line numbers
  26. def t_newline(t):
  27. r'\n+'
  28. t.lexer.lineno += len(t.value)
  29. # A string containing ignored characters (spaces and tabs)
  30. t_ignore = ' \t'
  31. # Error handling rule
  32. def t_error(t):
  33. print "Illegal character '%s'" % t.value[0]
  34. t.lexer.skip(1)

现在,如果你想要从不同的模块中构建分析器,应该这样(在交互模式下):

  1. >>> import tokrules
  2. >>> lexer = lex.lex(module=tokrules)
  3. >>> lexer.input("3 + 4")
  4. >>> lexer.token()
  5. LexToken(NUMBER,3,1,1,0)
  6. >>> lexer.token()
  7. LexToken(PLUS,'+',1,2)
  8. >>> lexer.token()
  9. LexToken(NUMBER,4,1,4)
  10. >>> lexer.token()
  11. None

module选项也可以指定类型的实例,例如:

  1. import ply.lex as lex
  2. class MyLexer:
  3. # List of token names. This is always required
  4. tokens = (
  5. 'NUMBER',
  6. 'PLUS',
  7. 'MINUS',
  8. 'TIMES',
  9. 'DIVIDE',
  10. 'LPAREN',
  11. 'RPAREN',
  12. )
  13. # Regular expression rules for simple tokens
  14. t_PLUS = r'\+'
  15. t_MINUS = r'-'
  16. t_TIMES = r'\*'
  17. t_DIVIDE = r'/'
  18. t_LPAREN = r'\('
  19. t_RPAREN = r'\)'
  20. # A regular expression rule with some action code
  21. # Note addition of self parameter since we're in a class
  22. def t_NUMBER(self,t):
  23. r'\d+'
  24. t.value = int(t.value)
  25. return t
  26. # Define a rule so we can track line numbers
  27. def t_newline(self,t):
  28. r'\n+'
  29. t.lexer.lineno += len(t.value)
  30. # A string containing ignored characters (spaces and tabs)
  31. t_ignore = ' \t'
  32. # Error handling rule
  33. def t_error(self,t):
  34. print "Illegal character '%s'" % t.value[0]
  35. t.lexer.skip(1)
  36. # Build the lexer
  37. def build(self,**kwargs):
  38. self.lexer = lex.lex(module=self, **kwargs)
  39. # Test it output
  40. def test(self,data):
  41. self.lexer.input(data)
  42. while True:
  43. tok = lexer.token()
  44. if not tok: break
  45. print tok
  46. # Build the lexer and try it out
  47. m = MyLexer()
  48. m.build() # Build the lexer
  49. m.test("3 + 4") # Test it

当从类中定义 lexer,你需要创建类的实例,而不是类本身。这是因为,lexer 的方法只有被绑定(bound-methods)对象后才能使 PLY 正常工作。

当给 lex() 方法使用 module 选项时,PLY 使用dir()方法,从对象中获取符号信息,因为不能直接访问对象的__dict__属性。(译者注:可能是因为兼容性原因,dict这个方法可能不存在)

最后,如果你希望保持较好的封装性,但不希望什么东西都写在类里面,lexers 可以在闭包中定义,例如:

  1. import ply.lex as lex
  2. # List of token names. This is always required
  3. tokens = (
  4. 'NUMBER',
  5. 'PLUS',
  6. 'MINUS',
  7. 'TIMES',
  8. 'DIVIDE',
  9. 'LPAREN',
  10. 'RPAREN',
  11. )
  12. def MyLexer():
  13. # Regular expression rules for simple tokens
  14. t_PLUS = r'\+'
  15. t_MINUS = r'-'
  16. t_TIMES = r'\*'
  17. t_DIVIDE = r'/'
  18. t_LPAREN = r'\('
  19. t_RPAREN = r'\)'
  20. # A regular expression rule with some action code
  21. def t_NUMBER(t):
  22. r'\d+'
  23. t.value = int(t.value)
  24. return t
  25. # Define a rule so we can track line numbers
  26. def t_newline(t):
  27. r'\n+'
  28. t.lexer.lineno += len(t.value)
  29. # A string containing ignored characters (spaces and tabs)
  30. t_ignore = ' \t'
  31. # Error handling rule
  32. def t_error(t):
  33. print "Illegal character '%s'" % t.value[0]
  34. t.lexer.skip(1)
  35. # Build the lexer from my environment and return it
  36. return lex.lex()

4.15 额外状态维护

在你的词法分析器中,你可能想要维护一些状态。这可能包括模式设置,符号表和其他细节。例如,假设你想要跟踪NUMBER标记的出现个数。

一种方法是维护一个全局变量:

  1. num_count = 0
  2. def t_NUMBER(t):
  3. r'\d+'
  4. global num_count
  5. num_count += 1
  6. t.value = int(t.value)
  7. return t

如果你不喜欢全局变量,另一个记录信息的地方是 lexer 对象内部。可以通过当前标记的 lexer 属性访问:

  1. def t_NUMBER(t):
  2. r'\d+'
  3. t.lexer.num_count += 1 # Note use of lexer attribute
  4. t.value = int(t.value)
  5. return t
  6. lexer = lex.lex()
  7. lexer.num_count = 0 # Set the initial count

上面这样做的优点是当同时存在多个 lexer 实例的情况下,简单易行。不过这看上去似乎是严重违反了面向对象的封装原则。lexer 的内部属性(除了 lineno )都是以 lex 开头命名的(lexdata、lexpos)。因此,只要不以 lex 开头来命名属性就很安全的。

如果你不喜欢给 lexer 对象赋值,你可以自定义你的 lexer 类型,就像前面看到的那样:

  1. class MyLexer:
  2. ...
  3. def t_NUMBER(self,t):
  4. r'\d+'
  5. self.num_count += 1
  6. t.value = int(t.value)
  7. return t
  8. def build(self, **kwargs):
  9. self.lexer = lex.lex(object=self,**kwargs)
  10. def __init__(self):
  11. self.num_count = 0

如果你的应用会创建很多 lexer 的实例,并且需要维护很多状态,上面的类可能是最容易管理的。

状态也可以用闭包来管理,比如,在 Python3 中:

  1. def MyLexer():
  2. num_count = 0
  3. ...
  4. def t_NUMBER(t):
  5. r'\d+'
  6. nonlocal num_count
  7. num_count += 1
  8. t.value = int(t.value)
  9. return t
  10. ...

4.16 Lexer 克隆

如果有必要的话,lexer 对象可以通过clone()方法来复制:

  1. lexer = lex.lex()
  2. ...
  3. newlexer = lexer.clone()

当 lexer 被克隆后,复制品能够精确的保留输入串和内部状态,不过,新的 lexer 可以接受一个不同的输出字串,并独立运作起来。这在几种情况下也许有用:当你在编写的解析器或编译器涉及到递归或者回退处理时,你需要扫描先前的部分,你可以clone并使用复制品,或者你在实现某种预编译处理,可以 clone 一些 lexer 来处理不同的输入文件。

创建克隆跟重新调用 lex.lex() 的不同点在于,PLY 不会重新构建任何的内部分析表或者正则式。当 lexer 是用类或者闭包创建的,需要注意类或闭包本身的的状态。换句话说你要注意新创建的 lexer 会共享原始 lexer 的这些状态,比如:

  1. m = MyLexer()
  2. a = lex.lex(object=m) # Create a lexer
  3. b = a.clone() # Clone the lexer

4.17 Lexer 的内部状态

lexer 有一些内部属性在特定情况下有用:

  • lexer.lexpos。这是一个表示当前分析点的位置的整型值。如果你修改这个值的话,这会改变下一个 token() 的调用行为。在标记的规则方法里面,这个值表示紧跟匹配字串后面的第一个字符的位置,如果这个值在规则中修改,下一个返回的标记将从新的位置开始匹配
  • lexer.lineno。表示当前行号。PLY 只是声明这个属性的存在,却永远不更新这个值。如果你想要跟踪行号的话,你需要自己添加代码( 4.6 行号和位置信息)
  • lexer.lexdata。当前 lexer 的输入字串,这个字符串就是 input() 方法的输入字串,更改它可能是个糟糕的做法,除非你知道自己在干什么。
  • lexer.lexmatch。PLY 内部调用 Python 的 re.match() 方法得到的当前标记的原始的 Match 对象,该对象被保存在这个属性中。如果你的正则式中包含分组的话,你可以通过这个对象获得这些分组的值。注意:这个属性只在有标记规则定义的方法中才有效。

4.18 基于条件的扫描和启动条件

在高级的分析器应用程序中,使用状态化的词法扫描是很有用的。比如,你想在出现特定标记或句子结构的时候触发开始一个不同的词法分析逻辑。PLY 允许 lexer 在不同的状态之间转换。每个状态可以包含一些自己独特的标记和规则等。这是基于 GNU flex 的“启动条件”来实现的,关于 flex 详见 http://flex.sourceforge.net/manual/Start-Conditions.html#Start-Conditions

要使用 lex 的状态,你必须首先声明。通过在 lex 模块中声明”states”来做到:

  1. states = (
  2. ('foo','exclusive'),
  3. ('bar','inclusive'),
  4. )

这个声明中包含有两个状态:’foo’和’bar’。状态可以有两种类型:’排他型’和’包容型’。排他型的状态会使得 lexer 的行为发生完全的改变:只有能够匹配在这个状态下定义的规则的标记才会返回;包容型状态会将定义在这个状态下的规则添加到默认的规则集中,进而,只要能匹配这个规则集的标记都会返回。

一旦声明好之后,标记规则的命名需要包含状态名:

  1. t_foo_NUMBER = r'\d+' # Token 'NUMBER' in state 'foo'
  2. t_bar_ID = r'[a-zA-Z_][a-zA-Z0-9_]*' # Token 'ID' in state 'bar'
  3. def t_foo_newline(t):
  4. r'\n'
  5. t.lexer.lineno += 1

一个标记可以用在多个状态中,只要将多个状态名包含在声明中:

  1. t_foo_bar_NUMBER = r'\d+' # Defines token 'NUMBER' in both state 'foo' and 'bar'

同样的,在任何状态下都生效的声明可以在命名中使用ANY

  1. t_ANY_NUMBER = r'\d+' # Defines a token 'NUMBER' in all states

不包含状态名的情况下,标记被关联到一个特殊的状态INITIAL,比如,下面两个声明是等价的:

  1. t_NUMBER = r'\d+'
  2. t_INITIAL_NUMBER = r'\d+'

特殊的t_ignore()t_error()也可以用状态关联:

  1. t_foo_ignore = " \t\n" # Ignored characters for state 'foo'
  2. def t_bar_error(t): # Special error handler for state 'bar'
  3. pass

词法分析默认在INITIAL状态下工作,这个状态下包含了所有默认的标记规则定义。对于不希望使用“状态”的用户来说,这是完全透明的。在分析过程中,如果你想要改变词法分析器的这种的状态,使用begin()方法:

  1. def t_begin_foo(t):
  2. r'start_foo'
  3. t.lexer.begin('foo') # Starts 'foo' state

使用 begin() 切换回初始状态:

  1. def t_foo_end(t):
  2. r'end_foo'
  3. t.lexer.begin('INITIAL') # Back to the initial state

状态的切换可以使用栈:

  1. def t_begin_foo(t):
  2. r'start_foo'
  3. t.lexer.push_state('foo') # Starts 'foo' state
  4. def t_foo_end(t):
  5. r'end_foo'
  6. t.lexer.pop_state() # Back to the previous state

当你在面临很多状态可以选择进入,而又仅仅想要回到之前的状态时,状态栈比较有用。

举个例子会更清晰。假设你在写一个分析器想要从一堆 C 代码中获取任意匹配的闭合的大括号里面的部分:这意味着,当遇到起始括号’{‘,你需要读取与之匹配的’}’以上的所有部分。并返回字符串。使用通常的正则表达式几乎不可能,这是因为大括号可以嵌套,而且可以有注释,字符串等干扰。因此,试图简单的匹配第一个出现的’}’是不行的。这里你可以用lex的状态来做到:

  1. # Declare the state
  2. states = (
  3. ('ccode','exclusive'),
  4. )
  5. # Match the first {. Enter ccode state.
  6. def t_ccode(t):
  7. r'\{'
  8. t.lexer.code_start = t.lexer.lexpos # Record the starting position
  9. t.lexer.level = 1 # Initial brace level
  10. t.lexer.begin('ccode') # Enter 'ccode' state
  11. # Rules for the ccode state
  12. def t_ccode_lbrace(t):
  13. r'\{'
  14. t.lexer.level +=1
  15. def t_ccode_rbrace(t):
  16. r'\}'
  17. t.lexer.level -=1
  18. # If closing brace, return the code fragment
  19. if t.lexer.level == 0:
  20. t.value = t.lexer.lexdata[t.lexer.code_start:t.lexer.lexpos+1]
  21. t.type = "CCODE"
  22. t.lexer.lineno += t.value.count('\n')
  23. t.lexer.begin('INITIAL')
  24. return t
  25. # C or C++ comment (ignore)
  26. def t_ccode_comment(t):
  27. r'(/\*(.|\n)*?*/)|(//.*)'
  28. pass
  29. # C string
  30. def t_ccode_string(t):
  31. r'\"([^\\\n]|(\\.))*?\"'
  32. # C character literal
  33. def t_ccode_char(t):
  34. r'\'([^\\\n]|(\\.))*?\''
  35. # Any sequence of non-whitespace characters (not braces, strings)
  36. def t_ccode_nonspace(t):
  37. r'[^\s\{\}\'\"]+'
  38. # Ignored characters (whitespace)
  39. t_ccode_ignore = " \t\n"
  40. # For bad characters, we just skip over it
  41. def t_ccode_error(t):
  42. t.lexer.skip(1)

这个例子中,第一个’{‘使得 lexer 记录了起始位置,并且进入新的状态’ccode’。一系列规则用来匹配接下来的输入,这些规则只是丢弃掉标记(不返回值),如果遇到闭合右括号,t_ccode_rbrace 规则收集其中所有的代码(利用先前记录的开始位置),并保存,返回的标记类型为’CCODE’,与此同时,词法分析的状态退回到初始状态。

4.19 其他问题

  • lexer 需要输入的是一个字符串。好在大多数机器都有足够的内存,这很少导致性能的问题。这意味着,lexer 现在还不能用来处理文件流或者 socket 流。这主要是受到 re 模块的限制。
  • lexer 支持用 Unicode 字符描述标记的匹配规则,也支持输入字串包含 Unicode
  • 如果你想要向re.compile()方法提供 flag,使用 reflags 选项:lex.lex(reflags=re.UNICODE)
  • 由于 lexer 是全部用 Python 写的,性能很大程度上取决于 Python 的 re 模块,即使已经尽可能的高效了。当接收极其大量的输入文件时表现并不尽人意。如果担忧性能,你可以升级到最新的 Python,或者手工创建分析器,或者用 C 语言写 lexer 并做成扩展模块。

如果你要创建一个手写的词法分析器并计划用在 yacc.py 中,只需要满足下面的要求:

  • 需要提供一个 token() 方法来返回下一个标记,如果没有可用的标记了,则返回 None。
  • token() 方法必须返回一个 tok 对象,具有 type 和 valu e属性。如果行号需要跟踪的话,标记还需要定义 lineno 属性。