Сохраняйте пустые строки с помощью 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
было достаточно)