На этом шаге мы рассмотрим разные способы решения этой задачи.
У вас есть строка, которую вы хотите распарсить в поток токенов слева направо.
Предположим, у вас есть вот такая строка:
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(). Этот метод создает объект сканера, в котором повторно вызывается шаг match() для предоставленного текста, выполняя один поиск совпадения за раз. Вот интерактивный сеанс работы объекта сканера:
>>> scanner = master_pat.scanner('foo = 42') >>> scanner.match() <re.Match object; span=(0, 3), match='foo'> >>> _.lastgroup, _.group() ('NAME', 'foo') >>> scanner.match() <re.Match object; span=(3, 4), match=' '> >>> _.lastgroup, _.group() ('WS', ' ') >>> scanner.match() <re.Match object; span=(4, 5), match='='> >>> _.lastgroup, _.group() ('EQ', '=') >>> scanner.match() <re.Match object; span=(5, 6), match=' '> >>> _.lastgroup, _.group() ('WS', ' ') >>> scanner.match() <re.Match object; span=(6, 8), match='42'> >>> _.lastgroup, _.group() ('NUM', '42') >>> scanner.match() >>>
Чтобы взять этот прием и использовать в программе, он должен быть очищен и упакован в генератор:
>>> from collections import namedtuple >>> Token = namedtuple('Token', ['type', 'value']) >>> def generate_tokens(pat, text): scanner = pat.scanner(text) for m in iter(scanner.match, None): yield Token(m.lastgroup, m.group()) >>> # Пример использования >>> for tok in generate_tokens(master_pat, 'foo = 42'): print(tok) 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, 'foo = 42') if tok.type != 'WS') >>> for tok in tokens: print(tok) Token(type='NAME', value='foo') Token(type='EQ', value='=') Token(type='NUM', value='42') >>>
Токенизация часто является первым шагом более продвинутого парсинга и обработки текста. Чтобы использовать показанные приемы сканирования, нужно держать в уме несколько важных моментов. Во-первых, вы должны убедиться, что вы определили соответствующие шаблоны регулярных выражений для всех возможных текстовых последовательностей, которые могут встретиться во входных данных. Если встретится текст, для которого нельзя найти совпадение, сканирование просто остановится. Вот почему необходимо было определить токен пробела (WS) в примере выше.
Порядок токенов в главном регулярном выражении также важен. При поиске совпадений регулярное выражение пытается отыскать совпадения с шаблонами в заданном порядке. Поэтому если шаблон окажется подстрокой более длинного шаблона, вы должны убедиться, что более длинный шаблон вписан в выражение первым. Например:
LT = r'(?P<LT><)' LE = r'(?P<LE><=)' EQ = r'(?P<EQ>=)' master_pat = re.compile('|'.join([LE, LT, EQ])) # Правильно master_pat = re.compile('|'.join([LT, LE, EQ])) # Неправильно
Второй шаблон неправильный, потому что он будет отыскивать совпадение с <=, поскольку за токеном LT следует токен EO, а не 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) # Выводит: # Token(type='PRINT', value='print') # Token(type='NAME', value='er')
Для более продвинутой токенизации вы можете обратиться к пакетам PyParsing (https://pypi.org/project/pyparsing/) или PLY (http://www.dabeaz.com/ply/index.html). Пример использования PLY вы найдете в следующем рецепте.
На следующем шаге мы рассмотрим написание простого парсера на основе метода рекурсивного спуска.