Pythonic способ реализовать токенизатор - PullRequest
12 голосов
/ 27 марта 2009

Я собираюсь реализовать токенайзер в Python, и мне было интересно, не могли бы вы дать какой-нибудь совет по стилю?

Я уже реализовывал токенизатор в C и Java, поэтому я в порядке с теорией, я просто хотел бы убедиться, что я следую питонским стилям и лучшим практикам.

Типы списочных токенов:

Например, в Java у меня был бы список полей, например:

public static final int TOKEN_INTEGER = 0

Но, очевидно, нет способа (я думаю) объявить постоянную переменную в Python, поэтому я мог бы просто заменить это на обычные объявления переменных, но это не кажется мне хорошим решением, поскольку объявления могут быть изменены.

Возвращение токенов из токенизатора:

Есть ли лучшая альтернатива простому возвращению списка кортежей, например,

[ (TOKEN_INTEGER, 17), (TOKEN_STRING, "Sixteen")]?

Приветствия

Пит

Ответы [ 12 ]

56 голосов
/ 29 марта 2009

В модуле re есть недокументированный класс с именем re.Scanner. Использовать токенизатор очень просто:

import re
scanner=re.Scanner([
  (r"[0-9]+",       lambda scanner,token:("INTEGER", token)),
  (r"[a-z_]+",      lambda scanner,token:("IDENTIFIER", token)),
  (r"[,.]+",        lambda scanner,token:("PUNCTUATION", token)),
  (r"\s+", None), # None == skip token.
])

results, remainder=scanner.scan("45 pigeons, 23 cows, 11 spiders.")
print results

приведет к

[('INTEGER', '45'),
 ('IDENTIFIER', 'pigeons'),
 ('PUNCTUATION', ','),
 ('INTEGER', '23'),
 ('IDENTIFIER', 'cows'),
 ('PUNCTUATION', ','),
 ('INTEGER', '11'),
 ('IDENTIFIER', 'spiders'),
 ('PUNCTUATION', '.')]

Я использовал re.Scanner, чтобы написать довольно изящный синтаксический анализатор конфигурации / структурированных данных всего за пару сотен строк.

29 голосов
/ 27 марта 2009

Python применяет подход «мы все взрослые по обоюдному согласию» к сокрытию информации. Можно использовать переменные, как если бы они были константами, и полагать, что пользователи вашего кода не будут делать глупостей.

10 голосов
/ 27 марта 2009

Во многих ситуациях эксп. при анализе длинных входных потоков может оказаться более полезным реализовать ваш токенизатор в качестве функции генератора. Таким образом, вы можете легко перебрать все токены, не тратя много памяти, чтобы сначала создать список токенов.

Для генератора см. оригинальное предложение или другие онлайн документы

7 голосов
/ 28 марта 2009

Спасибо за вашу помощь, я начал собирать эти идеи и придумал следующее. Что-то ужасно не так с этой реализацией (особенно я обеспокоен передачей файлового объекта токенизатору):

class Tokenizer(object):

  def __init__(self,file):
     self.file = file

  def __get_next_character(self):
      return self.file.read(1)

  def __peek_next_character(self):
      character = self.file.read(1)
      self.file.seek(self.file.tell()-1,0)
      return character

  def __read_number(self):
      value = ""
      while self.__peek_next_character().isdigit():
          value += self.__get_next_character()
      return value

  def next_token(self):
      character = self.__peek_next_character()

      if character.isdigit():
          return self.__read_number()
4 голосов
/ 27 марта 2009

«Есть ли лучшая альтернатива простому возвращению списка кортежей?»

Нет. Это работает очень хорошо.

2 голосов
/ 28 марта 2009

Я недавно тоже создал токенизатор и решил некоторые ваши проблемы.

Типы токенов объявляются как «константы», то есть переменные с именами ALL_CAPS, на уровне модуля. Например,

_INTEGER = 0x0007
_FLOAT = 0x0008
_VARIABLE = 0x0009

и так далее. Я использовал подчеркивание перед именем, чтобы указать, что каким-то образом эти поля являются «частными» для модуля, но я действительно не знаю, является ли это типичным или желательным, даже не настолько, насколько Pythonic. (Кроме того, я, вероятно, буду отбрасывать числа в пользу строк, потому что во время отладки они намного более читабельны.)

Жетоны возвращаются как именованные кортежи.

from collections import namedtuple
Token = namedtuple('Token', ['value', 'type'])
# so that e.g. somewhere in a function/method I can write...
t = Token(n, _INTEGER)
# ...and return it properly

Я использовал именованные кортежи, потому что клиентский код токенизатора (например, анализатор) кажется немного более понятным при использовании имен (например, token.value) вместо индексов (например, token [0]).

Наконец, я заметил, что иногда, особенно при написании тестов, я предпочитаю передавать строку токенизатору вместо файлового объекта. Я называю это «читатель», и у меня есть специальный метод, чтобы открыть его и позволить токенизатору получить доступ к нему через тот же интерфейс.

def open_reader(self, source):
    """
    Produces a file object from source.
    The source can be either a file object already, or a string.
    """
    if hasattr(source, 'read'):
        return source
    else:
        from io import StringIO
        return StringIO(source)
2 голосов
/ 27 марта 2009

«Есть ли лучшая альтернатива простому возвращению списка кортежей?»

Этот подход используется модулем "tokenize" для анализа исходного кода Python. Возвращение простого списка кортежей может работать очень хорошо.

1 голос
/ 22 апреля 2013

Это поздний ответ, теперь в официальной документации есть что-то: Написание токенизатора со стандартной библиотекой re. Это содержимое документации по Python 3, которой нет в документации по Py 2.7. Но это все еще применимо к более старым Питонам.

Это включает в себя как краткий код, простую настройку, так и написание генератора, как предложено в нескольких ответах.

Если документы не Pythonic, я не знаю, что: -)

1 голос
/ 24 июля 2009

Я бы обратился к превосходному Обработка текста в Python Дэвида Мерца

1 голос
/ 24 июля 2009

Я реализовал токенизатор для C-подобного языка программирования. Что я сделал, так это разделил создание токенов на два слоя:

  • a сканер поверхности : этот фактически читает текст и использует регулярное выражение, чтобы разбить его на только самые простые токены (операторы, идентификаторы, числа, ...); этот возвращает кортежи (tokenname, scannedstring, startpos, endpos).
  • a tokenizer : он потребляет кортежи из первого слоя, превращая их в объекты токенов (я думаю, что названные кортежи тоже подойдут). Его цель состоит в том, чтобы обнаружить некоторые дальнодействующие зависимости в потоке токенов, в частности строки (с их открывающими и закрывающими кавычками) и комментарии (с их открывающими закрывающими лексемами; - да, я хотел сохранить комментарии!) И привести их в единый жетоны. Полученный поток объектов-токенов затем возвращается анализатору-потребителю.

Оба являются генераторами. Преимущества этого подхода были:

  • Чтение необработанного текста выполняется только самым примитивным способом, с простыми регулярными выражениями - быстро и чисто.
  • Второй уровень уже реализован как примитивный парсер, для обнаружения строковых литералов и комментариев - повторное использование технологии парсера.
  • Вам не нужно напрягать поверхностный сканер со сложными обнаружениями.
  • Но настоящий анализатор получает токены на семантическом уровне анализируемого языка (снова строки, комментарии).

Я чувствую себя довольно счастливым с этим многоуровневым подходом.

...