2.18 字符串令牌解�¶

问题¶

你有一个字符串,想从左至�将其解�为一个令牌�。

解决方案¶

�如你有下�这样一个文本字符串:

text = 'foo = 23 + 42 * 10'

为了令牌化字符串,你�仅需�匹�模�,还得指定模�的类型。 比如,你�能想将字符串�下�这样转�为�列对:

tokens = [('NAME', 'foo'), ('EQ','='), ('NUM', '23'), ('PLUS','+'),
          ('NUM', '42'), ('TIMES', '*'), ('NUM', '10')]

为了执行这样的切分,第一步就是�下�这样利用命��获组的正则表达��定义所有�能的令牌,包括空格:

import re
NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'
NUM = r'(?P<NUM>\d+)'
PLUS = r'(?P<PLUS>\+)'
TIMES = r'(?P<TIMES>\*)'
EQ = r'(?P<EQ>=)'
WS = r'(?P<WS>\s+)'

master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS]))

在上�的模�中, ?P<TOKENNAME> 用于给一个模�命�,供��使用。

下一步,为了令牌化,使用模�对象很少被人知�的 scanner() 方法。 这个方法会创建一个 scanner 对象, 在这个对象上�断的调用 match() 方法会一步步的扫�目标文本,�步一个匹�。 下�是演示一个 scanner 对象如何工作的交互�例�:

>>> scanner = master_pat.scanner('foo = 42')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('NAME', 'foo')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('WS', ' ')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('EQ', '=')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('WS', ' ')
>>> scanner.match()
<_sre.SRE_Match object at 0x100677738>
>>> _.lastgroup, _.group()
('NUM', '42')
>>> scanner.match()
>>>

实际使用这�技术的时候,�以很容易的�下�这样将上述代�打包到一个生�器中:

def generate_tokens(pat, text):
    Token = namedtuple('Token', ['type', 'value'])
    scanner = pat.scanner(text)
    for m in iter(scanner.match, None):
        yield Token(m.lastgroup, m.group())

# Example use
for tok in generate_tokens(master_pat, 'foo = 42'):
    print(tok)
# Produces output
# Token(type='NAME', value='foo')
# Token(type='WS', value=' ')
# Token(type='EQ', value='=')
# Token(type='WS', value=' ')
# Token(type='NUM', value='42')

如果你想过滤令牌�,你�以定义更多的生�器函数或者使用一个生�器表达�。 比如,下�演示怎样过滤所有的空白令牌:

tokens = (tok for tok in generate_tokens(master_pat, text)
          if tok.type != 'WS')
for tok in tokens:
    print(tok)

讨论¶

通常�讲令牌化是很多高级文本解�与处�的第一步。 为了使用上�的扫�方法,你需�记�这里一些��的几点。 第一点就是你必须确认你使用正则表达�指定了所有输入中�能出现的文本�列。 如果有任何��匹�的文本出现了,扫�就会直接�止。这也是为什么上�例�中必须指定空白字符令牌的原因。

令牌的顺�也是有影�的。 re 模�会按照指定好的顺�去�匹�。 因此,如果一个模��好是�一个更长模�的�字符串,那么你需�确定长模�写在��。比如:

LT = r'(?P<LT><)'
LE = r'(?P<LE><=)'
EQ = r'(?P<EQ>=)'

master_pat = re.compile('|'.join([LE, LT, EQ])) # Correct
# master_pat = re.compile('|'.join([LT, LE, EQ])) # Incorrect

第二个模�是错的,因为它会将文本<=匹�为令牌LT紧跟�EQ,而�是�独的令牌LE,这个并�是我们想�的结果。

最�,你需�留�下�字符串形�的模�。比如,�设你有如下两个模�:

PRINT = r'(?P<PRINT>print)'
NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'

master_pat = re.compile('|'.join([PRINT, NAME]))

for tok in generate_tokens(master_pat, 'printer'):
    print(tok)

# Outputs :
# Token(type='PRINT', value='print')
# Token(type='NAME', value='er')

关于更高阶的令牌化技术,你�能需�查看 PyParsing 或者 PLY 包。 一个调用PLY的例�在下一节会有演示。