Como posso dividir um texto em frases?

108

Eu tenho um arquivo de texto. Preciso obter uma lista de frases.

Como isso pode ser implementado? Existem muitas sutilezas, como um ponto sendo usado em abreviações.

Minha antiga expressão regular funciona mal:

re.compile('(\. |^|!|\?)([A-Z][^;↑\.<>@\^&/\[\]]*(\.|!|\?) )',re.M)
Artyom
fonte
18
Defina "frase".
martineau de
Eu quero fazer isso, mas quero dividir onde quer que haja um ponto ou uma nova linha
yishairasowsky

Respostas:

152

O Natural Language Toolkit ( nltk.org ) tem o que você precisa. Esta postagem em grupo indica que isso acontece:

import nltk.data

tokenizer = nltk.data.load('tokenizers/punkt/english.pickle')
fp = open("test.txt")
data = fp.read()
print '\n-----\n'.join(tokenizer.tokenize(data))

(Eu não tentei!)

Ned Batchelder
fonte
3
@Artyom: Provavelmente pode funcionar com russo - veja se o NLTK / pyNLTK funciona “por idioma” (ou seja, não inglês) e como? .
martineau
4
@Artyom: Este é o link direto para a documentação online do nltk .tokenize.punkt.PunktSentenceTokenizer.
martineau
10
Você pode ter que executar nltk.download()primeiro e baixar os modelos ->punkt
Martin Thoma
2
Isso falha em casos com aspas finais. Se tivermos uma frase que termina assim.
Fosa
1
Ok, você me convenceu. Mas acabei de testar e não parece falhar. Minha entrada é 'This fails on cases with ending quotation marks. If we have a sentence that ends like "this." This is another sentence.'e minha saída ['This fails on cases with ending quotation marks.', 'If we have a sentence that ends like "this."', 'This is another sentence.']parece correta para mim.
szedjani
100

Esta função pode dividir todo o texto de Huckleberry Finn em frases em cerca de 0,1 segundos e lida com muitos dos casos extremos mais dolorosos que tornam a análise de frases não trivial, por exemplo, "O Sr. John Johnson Jr. nasceu nos EUA, mas obteve seu Ph. D. em Israel antes de ingressar na Nike Inc. como engenheiro. Ele também trabalhou no craigslist.org como analista de negócios. "

# -*- coding: utf-8 -*-
import re
alphabets= "([A-Za-z])"
prefixes = "(Mr|St|Mrs|Ms|Dr)[.]"
suffixes = "(Inc|Ltd|Jr|Sr|Co)"
starters = "(Mr|Mrs|Ms|Dr|He\s|She\s|It\s|They\s|Their\s|Our\s|We\s|But\s|However\s|That\s|This\s|Wherever)"
acronyms = "([A-Z][.][A-Z][.](?:[A-Z][.])?)"
websites = "[.](com|net|org|io|gov)"

def split_into_sentences(text):
    text = " " + text + "  "
    text = text.replace("\n"," ")
    text = re.sub(prefixes,"\\1<prd>",text)
    text = re.sub(websites,"<prd>\\1",text)
    if "Ph.D" in text: text = text.replace("Ph.D.","Ph<prd>D<prd>")
    text = re.sub("\s" + alphabets + "[.] "," \\1<prd> ",text)
    text = re.sub(acronyms+" "+starters,"\\1<stop> \\2",text)
    text = re.sub(alphabets + "[.]" + alphabets + "[.]" + alphabets + "[.]","\\1<prd>\\2<prd>\\3<prd>",text)
    text = re.sub(alphabets + "[.]" + alphabets + "[.]","\\1<prd>\\2<prd>",text)
    text = re.sub(" "+suffixes+"[.] "+starters," \\1<stop> \\2",text)
    text = re.sub(" "+suffixes+"[.]"," \\1<prd>",text)
    text = re.sub(" " + alphabets + "[.]"," \\1<prd>",text)
    if "”" in text: text = text.replace(".”","”.")
    if "\"" in text: text = text.replace(".\"","\".")
    if "!" in text: text = text.replace("!\"","\"!")
    if "?" in text: text = text.replace("?\"","\"?")
    text = text.replace(".",".<stop>")
    text = text.replace("?","?<stop>")
    text = text.replace("!","!<stop>")
    text = text.replace("<prd>",".")
    sentences = text.split("<stop>")
    sentences = sentences[:-1]
    sentences = [s.strip() for s in sentences]
    return sentences
D Greenberg
fonte
19
Esta é uma solução incrível. No entanto, adicionei mais duas linhas a ele digits = "([0-9])" na declaração de expressões regulares e text = re.sub (digits + "[.]" + Digits, "\\ 1 <prd> \ \ 2 ", texto) na função. Agora ele não divide a linha em decimais, como 5.5. Obrigado por esta resposta.
Ameya Kulkarni
1
Como você analisou o Huckleberry Fin inteiro? Onde está isso em formato de texto?
PascalVKooten
6
Uma ótima solução. Na função, adicionei if "eg" no texto: text = text.replace ("eg", "e <prd> g <prd>") if "ie" no texto: text = text.replace ("ie" , "i <prd> e <prd>") e resolveu totalmente o meu problema.
Sisay Chala
3
Ótima solução com comentários muito úteis! Apenas para torná-lo um embora pouco mais robusto: prefixes = "(Mr|St|Mrs|Ms|Dr|Prof|Capt|Cpt|Lt|Mt)[.]", websites = "[.](com|net|org|io|gov|me|edu)"eif "..." in text: text = text.replace("...","<prd><prd><prd>")
Dascienz
1
Essa função pode ser feita para ver frases como esta como uma frase: Quando uma criança pergunta à mãe "De onde vêm os bebês?", O que se deve responder a ela?
twhale
50

Em vez de usar regex para dividir o texto em frases, você também pode usar a biblioteca nltk.

>>> from nltk import tokenize
>>> p = "Good morning Dr. Adams. The patient is waiting for you in room number 3."

>>> tokenize.sent_tokenize(p)
['Good morning Dr. Adams.', 'The patient is waiting for you in room number 3.']

ref: https://stackoverflow.com/a/9474645/2877052

Hassan Raza
fonte
Ótimo exemplo, mais simples e reutilizável do que a resposta aceita.
Jay D.
Se você remover um espaço após um ponto, tokenize.sent_tokenize () não funciona, mas tokenizer.tokenize () funciona! Hmm ...
Leonid Ganeline
1
for sentence in tokenize.sent_tokenize(text): print(sentence)
Victoria Stuart
11

Você pode tentar usar Spacy em vez de regex. Eu uso e faz o trabalho.

import spacy
nlp = spacy.load('en')

text = '''Your text here'''
tokens = nlp(text)

for sent in tokens.sents:
    print(sent.string.strip())
Duende
fonte
1
O espaço é mega grande. mas se você só precisa separar em sentenças, passar o texto para o espaço demorará muito se você estiver lidando com um tubo de dados
Berlines
@Berlines Eu concordo, mas não consegui encontrar nenhuma outra biblioteca que faça o trabalho tão limpo quanto spaCy. Mas se você tiver alguma sugestão, posso tentar.
Elf de
Também para os usuários do AWS Lambda Serverless, os arquivos de dados de suporte do spacy têm muitos 100 MB (o tamanho do inglês é> 400 MB), então você não pode usar coisas assim fora da caixa, infelizmente (grande fã do Spacy aqui)
Julian H
9

Aqui está uma abordagem intermediária que não depende de nenhuma biblioteca externa. Eu uso a compreensão de lista para excluir sobreposições entre abreviações e terminadores, bem como para excluir sobreposições entre variações em terminações, por exemplo: '.' vs. '. "'

abbreviations = {'dr.': 'doctor', 'mr.': 'mister', 'bro.': 'brother', 'bro': 'brother', 'mrs.': 'mistress', 'ms.': 'miss', 'jr.': 'junior', 'sr.': 'senior',
                 'i.e.': 'for example', 'e.g.': 'for example', 'vs.': 'versus'}
terminators = ['.', '!', '?']
wrappers = ['"', "'", ')', ']', '}']


def find_sentences(paragraph):
   end = True
   sentences = []
   while end > -1:
       end = find_sentence_end(paragraph)
       if end > -1:
           sentences.append(paragraph[end:].strip())
           paragraph = paragraph[:end]
   sentences.append(paragraph)
   sentences.reverse()
   return sentences


def find_sentence_end(paragraph):
    [possible_endings, contraction_locations] = [[], []]
    contractions = abbreviations.keys()
    sentence_terminators = terminators + [terminator + wrapper for wrapper in wrappers for terminator in terminators]
    for sentence_terminator in sentence_terminators:
        t_indices = list(find_all(paragraph, sentence_terminator))
        possible_endings.extend(([] if not len(t_indices) else [[i, len(sentence_terminator)] for i in t_indices]))
    for contraction in contractions:
        c_indices = list(find_all(paragraph, contraction))
        contraction_locations.extend(([] if not len(c_indices) else [i + len(contraction) for i in c_indices]))
    possible_endings = [pe for pe in possible_endings if pe[0] + pe[1] not in contraction_locations]
    if len(paragraph) in [pe[0] + pe[1] for pe in possible_endings]:
        max_end_start = max([pe[0] for pe in possible_endings])
        possible_endings = [pe for pe in possible_endings if pe[0] != max_end_start]
    possible_endings = [pe[0] + pe[1] for pe in possible_endings if sum(pe) > len(paragraph) or (sum(pe) < len(paragraph) and paragraph[sum(pe)] == ' ')]
    end = (-1 if not len(possible_endings) else max(possible_endings))
    return end


def find_all(a_str, sub):
    start = 0
    while True:
        start = a_str.find(sub, start)
        if start == -1:
            return
        yield start
        start += len(sub)

Usei a função find_all de Karl a partir desta entrada: Encontre todas as ocorrências de uma substring em Python

TennisVisuals
fonte
1
Abordagem perfeita! Os outros não pegam ...e ?!.
Shane Smiskol
6

Para casos simples (onde as sentenças são encerradas normalmente), isso deve funcionar:

import re
text = ''.join(open('somefile.txt').readlines())
sentences = re.split(r' *[\.\?!][\'"\)\]]* *', text)

O regex é *\. + , que corresponde a um ponto cercado por 0 ou mais espaços à esquerda e 1 ou mais à direita (para evitar que algo como o ponto em re.split seja contado como uma mudança na frase).

Obviamente, não é a solução mais robusta, mas funcionará bem na maioria dos casos. O único caso que isso não cobrirá são as abreviações (talvez percorra a lista de frases e verifique se cada string sentencescomeça com uma letra maiúscula?)

Rafe Kettler
fonte
29
Você não consegue pensar em uma situação em inglês onde uma frase não termine com um ponto final? Imagine isso! Minha resposta a isso seria: "pense novamente". (Viu o que eu fiz lá?)
Ned Batchelder
@Ned uau, não acredito que fui tão estúpido. Devo estar bêbado ou algo assim.
Rafe Kettler
Estou usando o Python 2.7.2 no Win 7 x86 e o ​​regex no código acima me dá este erro SyntaxError: EOL while scanning string literal:, apontando para o parêntese de fechamento (depois text). Além disso, a regex que você faz referência em seu texto não existe em seu exemplo de código.
Sabuncu
1
A regex não está completamente correta, como deveria estarr' *[\.\?!][\'"\)\]]* +'
fsociety,
Isso pode causar muitos problemas e fragmentar uma frase em partes menores também. Considere o caso em que temos "Paguei $ 3,5 por este sorvete", os pedaços são "Paguei $ 3" ​​e "5 por este sorvete". use a frase nltk padrão.tokenizer é mais seguro!
Reihan_amn
6

Você também pode usar a função de tokenização de frase em NLTK:

from nltk.tokenize import sent_tokenize
sentence = "As the most quoted English writer Shakespeare has more than his share of famous quotes.  Some Shakespare famous quotes are known for their beauty, some for their everyday truths and some for their wisdom. We often talk about Shakespeare’s quotes as things the wise Bard is saying to us but, we should remember that some of his wisest words are spoken by his biggest fools. For example, both ‘neither a borrower nor a lender be,’ and ‘to thine own self be true’ are from the foolish, garrulous and quite disreputable Polonius in Hamlet."

sent_tokenize(sentence)
Amiref
fonte
2

@Artyom,

Oi! Você pode fazer um novo tokenizer para russo (e alguns outros idiomas) usando esta função:

def russianTokenizer(text):
    result = text
    result = result.replace('.', ' . ')
    result = result.replace(' .  .  . ', ' ... ')
    result = result.replace(',', ' , ')
    result = result.replace(':', ' : ')
    result = result.replace(';', ' ; ')
    result = result.replace('!', ' ! ')
    result = result.replace('?', ' ? ')
    result = result.replace('\"', ' \" ')
    result = result.replace('\'', ' \' ')
    result = result.replace('(', ' ( ')
    result = result.replace(')', ' ) ') 
    result = result.replace('  ', ' ')
    result = result.replace('  ', ' ')
    result = result.replace('  ', ' ')
    result = result.replace('  ', ' ')
    result = result.strip()
    result = result.split(' ')
    return result

e então chame desta forma:

text = 'вы выполняете поиск, используя Google SSL;'
tokens = russianTokenizer(text)

Boa sorte, Mara.

Marilena Di Bari
fonte
0

Sem dúvida, o NLTK é o mais adequado para esse fim. Mas começar com NLTK é muito doloroso (mas depois de instalá-lo - você colhe os frutos)

Portanto, aqui está o código baseado em revisão simples disponível em http://pythonicprose.blogspot.com/2009/09/python-split-paragraph-into-sentences.html

# split up a paragraph into sentences
# using regular expressions


def splitParagraphIntoSentences(paragraph):
    ''' break a paragraph into sentences
        and return a list '''
    import re
    # to split by multile characters

    #   regular expressions are easiest (and fastest)
    sentenceEnders = re.compile('[.!?]')
    sentenceList = sentenceEnders.split(paragraph)
    return sentenceList


if __name__ == '__main__':
    p = """This is a sentence.  This is an excited sentence! And do you think this is a question?"""

    sentences = splitParagraphIntoSentences(p)
    for s in sentences:
        print s.strip()

#output:
#   This is a sentence
#   This is an excited sentence

#   And do you think this is a question 
Vaichidrewar
fonte
3
Sim, mas isso falha tão facilmente, com: "O Sr. Smith sabe que isso é uma frase."
thomas de
0

Tive que ler arquivos de legendas e dividi-los em frases. Após o pré-processamento (como remover informações de tempo, etc. nos arquivos .srt), a variável fullFile continha o texto completo do arquivo de legenda. A forma grosseira abaixo dividi-los nitidamente em frases. Provavelmente tive sorte porque as frases sempre terminavam (corretamente) com um espaço. Experimente primeiro e, se houver exceções, adicione mais verificações e saldos.

# Very approximate way to split the text into sentences - Break after ? . and !
fullFile = re.sub("(\!|\?|\.) ","\\1<BRK>",fullFile)
sentences = fullFile.split("<BRK>");
sentFile = open("./sentences.out", "w+");
for line in sentences:
    sentFile.write (line);
    sentFile.write ("\n");
sentFile.close;

Oh! bem. Agora percebo que, como meu conteúdo era espanhol, não tive problemas em lidar com o "Sr. Smith" etc. Ainda assim, se alguém quiser um analisador rápido e sujo ...

Kishore
fonte
0

Espero que isso ajude você no texto em latim, chinês, árabe

import re

punctuation = re.compile(r"([^\d+])(\.|!|\?|;|\n|。|!|?|;|…| |!|؟|؛)+")
lines = []

with open('myData.txt','r',encoding="utf-8") as myFile:
    lines = punctuation.sub(r"\1\2<pad>", myFile.read())
    lines = [line.strip() for line in lines.split("<pad>") if line.strip()]
mamtimen
fonte
0

Estava trabalhando em uma tarefa semelhante e me deparei com essa consulta, seguindo alguns links e trabalhando em alguns exercícios para nltk o código abaixo funcionou para mim como mágica.

from nltk.tokenize import sent_tokenize 
  
text = "Hello everyone. Welcome to GeeksforGeeks. You are studying NLP article"
sent_tokenize(text) 

resultado:

['Hello everyone.',
 'Welcome to GeeksforGeeks.',
 'You are studying NLP article']

Fonte: https://www.geeksforgeeks.org/nlp-how-tokenizing-text-sentence-words-works/

Mazeen Muhammed
fonte