Jak mogę podzielić tekst na zdania?

108

Mam plik tekstowy. Potrzebuję listę zdań.

Jak można to wdrożyć? Istnieje wiele subtelności, takich jak kropka używana w skrótach.

Moje stare wyrażenie regularne działa źle:

re.compile('(\. |^|!|\?)([A-Z][^;↑\.<>@\^&/\[\]]*(\.|!|\?) )',re.M)
Artem
źródło
18
Zdefiniuj „zdanie”.
martineau
chcę to zrobić, ale chcę podzielić, gdziekolwiek jest kropka lub nowa linia
yishairasowsky

Odpowiedzi:

152

Natural Language Toolkit ( nltk.org ) ma to, czego potrzebujesz. Ten post grupowy wskazuje, że to robi:

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))

(Nie próbowałem tego!)

Ned Batchelder
źródło
3
@Artyom: Prawdopodobnie może działać z rosyjskim - zobacz, czy NLTK / pyNLTK działa „w każdym języku” (tj. Innym niż angielski) i jak? .
martineau
4
@Artyom: Oto bezpośredni link do dokumentacji online dla nltk .tokenize.punkt.PunktSentenceTokenizer.
martineau
10
Być może będziesz musiał nltk.download()najpierw uruchomić i pobrać modele ->punkt
Martin Thoma
2
Nie udaje się to w przypadkach z końcowymi cudzysłowami. Jeśli mamy zdanie, które kończy się jak „to”.
Fosa
1
Okej, przekonałeś mnie. Ale właśnie przetestowałem i nie wygląda na to, żeby mi się udało. Mój wkład jest, 'This fails on cases with ending quotation marks. If we have a sentence that ends like "this." This is another sentence.'a mój wynik ['This fails on cases with ending quotation marks.', 'If we have a sentence that ends like "this."', 'This is another sentence.']wydaje się być poprawny dla mnie.
szedjani
101

Ta funkcja może podzielić cały tekst Huckleberry Finn na zdania w około 0,1 sekundy i obsługuje wiele bardziej bolesnych skrajnych przypadków, które sprawiają, że analiza zdań jest nietrywialna, np. „ Pan John Johnson Jr. urodził się w USA, ale zdobył tytuł doktora. D. w Izraelu, zanim dołączył do Nike Inc. jako inżynier. Pracował również na craigslist.org jako analityk biznesowy ”.

# -*- 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
źródło
19
To świetne rozwiązanie. Jednak dodałem do niego jeszcze dwa wiersze digits = "([0-9])" w deklaracji wyrażeń regularnych i text = re.sub (cyfry + "[.]" + Cyfry, "\\ 1 <prd> \ \ 2 ", tekst) w funkcji. Teraz nie dzieli linii w miejscach po przecinku, takich jak 5,5. Dziękuję za tę odpowiedź.
Ameya Kulkarni
1
Jak przeanalizowałeś cały Huckleberry Fin? Gdzie to jest w formacie tekstowym?
PascalVKooten
6
Świetne rozwiązanie. W funkcji dodałem if "np." In text: text = text.replace ("eg", "e <prd> g <prd>") if "ie" in text: text = text.replace ("ie" , „i <prd> e <prd>”) i całkowicie rozwiązało mój problem.
Sisay Chala
3
Świetne rozwiązanie z bardzo pomocnymi komentarzami! Wystarczy, aby uczynić go trochę bardziej wytrzymała jednak: prefixes = "(Mr|St|Mrs|Ms|Dr|Prof|Capt|Cpt|Lt|Mt)[.]", websites = "[.](com|net|org|io|gov|me|edu)", iif "..." in text: text = text.replace("...","<prd><prd><prd>")
Dascienz
1
Czy można sprawić, by ta funkcja traktowała zdania takie jak to jako jedno zdanie: Kiedy dziecko pyta matkę „Skąd się biorą dzieci?”, Co należy jej odpowiedzieć?
twhale
50

Zamiast używać wyrażenia regularnego do dzielenia tekstu na zdania, możesz również użyć biblioteki 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
źródło
Świetny, prostszy i wielokrotnego użytku przykład niż zaakceptowana odpowiedź.
Jay D.
Jeśli usuniesz spację po kropce, tokenize.sent_tokenize () nie działa, ale tokenizer.tokenize () działa! Hmm ...
Leonid Ganeline
1
for sentence in tokenize.sent_tokenize(text): print(sentence)
Victoria Stuart
11

Możesz spróbować użyć Spacy zamiast regex. Używam go i spełnia swoje zadanie.

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

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

for sent in tokens.sents:
    print(sent.string.strip())
Elf
źródło
1
Przestrzeń jest mega wielka. ale jeśli potrzebujesz tylko rozdzielić na zdania, przeniesienie tekstu do spacji zajmie zbyt dużo czasu, jeśli masz do czynienia z potokiem danych
Berlines
@Berlines Zgadzam się, ale nie mogłem znaleźć żadnej innej biblioteki, która wykonuje tę pracę tak czysto, jak spaCy. Ale jeśli masz jakieś sugestie, mogę spróbować.
Elf
Również dla użytkowników AWS Lambda Serverless, pliki danych wsparcia spacy mają wiele 100 MB (angielski duży to> 400 MB), więc nie możesz używać takich rzeczy po wyjęciu z pudełka, bardzo niestety (wielki fan Spacy tutaj)
Julian H.
9

Oto środek drogi, który nie polega na żadnych zewnętrznych bibliotekach. Używam funkcji rozumienia list, aby wykluczyć nakładanie się skrótów i terminatorów, a także aby wykluczyć nakładanie się między odmianami zakończeń, na przykład: „.” 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)

Użyłem funkcji find_all Karla z tego wpisu: Znajdź wszystkie wystąpienia podciągu w Pythonie

TennisVisuals
źródło
1
Idealne podejście! Inni nie łapią ...i ?!.
Shane Smiskol
6

W prostych przypadkach (gdzie zdania kończą się normalnie) powinno to działać:

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

Wyrażenie regularne to *\. +, które dopasowuje kropkę otoczoną 0 lub więcej spacjami po lewej stronie i 1 lub więcej po prawej stronie (aby zapobiec liczeniu czegoś takiego jak kropka w re.split jako zmiana w zdaniu).

Oczywiście nie jest to najbardziej solidne rozwiązanie, ale w większości przypadków będzie dobrze. Jedynym przypadkiem, którego to nie obejmuje, są skróty (może przejrzyj listę zdań i sprawdź, czy każdy ciąg sentenceszaczyna się od dużej litery?)

Rafe Kettler
źródło
29
Nie przychodzi Ci do głowy sytuacja w języku angielskim, w którym zdanie nie kończy się kropką? Wyobraź sobie, że! Moją odpowiedzią na to byłoby „pomyśl jeszcze raz”. (Widzicie, co tam zrobiłem?)
Ned Batchelder
@Ned wow, nie mogę uwierzyć, że byłem taki głupi. Muszę być pijany czy coś.
Rafe Kettler
Używam Pythona 2.7.2 na Win 7 x86, a wyrażenie regularne w powyższym kodzie daje mi ten błąd SyntaxError: EOL while scanning string literal:, wskazując na nawias zamykający (po text). Ponadto wyrażenie regularne, do którego odwołujesz się w tekście, nie istnieje w przykładowym kodzie.
Sabuncu
1
Wyrażenie regularne nie jest całkowicie poprawne, tak jak powinnor' *[\.\?!][\'"\)\]]* +'
fsociety
Może to spowodować wiele problemów, a także podzielić zdanie na mniejsze fragmenty. Rozważmy przypadek, w którym mamy „Zapłaciłem 3,5 USD za te lody”, a kawałki to „Zapłaciłem 3 USD” i „5 za te lody”. użyj domyślnego zdania nltk. tokenizer jest bezpieczniejszy!
Reihan_amn
6

Możesz również użyć funkcji tokenizacji zdań w 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
źródło
2

@Artyom,

Cześć! Możesz stworzyć nowy tokenizer dla języka rosyjskiego (i kilku innych języków), korzystając z tej funkcji:

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

a potem nazwij to w ten sposób:

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

Powodzenia, Marilena.

Marilena Di Bari
źródło
0

Bez wątpienia NLTK jest najbardziej odpowiedni do tego celu. Ale rozpoczęcie pracy z NLTK jest dość bolesne (ale po zainstalowaniu - po prostu czerpiesz korzyści)

Oto prosty kod ponownie oparty na 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
źródło
3
Tak, ale to tak łatwo zawodzi, mówiąc: „Pan Smith wie, że to zdanie”.
thomas
0

Musiałem przeczytać pliki z napisami i podzielić je na zdania. Po wstępnym przetworzeniu (takim jak usunięcie informacji o czasie itp. Z plików .srt) zmienna fullFile zawierała pełny tekst pliku z napisami. Poniższy, prymitywny sposób zgrabnie podzielił je na zdania. Zapewne miałem szczęście, że zdania zawsze kończyły się (poprawnie) spacją. Spróbuj najpierw tego, a jeśli ma jakieś wyjątki, dodaj więcej kontroli i sald.

# 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;

O! dobrze. Teraz zdaję sobie sprawę, że ponieważ moja treść była hiszpańska, nie miałem problemów z radzeniem sobie z „Mr.

kishore
źródło
0

mam nadzieję, że to pomoże ci w tekście łacińskim, chińskim i arabskim

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
źródło
0

Pracowałem nad podobnym zadaniem i natrafiłem na to zapytanie, klikając kilka linków i pracując nad kilkoma ćwiczeniami dla nltk, poniższy kod działał dla mnie jak magia.

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

wynik:

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

Źródło: https://www.geeksforgeeks.org/nlp-how-tokenizing-text-sentence-words-works/

Mazeen Muhammed
źródło