Сохраняйте пустые строки с помощью Punkt Tokenizer от NLTK

Я использую токенайзер предложений PUNKT от NLTK, чтобы разбить файл на список предложений, и хотел бы сохранить пустые строки в файле:

from nltk import data
tokenizer = data.load('tokenizers/punkt/english.pickle')
s = "That was a very loud beep.\n\n I don't even know\n if this is working. Mark?\n\n Mark are you there?\n\n\n"
sentences = tokenizer.tokenize(s)
print sentences

Я хотел бы это напечатать:

['That was a very loud beep.\n\n', "I don't even know\n if this is working.", 'Mark?\n\n', 'Mark are you there?\n\n\n']

Но фактически напечатанный контент показывает, что завершающие пустые строки были удалены из первого и третьего предложений:

['That was a very loud beep.', "I don't even know\n if this is working.", 'Mark?', 'Mark are you there?\n\n\n']

Другие токенизаторы в НЛТК имеют blanklines='keep' параметр, но я не вижу такой опции в случае токенайзера Punkt. Вполне возможно, что я упускаю что-то простое. Есть ли способ переобучить эти висячие пустые строки с помощью токенайзера предложений Punkt? Буду благодарен за любые идеи, которые могут предложить другие!

4 ответа

Решение

Эта проблема

К сожалению, вы не можете заставить токенизатор сохранять пустые строки, не так, как написано.

Начиная здесь и следуя вызовам функций через span_tokenize() и _slices_from_text(), вы можете видеть, что есть условие

if match.group('next_tok'):

это сделано для того, чтобы токенайзер пропускал пробелы до появления следующего возможного начального токена предложения. Ища регулярное выражение, к которому это относится, мы в конечном итоге смотрим на _period_context_fmt, где мы видим, что next_tok названной группе предшествует \s+ где пустые строки не будут захвачены.

Решение

Разбейте его, измените ту часть, которая вам не нравится, соберите собственное решение.

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

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

_period_context_fmt = r"""
    \S*                          # some word material
    %(SentEndChars)s             # a potential sentence ending
    (?=(?P<after_tok>
        %(NonWord)s              # either other punctuation
        |
        \s+(?P<next_tok>\S+)     # or whitespace and some other token
    ))"""

к этому:

_period_context_fmt = r"""
    \S*                          # some word material
    %(SentEndChars)s             # a potential sentence ending
    \s*                       #  <-- THIS is what I changed
    (?=(?P<after_tok>
        %(NonWord)s              # either other punctuation
        |
        (?P<next_tok>\S+)     #  <-- Normally you would have \s+ here
    ))"""

Теперь токенизатор, использующий это регулярное выражение вместо старого, будет содержать 0 или более \s символы после конца предложения.

Весь сценарий

import nltk.tokenize.punkt as pkt

class CustomLanguageVars(pkt.PunktLanguageVars):

    _period_context_fmt = r"""
        \S*                          # some word material
        %(SentEndChars)s             # a potential sentence ending
        \s*                       #  <-- THIS is what I changed
        (?=(?P<after_tok>
            %(NonWord)s              # either other punctuation
            |
            (?P<next_tok>\S+)     #  <-- Normally you would have \s+ here
        ))"""

custom_tknzr = pkt.PunktSentenceTokenizer(lang_vars=CustomLanguageVars())

s = "That was a very loud beep.\n\n I don't even know\n if this is working. Mark?\n\n Mark are you there?\n\n\n"

print(custom_tknzr.tokenize(s))

Это выводит:

['That was a very loud beep.\n\n ', "I don't even know\n if this is working. ", 'Mark?\n\n ', 'Mark are you there?\n\n\n']

Разбейте входные данные на абзацы, разделив их на регулярное выражение захвата (которое также возвращает захваченную строку):

paras = re.split("(\n\s*\n)", sentences)

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

sents_by_para = [ nltk.sent_tokenize(p) for p in paras ]
flat = [ sent for par in sents_by_para for sent in par ]

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

Если вы хотите добавить пробел к предыдущему предложению, вы можете легко вставить его обратно:

collapsed = []
for s in flat:
    if s.isspace() and len(collapsed) > 0:
        collapsed[-1] += s
    else:
        collapsed.append(s)

В итоге я соединил идеи как @alexis, так и @HugoMailhot, чтобы сохранить разрывы строк в тех случаях, когда в одном абзаце есть несколько предложений и / или разрывов строк:

import re, nltk, sys, codecs
import nltk.tokenize.punkt as pkt
from nltk import data

class CustomLanguageVars(pkt.PunktLanguageVars):

    _period_context_fmt = r"""
        \S*                          # some word material
        %(SentEndChars)s             # a potential sentence ending
        \s*                       #  <-- THIS is what I changed
        (?=(?P<after_tok>
            %(NonWord)s              # either other punctuation
            |
            (?P<next_tok>\S+)     #  <-- Normally you would have \s+ here
        ))"""

custom_tokenizer = pkt.PunktSentenceTokenizer(lang_vars=CustomLanguageVars())

def sentence_split(s):
        '''Read in a string and return a list of sentences with linebreaks intact'''
        paras = re.split("(\n\s*\n)", s)
        sents_by_para = [custom_tokenizer.tokenize(p) for p in paras ]
        flat = [ sent for par in sents_by_para for sent in par ]

        collapsed = []
        for s in flat:
            if s.isspace() and len(collapsed) > 0:
                collapsed[-1] += s
            else:
                collapsed.append(s)

        return collapsed

if __name__ == "__main__":
        s = codecs.open(sys.argv[1],'r','utf-8').read()
        sentences = sentence_split(s)

Я бы пошел с itertools.groupbyсм. Python: Как пройтись по блокам строк:

alvas@ubi:~$ echo """This is a foo bar sentence,
that is also a foo bar sentence.

But I don't like foobars.
Yes you do like bars with foos, no?


I'm not sure whether you like bar bar!
Neither do I like black sheep.""" > test.in



alvas@ubi:~$ python
>>> from nltk import sent_tokenize
>>> import itertools
>>> with open('test.in', 'r') as fin:
...     for key, group in itertools.groupby(fin, lambda x: x!='\n'):
...             if key:
...                     print list(group)
... 
['This is a foo bar sentence,\n', 'that is also a foo bar sentence.\n']
["But I don't like foobars.\n", 'Yes you do like bars with foos, no?\n']
["I'm not sure whether you like bar bar!\n", 'Neither do I like black sheep.\n']

И после этого, если вы хотите сделать sent_tokenize или другие модели пунктов в группе:

>>> with open('test.in', 'r') as fin:
...     for key, group in itertools.groupby(fin, lambda x: x!='\n'):
...             if key:
...                     paragraph = " ".join(line.strip() for line in group)
...                     print sent_tokenize(paragraph)
... 
['This is a foo bar sentence, that is also a foo bar sentence.']
["But I don't like foobars.", 'Yes you do like bars with foos, no?']
["I'm not sure whether you like bar bar!", 'Neither do I like black sheep.']

(Примечание: более вычислительно эффективный метод будет использовать mmapсм. /questions/45413434/python-kak-perebirat-bloki-strok/45413447#45413447. Но для размера, над которым я работаю (~20 миллионов токенов) itertools.groupby было достаточно)

Другие вопросы по тегам