在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ Python/ Lex
高級(jí)調(diào)試
PLY 概要
前言和預(yù)備
如何繼續(xù)
Yacc
使用Python的優(yōu)化模式
語法分析基礎(chǔ)
Lex
多個(gè)語法和詞法分析器
介紹
序言

Lex

lex.py是用來將輸入字符串標(biāo)記化。例如,假設(shè)你正在設(shè)計(jì)一個(gè)編程語言,用戶的輸入字符串如下:

x = 3 + 42 * (s - t)

標(biāo)記器將字符串分割成獨(dú)立的標(biāo)記:

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

標(biāo)記通常用一組名字來命名和表示:

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

將標(biāo)記名和標(biāo)記值本身組合起來:

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

正則表達(dá)式是描述標(biāo)記規(guī)則的典型方法,下一節(jié)展示如何用 lex.py 實(shí)現(xiàn)。

Lex 的例子

下面的例子展示了如何使用 lex.py 對(duì)輸入進(jìn)行標(biāo)記

# ------------------------------------------------------------
# calclex.py
#
# tokenizer for a simple expression evaluator for
# numbers and +,-,*,/
# ------------------------------------------------------------
import ply.lex as lex

# List of token names.   This is always required
tokens = (
   'NUMBER',
   'PLUS',
   'MINUS',
   'TIMES',
   'DIVIDE',
   'LPAREN',
   'RPAREN',
)

# Regular expression rules for simple tokens
t_PLUS    = r'\+'
t_MINUS   = r'-'
t_TIMES   = r'\*'
t_DIVIDE  = r'/'
t_LPAREN  = r'\('
t_RPAREN  = r'\)'

# A regular expression rule with some action code
def t_NUMBER(t):
    r'\d+'
    t.value = int(t.value)    
    return t

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

# A string containing ignored characters (spaces and tabs)
t_ignore  = ' \t'

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

# Build the lexer
lexer = lex.lex()

為了使 lexer 工作,你需要給定一個(gè)輸入,并傳遞給input()方法。然后,重復(fù)調(diào)用token()方法來獲取標(biāo)記序列,下面的代碼展示了這種用法:

# Test it out
data = '''
3 + 4 * 10
  + -20 *2
'''

# Give the lexer some input
lexer.input(data)

# Tokenize
while True:
    tok = lexer.token()
    if not tok: break      # No more input
    print tok

程序執(zhí)行,將給出如下輸出:

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

Lexers 也同時(shí)支持迭代,你可以把上面的循環(huán)寫成這樣:

for tok in lexer:
    print tok

由 lexer.token()方法返回的標(biāo)記是 LexToken 類型的實(shí)例,擁有tok.type,tok.value,tok.linenotok.lexpos屬性,下面的代碼展示了如何訪問這些屬性:

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

tok.typetok.value屬性表示標(biāo)記本身的類型和值。tok.linetok.lexpos屬性包含了標(biāo)記的位置信息,tok.lexpos表示標(biāo)記相對(duì)于輸入串起始位置的偏移。

標(biāo)記列表

詞法分析器必須提供一個(gè)標(biāo)記的列表,這個(gè)列表將所有可能的標(biāo)記告訴分析器,用來執(zhí)行各種驗(yàn)證,同時(shí)也提供給 yacc.py 作為終結(jié)符。

在上面的例子中,是這樣給定標(biāo)記列表的:

tokens = (
   'NUMBER',
   'PLUS',
   'MINUS',
   'TIMES',
   'DIVIDE',
   'LPAREN',
   'RPAREN',
)

標(biāo)記的規(guī)則

每種標(biāo)記用一個(gè)正則表達(dá)式規(guī)則來表示,每個(gè)規(guī)則需要以"t_"開頭聲明,表示該聲明是對(duì)標(biāo)記的規(guī)則定義。對(duì)于簡(jiǎn)單的標(biāo)記,可以定義成這樣(在 Python 中使用 raw string 能比較方便的書寫正則表達(dá)式):

t_PLUS = r'\+'

這里,緊跟在 t_ 后面的單詞,必須跟標(biāo)記列表中的某個(gè)標(biāo)記名稱對(duì)應(yīng)。如果需要執(zhí)行動(dòng)作的話,規(guī)則可以寫成一個(gè)方法。例如,下面的規(guī)則匹配數(shù)字字串,并且將匹配的字符串轉(zhuǎn)化成 Python 的整型:

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

如果使用方法的話,正則表達(dá)式寫成方法的文檔字符串。方法總是需要接受一個(gè) LexToken 實(shí)例的參數(shù),該實(shí)例有一個(gè) t.type 的屬性(字符串表示)來表示標(biāo)記的類型名稱,t.value 是標(biāo)記值(匹配的實(shí)際的字符串),t.lineno 表示當(dāng)前在源輸入串中的作業(yè)行,t.lexpos 表示標(biāo)記相對(duì)于輸入串起始位置的偏移。默認(rèn)情況下,t.type 是以t_開頭的變量或方法的后面部分。方法可以在方法體里面修改這些屬性。但是,如果這樣做,應(yīng)該返回結(jié)果 token,否則,標(biāo)記將被丟棄。

在 lex 內(nèi)部,lex.py 用re模塊處理模式匹配,在構(gòu)造最終的完整的正則式的時(shí)候,用戶提供的規(guī)則按照下面的順序加入:

  1. 所有由方法定義的標(biāo)記規(guī)則,按照他們的出現(xiàn)順序依次加入
  2. 由字符串變量定義的標(biāo)記規(guī)則按照其正則式長(zhǎng)度倒序后,依次加入(長(zhǎng)的先入)
  3. 順序的約定對(duì)于精確匹配是必要的。比如,如果你想?yún)^(qū)分‘=’和‘==’,你需要確?!?=’優(yōu)先檢查。如果用字符串來定義這樣的表達(dá)式的話,通過將較長(zhǎng)的正則式先加入,可以幫助解決這個(gè)問題。用方法定義標(biāo)記,可以顯示地控制哪個(gè)規(guī)則優(yōu)先檢查。

為了處理保留字,你應(yīng)該寫一個(gè)單一的規(guī)則來匹配這些標(biāo)識(shí),并在方法里面作特殊的查詢:

reserved = {
   'if' : 'IF',
   'then' : 'THEN',
   'else' : 'ELSE',
   'while' : 'WHILE',
   ...
}

tokens = ['LPAREN','RPAREN',...,'ID'] + list(reserved.values())

def t_ID(t):
    r'[a-zA-Z_][a-zA-Z_0-9]*'
    t.type = reserved.get(t.value,'ID')    # Check for reserved words
    return t

這樣做可以大大減少正則式的個(gè)數(shù),并稍稍加快處理速度。注意:你應(yīng)該避免為保留字編寫單獨(dú)的規(guī)則,例如,如果你像下面這樣寫:

t_FOR   = r'for'
t_PRINT = r'print'

但是,這些規(guī)則照樣也能夠匹配以這些字符開頭的單詞,比如'forget'或者'printed',這通常不是你想要的。

標(biāo)記的值

標(biāo)記被 lex 返回后,它們的值被保存在value屬性中。正常情況下,value 是匹配的實(shí)際文本。事實(shí)上,value 可以被賦為任何 Python 支持的類型。例如,當(dāng)掃描到標(biāo)識(shí)符的時(shí)候,你可能不僅需要返回標(biāo)識(shí)符的名字,還需要返回其在符號(hào)表中的位置,可以像下面這樣寫:

def t_ID(t):
    ...
    # Look up symbol table information and return a tuple
    t.value = (t.value, symbol_lookup(t.value))
    ...
    return t

需要注意的是,不推薦用其他屬性來保存值,因?yàn)?yacc.py 模塊只會(huì)暴露出標(biāo)記的 value屬 性,訪問其他屬性會(huì)變得不自然。如果想保存多種屬性,可以將元組、字典、或者對(duì)象實(shí)例賦給 value。

丟棄標(biāo)記

想丟棄像注釋之類的標(biāo)記,只要不返回 value 就行了,像這樣:

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

為標(biāo)記聲明添加"ignore_"前綴同樣可以達(dá)到目的:

t_ignore_COMMENT = r'\#.*'

如果有多種文本需要丟棄,建議使用方法來定義規(guī)則,因?yàn)榉椒軌蛱峁└_的匹配優(yōu)先級(jí)控制(方法根據(jù)出現(xiàn)的順序,而字符串的正則表達(dá)式依據(jù)正則表達(dá)式的長(zhǎng)度)

行號(hào)和位置信息

默認(rèn)情況下,lex.py 對(duì)行號(hào)一無所知。因?yàn)?lex.py 根本不知道何為"行"的概念(換行符本身也作為文本的一部分)。不過,可以通過寫一個(gè)特殊的規(guī)則來記錄行號(hào):

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

在這個(gè)規(guī)則中,當(dāng)前 lexer 對(duì)象 t.lexer 的 lineno 屬性被修改了,而且空行被簡(jiǎn)單的丟棄了,因?yàn)闆]有任何的返回。

lex.py 也不自動(dòng)做列跟蹤。但是,位置信息被記錄在了每個(gè)標(biāo)記對(duì)象的lexpos屬性中,這樣,就有可能來計(jì)算列信息了。例如:每當(dāng)遇到新行的時(shí)候就重置列值:

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

通常,計(jì)算列的信息是為了指示上下文的錯(cuò)誤位置,所以只在必要時(shí)有用。

忽略字符

t_ignore規(guī)則比較特殊,是lex.py所保留用來忽略字符的,通常用來跳過空白或者不需要的字符。雖然可以通過定義像t_newline()這樣的規(guī)則來完成相同的事情,不過使用t_ignore能夠提供較好的詞法分析性能,因?yàn)橄啾绕胀ǖ恼齽t式,它被特殊化處理了。

字面字符

字面字符可以通過在詞法模塊中定義一個(gè)literals變量做到,例如:

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

或者

literals = "+-*/"

字面字符是指單個(gè)字符,表示把字符本身作為標(biāo)記,標(biāo)記的typevalue都是字符本身。不過,字面字符是在其他正則式之后被檢查的,因此如果有規(guī)則是以這些字符開頭的,那么這些規(guī)則的優(yōu)先級(jí)較高。

錯(cuò)誤處理

最后,在詞法分析中遇到非法字符時(shí),t_error()用來處理這類錯(cuò)誤。這種情況下,t.value包含了余下還未被處理的輸入字串,在之前的例子中,錯(cuò)誤處理方法是這樣的:

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

這個(gè)例子中,我們只是簡(jiǎn)單的輸出不合法的字符,并且通過調(diào)用t.lexer.skip(1)跳過一個(gè)字符。

構(gòu)建和使用 lexer

函數(shù)lex.lex()使用 Python 的反射機(jī)制讀取調(diào)用上下文中的正則表達(dá)式,來創(chuàng)建 lexer。lexer 一旦創(chuàng)建好,有兩個(gè)方法可以用來控制 lexer 對(duì)象:

  • lexer.input(data) 重置 lexer 和輸入字串
  • lexer.token() 返回下一個(gè) LexToken 類型的標(biāo)記實(shí)例,如果進(jìn)行到輸入字串的尾部時(shí)將返回None

推薦直接在 lex() 函數(shù)返回的 lexer 對(duì)象上調(diào)用上述接口,盡管也可以向下面這樣用模塊級(jí)別的 lex.input() 和 lex.token():

lex.lex()
lex.input(sometext)
while 1:
    tok = lex.token()
    if not tok: break
    print tok

在這個(gè)例子中,lex.input() 和 lex.token() 是模塊級(jí)別的方法,在 lex 模塊中,input() 和 token() 方法綁定到最新創(chuàng)建的 lexer 對(duì)象的對(duì)應(yīng)方法上。最好不要這樣用,因?yàn)檫@種接口可能不知道在什么時(shí)候就失效(譯者注:垃圾回收?)

@TOKEN 裝飾器

在一些應(yīng)用中,你可能需要定義一系列輔助的記號(hào)來構(gòu)建復(fù)雜的正則表達(dá)式,例如:

digit            = r'([0-9])'
nondigit         = r'([_A-Za-z])'
identifier       = r'(' + nondigit + r'(' + digit + r'|' + nondigit + r')*)'        

def t_ID(t):
    # want docstring to be identifier above. ?????
    ...

在這個(gè)例子中,我們希望 ID 的規(guī)則引用上面的已有的變量。然而,使用文檔字符串無法做到,為了解決這個(gè)問題,你可以使用@TOKEN裝飾器:

from ply.lex import TOKEN

@TOKEN(identifier)
def t_ID(t):
    ...

裝飾器可以將 identifier 關(guān)聯(lián)到 t_ID() 的文檔字符串上以使 lex.py 正常工作,一種等價(jià)的做法是直接給文檔字符串賦值:

def t_ID(t):
    ...

t_ID.__doc__ = identifier

注意:@TOKEN 裝飾器需要 Python-2.4 以上的版本。如果你在意老版本Python的兼容性問題,使用上面的等價(jià)辦法。

優(yōu)化模式

為了提高性能,你可能希望使用 Python 的優(yōu)化模式(比如,使用 -o 選項(xiàng)執(zhí)行 Python)。然而,這樣的話,Python 會(huì)忽略文檔字串,這是 lex.py 的特殊問題,可以通過在創(chuàng)建 lexer 的時(shí)候使用 optimize 選項(xiàng):

lexer = lex.lex(optimize=1)

接著,用 Python 常規(guī)的模式運(yùn)行,這樣,lex.py 會(huì)在當(dāng)前目錄下創(chuàng)建一個(gè) lextab.py 文件,這個(gè)文件會(huì)包含所有的正則表達(dá)式規(guī)則和詞法分析階段的分析表。然后,lextab.py 可以被導(dǎo)入用來構(gòu)建 lexer。這種方法大大改善了詞法分析程序的啟動(dòng)時(shí)間,而且可以在 Python 的優(yōu)化模式下工作。

想要更改生成的文件名,使用如下參數(shù):

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

在優(yōu)化模式下執(zhí)行,需要注意的是 lex 會(huì)被禁用大多數(shù)的錯(cuò)誤檢查。因此,建議只在確保萬事俱備準(zhǔn)備發(fā)布最終代碼時(shí)使用。

調(diào)試

如果想要調(diào)試,可以使 lex() 運(yùn)行在調(diào)試模式:

lexer = lex.lex(debug=1)

這將打出一些調(diào)試信息,包括添加的規(guī)則、最終的正則表達(dá)式和詞法分析過程中得到的標(biāo)記。

除此之外,lex.py 有一個(gè)簡(jiǎn)單的主函數(shù),不但支持對(duì)命令行參數(shù)輸入的字串進(jìn)行掃描,還支持命令行參數(shù)指定的文件名:

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

想要了解高級(jí)調(diào)試的詳情,請(qǐng)移步至最后的高級(jí)調(diào)試部分。

其他方式定義詞法規(guī)則

上面的例子,詞法分析器都是在單個(gè)的 Python 模塊中指定的。如果你想將標(biāo)記的規(guī)則放到不同的模塊,使用 module 關(guān)鍵字參數(shù)。例如,你可能有一個(gè)專有的模塊,包含了標(biāo)記的規(guī)則:

# module: tokrules.py
# This module just contains the lexing rules

# List of token names.   This is always required
tokens = (
   'NUMBER',
   'PLUS',
   'MINUS',
   'TIMES',
   'DIVIDE',
   'LPAREN',
   'RPAREN',
)

# Regular expression rules for simple tokens
t_PLUS    = r'\+'
t_MINUS   = r'-'
t_TIMES   = r'\*'
t_DIVIDE  = r'/'
t_LPAREN  = r'\('
t_RPAREN  = r'\)'

# A regular expression rule with some action code
def t_NUMBER(t):
    r'\d+'
    t.value = int(t.value)    
    return t

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

# A string containing ignored characters (spaces and tabs)
t_ignore  = ' \t'

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

現(xiàn)在,如果你想要從不同的模塊中構(gòu)建分析器,應(yīng)該這樣(在交互模式下):

>>> import tokrules
>>> lexer = lex.lex(module=tokrules)
>>> lexer.input("3 + 4")
>>> lexer.token()
LexToken(NUMBER,3,1,1,0)
>>> lexer.token()
LexToken(PLUS,'+',1,2)
>>> lexer.token()
LexToken(NUMBER,4,1,4)
>>> lexer.token()
None

module選項(xiàng)也可以指定類型的實(shí)例,例如:

import ply.lex as lex

class MyLexer:
    # List of token names.   This is always required
    tokens = (
       'NUMBER',
       'PLUS',
       'MINUS',
       'TIMES',
       'DIVIDE',
       'LPAREN',
       'RPAREN',
    )

    # Regular expression rules for simple tokens
    t_PLUS    = r'\+'
    t_MINUS   = r'-'
    t_TIMES   = r'\*'
    t_DIVIDE  = r'/'
    t_LPAREN  = r'\('
    t_RPAREN  = r'\)'

    # A regular expression rule with some action code
    # Note addition of self parameter since we're in a class
    def t_NUMBER(self,t):
        r'\d+'
        t.value = int(t.value)    
        return t

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

    # A string containing ignored characters (spaces and tabs)
    t_ignore  = ' \t'

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

    # Build the lexer
    def build(self,**kwargs):
        self.lexer = lex.lex(module=self, **kwargs)

    # Test it output
    def test(self,data):
        self.lexer.input(data)
        while True:
             tok = lexer.token()
             if not tok: break
             print tok

# Build the lexer and try it out
m = MyLexer()
m.build()           # Build the lexer
m.test("3 + 4")     # Test it

當(dāng)從類中定義 lexer,你需要?jiǎng)?chuàng)建類的實(shí)例,而不是類本身。這是因?yàn)?,lexer 的方法只有被綁定(bound-methods)對(duì)象后才能使 PLY 正常工作。

當(dāng)給 lex() 方法使用 module 選項(xiàng)時(shí),PLY 使用dir()方法,從對(duì)象中獲取符號(hào)信息,因?yàn)椴荒苤苯釉L問對(duì)象的__dict__屬性。(譯者注:可能是因?yàn)榧嫒菪栽颍?strong>dict這個(gè)方法可能不存在)

最后,如果你希望保持較好的封裝性,但不希望什么東西都寫在類里面,lexers 可以在閉包中定義,例如:

import ply.lex as lex

# List of token names.   This is always required
tokens = (
  'NUMBER',
  'PLUS',
  'MINUS',
  'TIMES',
  'DIVIDE',
  'LPAREN',
  'RPAREN',
)

def MyLexer():
    # Regular expression rules for simple tokens
    t_PLUS    = r'\+'
    t_MINUS   = r'-'
    t_TIMES   = r'\*'
    t_DIVIDE  = r'/'
    t_LPAREN  = r'\('
    t_RPAREN  = r'\)'

    # A regular expression rule with some action code
    def t_NUMBER(t):
        r'\d+'
        t.value = int(t.value)    
        return t

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

    # A string containing ignored characters (spaces and tabs)
    t_ignore  = ' \t'

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

    # Build the lexer from my environment and return it    
    return lex.lex()

額外狀態(tài)維護(hù)

在你的詞法分析器中,你可能想要維護(hù)一些狀態(tài)。這可能包括模式設(shè)置,符號(hào)表和其他細(xì)節(jié)。例如,假設(shè)你想要跟蹤NUMBER標(biāo)記的出現(xiàn)個(gè)數(shù)。

一種方法是維護(hù)一個(gè)全局變量:

num_count = 0
def t_NUMBER(t):
    r'\d+'
    global num_count
    num_count += 1
    t.value = int(t.value)    
    return t

如果你不喜歡全局變量,另一個(gè)記錄信息的地方是 lexer 對(duì)象內(nèi)部??梢酝ㄟ^當(dāng)前標(biāo)記的 lexer 屬性訪問:

def t_NUMBER(t):
    r'\d+'
    t.lexer.num_count += 1     # Note use of lexer attribute
    t.value = int(t.value)    
    return t

lexer = lex.lex()
lexer.num_count = 0            # Set the initial count

上面這樣做的優(yōu)點(diǎn)是當(dāng)同時(shí)存在多個(gè) lexer 實(shí)例的情況下,簡(jiǎn)單易行。不過這看上去似乎是嚴(yán)重違反了面向?qū)ο蟮姆庋b原則。lexer 的內(nèi)部屬性(除了 lineno )都是以 lex 開頭命名的(lexdata、lexpos)。因此,只要不以 lex 開頭來命名屬性就很安全的。

如果你不喜歡給 lexer 對(duì)象賦值,你可以自定義你的 lexer 類型,就像前面看到的那樣:

class MyLexer:
    ...
    def t_NUMBER(self,t):
        r'\d+'
        self.num_count += 1
        t.value = int(t.value)    
        return t

    def build(self, **kwargs):
        self.lexer = lex.lex(object=self,**kwargs)

    def __init__(self):
        self.num_count = 0

如果你的應(yīng)用會(huì)創(chuàng)建很多 lexer 的實(shí)例,并且需要維護(hù)很多狀態(tài),上面的類可能是最容易管理的。

狀態(tài)也可以用閉包來管理,比如,在 Python3 中:

def MyLexer():
    num_count = 0
    ...
    def t_NUMBER(t):
        r'\d+'
        nonlocal num_count
        num_count += 1
        t.value = int(t.value)    
        return t
    ...

Lexer 克隆

如果有必要的話,lexer 對(duì)象可以通過clone()方法來復(fù)制:

lexer = lex.lex()
...
newlexer = lexer.clone()

當(dāng) lexer 被克隆后,復(fù)制品能夠精確的保留輸入串和內(nèi)部狀態(tài),不過,新的 lexer 可以接受一個(gè)不同的輸出字串,并獨(dú)立運(yùn)作起來。這在幾種情況下也許有用:當(dāng)你在編寫的解析器或編譯器涉及到遞歸或者回退處理時(shí),你需要掃描先前的部分,你可以clone并使用復(fù)制品,或者你在實(shí)現(xiàn)某種預(yù)編譯處理,可以 clone 一些 lexer 來處理不同的輸入文件。

創(chuàng)建克隆跟重新調(diào)用 lex.lex() 的不同點(diǎn)在于,PLY 不會(huì)重新構(gòu)建任何的內(nèi)部分析表或者正則式。當(dāng) lexer 是用類或者閉包創(chuàng)建的,需要注意類或閉包本身的的狀態(tài)。換句話說你要注意新創(chuàng)建的 lexer 會(huì)共享原始 lexer 的這些狀態(tài),比如:

m = MyLexer()
a = lex.lex(object=m)      # Create a lexer

b = a.clone()              # Clone the lexer

Lexer 的內(nèi)部狀態(tài)

lexer 有一些內(nèi)部屬性在特定情況下有用:

  • lexer.lexpos。這是一個(gè)表示當(dāng)前分析點(diǎn)的位置的整型值。如果你修改這個(gè)值的話,這會(huì)改變下一個(gè) token() 的調(diào)用行為。在標(biāo)記的規(guī)則方法里面,這個(gè)值表示緊跟匹配字串后面的第一個(gè)字符的位置,如果這個(gè)值在規(guī)則中修改,下一個(gè)返回的標(biāo)記將從新的位置開始匹配
  • lexer.lineno。表示當(dāng)前行號(hào)。PLY 只是聲明這個(gè)屬性的存在,卻永遠(yuǎn)不更新這個(gè)值。如果你想要跟蹤行號(hào)的話,你需要自己添加代碼( 4.6 行號(hào)和位置信息)
  • lexer.lexdata。當(dāng)前 lexer 的輸入字串,這個(gè)字符串就是 input() 方法的輸入字串,更改它可能是個(gè)糟糕的做法,除非你知道自己在干什么。
  • lexer.lexmatch。PLY 內(nèi)部調(diào)用 Python 的 re.match() 方法得到的當(dāng)前標(biāo)記的原始的 Match 對(duì)象,該對(duì)象被保存在這個(gè)屬性中。如果你的正則式中包含分組的話,你可以通過這個(gè)對(duì)象獲得這些分組的值。注意:這個(gè)屬性只在有標(biāo)記規(guī)則定義的方法中才有效。

基于條件的掃描和啟動(dòng)條件

在高級(jí)的分析器應(yīng)用程序中,使用狀態(tài)化的詞法掃描是很有用的。比如,你想在出現(xiàn)特定標(biāo)記或句子結(jié)構(gòu)的時(shí)候觸發(fā)開始一個(gè)不同的詞法分析邏輯。PLY 允許 lexer 在不同的狀態(tài)之間轉(zhuǎn)換。每個(gè)狀態(tài)可以包含一些自己獨(dú)特的標(biāo)記和規(guī)則等。這是基于 GNU flex 的“啟動(dòng)條件”來實(shí)現(xiàn)的,關(guān)于 flex 詳見 http://flex.sourceforge.net/manual/Start-Conditions.html#Start-Conditions

要使用 lex 的狀態(tài),你必須首先聲明。通過在 lex 模塊中聲明"states"來做到:

states = (
   ('foo','exclusive'),
   ('bar','inclusive'),
)

這個(gè)聲明中包含有兩個(gè)狀態(tài):'foo'和'bar'。狀態(tài)可以有兩種類型:'排他型'和'包容型'。排他型的狀態(tài)會(huì)使得 lexer 的行為發(fā)生完全的改變:只有能夠匹配在這個(gè)狀態(tài)下定義的規(guī)則的標(biāo)記才會(huì)返回;包容型狀態(tài)會(huì)將定義在這個(gè)狀態(tài)下的規(guī)則添加到默認(rèn)的規(guī)則集中,進(jìn)而,只要能匹配這個(gè)規(guī)則集的標(biāo)記都會(huì)返回。

一旦聲明好之后,標(biāo)記規(guī)則的命名需要包含狀態(tài)名:

t_foo_NUMBER = r'\d+'                      # Token 'NUMBER' in state 'foo'        
t_bar_ID     = r'[a-zA-Z_][a-zA-Z0-9_]*'   # Token 'ID' in state 'bar'

def t_foo_newline(t):
    r'\n'
    t.lexer.lineno += 1

一個(gè)標(biāo)記可以用在多個(gè)狀態(tài)中,只要將多個(gè)狀態(tài)名包含在聲明中:

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

同樣的,在任何狀態(tài)下都生效的聲明可以在命名中使用ANY

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

不包含狀態(tài)名的情況下,標(biāo)記被關(guān)聯(lián)到一個(gè)特殊的狀態(tài)INITIAL,比如,下面兩個(gè)聲明是等價(jià)的:

t_NUMBER = r'\d+'
t_INITIAL_NUMBER = r'\d+'

特殊的t_ignore()t_error()也可以用狀態(tài)關(guān)聯(lián):

t_foo_ignore = " \t\n"       # Ignored characters for state 'foo'

def t_bar_error(t):          # Special error handler for state 'bar'
    pass 

詞法分析默認(rèn)在INITIAL狀態(tài)下工作,這個(gè)狀態(tài)下包含了所有默認(rèn)的標(biāo)記規(guī)則定義。對(duì)于不希望使用“狀態(tài)”的用戶來說,這是完全透明的。在分析過程中,如果你想要改變?cè)~法分析器的這種的狀態(tài),使用begin()方法:

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

使用 begin() 切換回初始狀態(tài):

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

狀態(tài)的切換可以使用棧:

def t_begin_foo(t):
    r'start_foo'
    t.lexer.push_state('foo')             # Starts 'foo' state

def t_foo_end(t):
    r'end_foo'
    t.lexer.pop_state()                   # Back to the previous state

當(dāng)你在面臨很多狀態(tài)可以選擇進(jìn)入,而又僅僅想要回到之前的狀態(tài)時(shí),狀態(tài)棧比較有用。

舉個(gè)例子會(huì)更清晰。假設(shè)你在寫一個(gè)分析器想要從一堆 C 代碼中獲取任意匹配的閉合的大括號(hào)里面的部分:這意味著,當(dāng)遇到起始括號(hào)'{',你需要讀取與之匹配的'}'以上的所有部分。并返回字符串。使用通常的正則表達(dá)式幾乎不可能,這是因?yàn)榇罄ㄌ?hào)可以嵌套,而且可以有注釋,字符串等干擾。因此,試圖簡(jiǎn)單的匹配第一個(gè)出現(xiàn)的'}'是不行的。這里你可以用lex的狀態(tài)來做到:

# Declare the state
states = (
  ('ccode','exclusive'),
)

# Match the first {. Enter ccode state.
def t_ccode(t):
    r'\{'
    t.lexer.code_start = t.lexer.lexpos        # Record the starting position
    t.lexer.level = 1                          # Initial brace level
    t.lexer.begin('ccode')                     # Enter 'ccode' state

# Rules for the ccode state
def t_ccode_lbrace(t):     
    r'\{'
    t.lexer.level +=1                

def t_ccode_rbrace(t):
    r'\}'
    t.lexer.level -=1

    # If closing brace, return the code fragment
    if t.lexer.level == 0:
         t.value = t.lexer.lexdata[t.lexer.code_start:t.lexer.lexpos+1]
         t.type = "CCODE"
         t.lexer.lineno += t.value.count('\n')
         t.lexer.begin('INITIAL')           
         return t

# C or C++ comment (ignore)    
def t_ccode_comment(t):
    r'(/\*(.|\n)*?*/)|(//.*)'
    pass

# C string
def t_ccode_string(t):
   r'\"([^\\\n]|(\\.))*?\"'

# C character literal
def t_ccode_char(t):
   r'\'([^\\\n]|(\\.))*?\''

# Any sequence of non-whitespace characters (not braces, strings)
def t_ccode_nonspace(t):
   r'[^\s\{\}\'\"]+'

# Ignored characters (whitespace)
t_ccode_ignore = " \t\n"

# For bad characters, we just skip over it
def t_ccode_error(t):
    t.lexer.skip(1)

這個(gè)例子中,第一個(gè)'{'使得 lexer 記錄了起始位置,并且進(jìn)入新的狀態(tài)'ccode'。一系列規(guī)則用來匹配接下來的輸入,這些規(guī)則只是丟棄掉標(biāo)記(不返回值),如果遇到閉合右括號(hào),t_ccode_rbrace 規(guī)則收集其中所有的代碼(利用先前記錄的開始位置),并保存,返回的標(biāo)記類型為'CCODE',與此同時(shí),詞法分析的狀態(tài)退回到初始狀態(tài)。

其他問題

  • lexer 需要輸入的是一個(gè)字符串。好在大多數(shù)機(jī)器都有足夠的內(nèi)存,這很少導(dǎo)致性能的問題。這意味著,lexer 現(xiàn)在還不能用來處理文件流或者 socket 流。這主要是受到 re 模塊的限制。
  • lexer 支持用 Unicode 字符描述標(biāo)記的匹配規(guī)則,也支持輸入字串包含 Unicode
  • 如果你想要向re.compile()方法提供 flag,使用 reflags 選項(xiàng):lex.lex(reflags=re.UNICODE)
  • 由于 lexer 是全部用 Python 寫的,性能很大程度上取決于 Python 的 re 模塊,即使已經(jīng)盡可能的高效了。當(dāng)接收極其大量的輸入文件時(shí)表現(xiàn)并不盡人意。如果擔(dān)憂性能,你可以升級(jí)到最新的 Python,或者手工創(chuàng)建分析器,或者用 C 語言寫 lexer 并做成擴(kuò)展模塊。

如果你要?jiǎng)?chuàng)建一個(gè)手寫的詞法分析器并計(jì)劃用在 yacc.py 中,只需要滿足下面的要求:

  • 需要提供一個(gè) token() 方法來返回下一個(gè)標(biāo)記,如果沒有可用的標(biāo)記了,則返回 None。
  • token() 方法必須返回一個(gè) tok 對(duì)象,具有 type 和 valu e屬性。如果行號(hào)需要跟蹤的話,標(biāo)記還需要定義 lineno 屬性。
上一篇:語法分析基礎(chǔ)下一篇:序言