Написание токенизатора в Python

Я хочу создать пользовательский модуль tokenizer в Python, который позволяет пользователям указывать, какой токенизатор использовать для ввода. Например, рассмотрим следующий ввод:

В: Каков хороший способ достичь этого? Ответ: Я не уверен. я думаю я будет использовать Python.

Я хочу быть в состоянии предоставить токенинг предложений NLTK, sent_tokenize() как вариант, потому что он хорошо работает во многих ситуациях, и я не хочу, изобрести колесо. В дополнение к этому, я также хочу предоставить более мелкозернистый конструктор токенизации (что-то вроде строк правила). Позвольте мне объяснить:

Предположим, что я предоставляю пару токенизаторов:

SENTENCE # Tokenizes the given input by using sent_tokenize()
WORD # Tokenizes the given input by using word_tokenize()
QA # Tokenizes using a custom regular expression. E.g., Q: (.*?) A: (.*?)

Я хочу поддерживать правила следующим образом:

  • QA → SENTENCE: сначала применить токен-код QA, а затем токенизатор предложения
  • QA: применить только токенизатор QA

Следовательно, ожидаемый результат выглядит следующим образом:

1. QA → SENTENCE

[
  ('QUESTION', 
             ('SENTENCE', 'What is a good way to achieve this?'), 
  ),
  ('ANSWER', 
             ('SENTENCE', 'I am not so sure', 'I think I will use Python')
  )
]

2. QA

[
  ('QUESTION', 'What is a good way to achieve this?'),
  ('ANSWER', 'I am not so sure. I think I will use Python')
]

Какая хорошая конструкция для достижения этой эффективности?

Ответы

Ответ 1

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

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

import re

# some tokenizers
def tokzr_WORD(txt): return ('WORD', re.findall(r'(?ms)\W*(\w+)', txt))  # split words
def tokzr_SENT(txt): return ('SENTENCE', re.findall(r'(?ms)\s*(.*?(?:\.|\?|!))', txt))  # split sentences
def tokzr_QA(txt):
    l_qa = []
    for m in re.finditer(r'(?ms)^[\s#\-\*]*(?:Q|Question)\s*:\s*(?P<QUESTION>\S.*?\?)[\s#\-\*]+(?:A|Answer)\s*:\s*(?P<ANSWER>\S.*?)$', txt):  # split (Q, A) sequences
        for k in ['QUESTION', 'ANSWER']:
            l_qa.append(m.groupdict()[k])
    return ('QA', l_qa)
def tokzr_QA_non_canonical(txt):  # Note: not supported by tokenize_recursively() as not canonical.
    l_qa = []
    for m in re.finditer(r'(?ms)^[\s#\-\*]*(?:Q|Question)\s*:\s*(?P<QUESTION>\S.*?\?)[\s#\-\*]+(?:A|Answer)\s*:\s*(?P<ANSWER>\S.*?)$', txt):  # split (Q, A) sequences
        for k in ['QUESTION', 'ANSWER']:
            l_qa.append((k, m.groupdict()[k]))
    return l_qa

dict_tokzr = {  # control string: tokenizer function
    'WORD'    : tokzr_WORD,
    'SENTENCE': tokzr_SENT,
    'QA'      : tokzr_QA,
}

# the core function
def tokenize_recursively(l_tokzr, work_on, lev=0):
    if isinstance(work_on, basestring):
        ctrl, work_on = dict_tokzr[l_tokzr[0]](work_on)  # tokenize
    else:
        ctrl, work_on = work_on[0], work_on[1:]  # get right part
    ret = [ctrl]
    if len(l_tokzr) == 1:
        ret.append(work_on)  # add right part
    else:
        for wo in work_on:  # dive into tree
            t = tokenize_recursively(l_tokzr[1:], wo, lev + 1)
            ret.append(t)
    return ret

# just for printing
def nestedListLines(aList, ind='    ', d=0):
    """ Returns multi-line string representation of \param aList.  Use \param ind to indent per level. """
    sRet = '\n' + d * ind + '['
    nested = 0
    for i, e in enumerate(aList):
        if i:
            sRet += ', '
        if type(e) == type(aList):
            sRet += nestedListLines(e, ind, d + 1)
            nested = 1
        else:
            sRet += '\n' + (d + 1) * ind + repr(e) if nested else repr(e)
    sRet += '\n' + d * ind + ']' if nested else ']'
    return sRet

# main()
inp1 = """
    * Question: I want try something.  Should I?
    * Answer  : I'd assume so.  Give it a try.
"""
inp2 = inp1 + 'Q: What is a good way to achieve this?  A: I am not so sure. I think I will use Python.'
print repr(tokzr_WORD(inp1))
print repr(tokzr_SENT(inp1))
print repr(tokzr_QA(inp1))
print repr(tokzr_QA_non_canonical(inp1))  # Really this way?
print

for ctrl, inp in [  # example control sequences
    ('SENTENCE-WORD', inp1),
    ('QA-SENTENCE', inp2)
]:
    res = tokenize_recursively(ctrl.split('-'), inp)
    print nestedListLines(res)

Btw. Python/Lib/tokenize.py(для самого кода Python) может стоить того, как обрабатывать вещи.

Ответ 2

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

http://en.wikipedia.org/wiki/Finite-state_machine

Пример машины состояния, которая будет принимать предложение с пробелами и распечатать слова, конечно, вы могли бы сделать этот конкретный пример более легкими способами! Но с государственными машинами в целом вы получаете линейную производительность по времени и можете легко ее окупить!

while 1:
    if state == "start":
        if i == len(text):
            state = "end"
        elif text[i] == " ":
            state = "new word"
            i = i - 1
        else:
            word.append(text[i])
    elif state == "new word":
        print(''.join(word))
        del word[:]
        state = "start"
    elif state == "end":
        print(''.join(word))
        break
    i = i + 1

http://docs.python.org/2/library/collections.html#collections.Counter

Тогда вы можете, например, использовать эту структуру данных python для сохранения ваших токенов. Я думаю, что он идеально подходит для ваших нужд!

Надеюсь, что это была некоторая помощь.

Ответ 3

Я написал этот простой токенизатор сегодня, нужно поддерживать простой DSL, который имеет такие выражения, как:

  • a = a + b
  • funcCall(b,c)
  • a < b and b > c

и не поддерживать вложенность арифметики глубже, чем указано выше. Другими словами, самый простой язык, который позволяет указать как условные обозначения стрелок, так и коды ввода конечного автомата. Я пробовал более сложные решения, но не смог. Почему существует поддержка неопределенного рекурсивного выражения, если вы знаете, что существует 100% вероятность того, что она не будет использоваться? Точно так же я ограничиваю and на ребре до 5, в противном случае конечный автомат выглядит неравномерно. or обрабатывается самим автоматом: у стрелок, естественно, есть альтернативная логика или or.

Comment, Float, Int, CmpOp, ArithOp, ElemFunc, FuncCall, Ident, TokenTypes = range(9)

tokenRegex = {
   Comment: re.compile(r"/\*.*\*/"),
   Float: re.compile(r"[-+]?\d*\.\d+"),
   Int: re.compile(r"[-+]?\d+"),
   CmpOp: re.compile(r">=|<=|!=|==|<|>"),
   ArithOp: re.compile(r"\+|-|\*|%|/|>>|<<"),
   ElemFunc: re.compile("(sin|cos|tan|exp|pow|log|ln)\((.*)\)"),
   FuncCall: re.compile("(^[^\d\W]\w*\Z)\((.*)\)"),
   Ident: re.compile(r"^[^\d\W]\w*\Z")
}



def tokenize(text):
     inputs = [(text, 0)]            # index is where to insert into tokens rel end
     tokens = []

     while inputs:
        text_elem = inputs.pop(0)
        string = text_elem[0]
        index = text_elem[1]

        for tok_type, regex in tokenRegex.items():
            match = regex.search(string)
            if match:
                tokens.insert(len(tokens) - index, (match, tok_type))
                prefix = string[:match.start()]
                if prefix != '':  inputs.insert(index, (prefix, index + 1))
                suffix = string[match.end():]
                if suffix != '':  inputs.insert(index+1, (suffix, index))
                break

    return tokens

Это не полностью протестировано, но в 1 < 0 and 0 > 1 (да, инструкция, которая всегда ложна, я использую в качестве случайного набора значений по умолчанию текста стрелки, чтобы помочь им), токены были возвращены в правильном порядке.

Если они не возвращаются в правильном порядке, это связано с тем, как я обрабатываю var index, который должен быть там, где они должны быть вставлены в токены (относительно конца). Я попытался сохранить список токенов и список входов в том же списке, и это был большой беспорядок. Эта версия выглядит довольно прилично.

Я не уверен, что он будет отвечать вашим потребностям, но я ясно использую его, поэтому, возможно, другие там тоже будут.

Мое приложение имеет фиксированный набор типов токенов, но это может быть использовано для того, что вы хотите: код пользователя просто вставляет новый тип токена (порядок важен) вместе с регулярным выражением для него.