Python - 使用正则表达式将文本拆分为句子(句子分词)

30
我希望能从一个字符串中获取句子列表并将其打印出来,不想使用NLTK来实现。因此,需要在句子结尾的句号上进行分割,而不是在小数点、缩写或名称标题上进行分割,或者如果该句子包含.com。这是一个不起作用的正则表达式尝试。
import re

text = """\
Mr. Smith bought cheapsite.com for 1.5 million dollars, i.e. he paid a lot for it. Did he mind? Adam Jones Jr. thinks he didn't. In any case, this isn't true... Well, with a probability of .9 it isn't.
"""
sentences = re.split(r' *[\.\?!][\'"\)\]]* *', text)

for stuff in sentences:
        print(stuff)    

应该看起来像什么的示例输出

Mr. Smith bought cheapsite.com for 1.5 million dollars, i.e. he paid a lot for it. 
Did he mind?
Adam Jones Jr. thinks he didn't.
In any case, this isn't true...
Well, with a probability of .9 it isn't.

1
猴子桶:即使您没有使用“NLTK”,也请使用比单个正则表达式更合适的东西。 - user2864740
1
你不想使用NLTK的特别原因是什么?这正是它所做的事情,除了其他一些功能。你也可以看看这个库:https://github.com/fnl/sentence_splitter,它是一个小型库,同样可以用来实现这个功能(实际上是使用正则表达式实现的)。 - Amadan
解析自然人类语言和人工撰写的文本对于计算机来说非常困难,其中存在许多微妙之处。为什么不使用专门针对这种问题设计的NLTK呢? - Dan Lenski
2
NLTK的基本tokenize.sent_tokenize()相当粗暴。请看我的回答,了解它存在的许多问题。不要对提问者或问题不尊重,这实际上是非常复杂和有趣的,也是活跃研究的主题。 - smci
1
@user3590149 你可以试试 virtualenv;它可以让你创建一个隔离的 Python 环境,在其中安装任何你喜欢的软件包。 - ben author
显示剩余4条评论
10个回答

50

我在上面添加了示例输出。 - user3590149
如果您按照句点进行拆分,那么在最终输出中句点将会消失。 - Avinash Raj
1
(?<!\w.\w.)(?<![A-Z].)(?<![A-Z][a-z].)(?<=.|?) 工作得稍微好一些。 - amann
2
我将其扩展为 (?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?|\!\:)\s+|\p{Cc}+|\p{Cf}+,以考虑不可见字符。 - Borhan Kazimipour
1
这个函数几乎满意地运行了! - Stanislav Koncebovski
显示剩余19条评论

35
Ok,所以句子分词器是我稍微了解了一下的东西,使用正则表达式、nltk、CoreNLP、spaCy。你最终会编写自己的代码,这取决于应用程序。这些东西很棘手、很有价值,人们不会轻易地放弃他们的分词器代码。(最终,分词并不是一个确定性过程,它是概率性的,并且也非常依赖于你的语料库或领域,例如法律/金融文件与社交媒体帖子、Yelp评论和生物医学论文等)。 总的来说,你不能依靠一个伟大的白色不可靠的正则表达式,你必须编写一个使用多个正则表达式(包括正面和负面的)的函数; 还要有缩写字典,以及一些基本的语言解析,知道例如'I'、'USA'、'FCC'和'TARP'在英语中是大写的。 为了说明这可以变得非常复杂,让我们尝试为您编写一个确定性分词器的功能规格,仅仅是为了确定单个还是多个句点('.')/'...'表示句子结束,或其他什么: function isEndOfSentence(leftContext, rightContext)
  1. 针对数字或货币中的小数点,例如 1.23,$1.23,“这只是我的0.02美元”,以及考虑类似于1.2.A.3.a的部分引用、欧洲日期格式如09.07.2014、IP地址如192.168.1.1和MAC地址等情况,请返回 False。保留 HTML 标签。
  2. 针对已知缩写,请返回 False(不要将其拆分成单个字母)。例如“美国股市正在下跌”,需要使用已知缩写词典。除此词典之外,您将会得到错误的结果,除非您添加代码来检测未知的缩写词,例如A.B.C.并将它们添加到列表中。
  3. 句子末尾的省略号“...”是终止符号,但在句子中间不是。这不像您想象的那么容易:您需要查看左侧上下文和右侧上下文,特别是右侧上下文是否大写,并再次考虑大写字母如“I”和缩写词。以下示例证明了歧义:她让我待下去……一个小时后我离开了。(这是一句话还是两句话?无法确定)
  4. 您可能还想编写几个模式来检测和拒绝杂项非句子结尾的使用标点符号:表情符号:-),ASCII 艺术,间隔的省略号. . .和其他东西,尤其是 Twitter。 (使其自适应更加困难)我们如何确定@midnight是 Twitter 用户、在喜剧中央播出的节目,还是文本缩写,或者仅仅是不需要或错误的标点符号?这是非常棘手的。
  5. 在处理所有这些负面情况之后,您可以任意地说,任何独立的句号后跟空格可能会成为句子的结束。 (最终,如果您真的想获得额外的准确性,您将编写自己的概率句子分词器,并使用权重对其进行训练,并将其用于特定语料库(例如法律文本、广播媒体、StackOverflow、Twitter、论坛评论等)。)然后您需要手动审查示例和训练错误。请参阅 Manning 和 Jurafsky 书籍或 Coursera 课程 [a]。最终,您将获得您准备支付的正确性。
  6. 上述所有内容明显都只针对英语/缩写、美国数字/时间/日期格式。如果您想要使其与国家和语言无关,这将是一个更大的问题,您将需要语料库、母语为该语言的人员对其进行标记和质量保证等。
  7. 上述所有内容仍然只涉及 ASCII,实际上只有 96 个字符。允许输入为 Unicode,事情会变得更加困难(并且必须使用更大或更稀疏的训练集)。
在简单(确定性)情况下,function isEndOfSentence(leftContext, rightContext)将返回布尔值,但在更一般的情况下,它是概率性的:它返回一个0.0-1.0的浮点数(特定“.”是句子结尾的置信度水平)。
参考资料:[a] Coursera视频:“基本文本处理2-5-句子分割-Stanford NLP- Dan Jurafsky教授和Chris Manning”[更新:曾经有一个非官方版本在YouTube上,已被删除]

@bootsmaat:Jurafsky的视频很好,但只涵盖了确定性而非概率性分词。真实世界的方法应该是概率性的。 - smci
@smci,请你更新视频链接,谢谢。 - Sundeep Pidugu
@Sundeep:很遗憾看到那个精彩视频因版权问题被下架了。我找不到Jurafsky第2-5节课的视频在线上。如果你在YouTube上看到任何非官方的视频,请告诉我们。 - smci
1
只是想说感谢你写出了一个相对详尽的注意事项清单!我需要在另一种语言中实现这个,而你的清单是我见过最全面的! - rococo
1
@rococo:当然没问题。无论如何,在最近几十年的自然语言处理中,标记化已经从严格基于规则的方式转向了概率、上下文特定、短暂的方式,我们使用机器学习来学习它。尤其是当你需要处理不完整、不符合语法和/或错误标点、多语言、俚语、首字母缩写、表情符号、Emoji、Unicode等时...目标一直在不断发展。 - smci

6
尝试按照空格而不是点或?来分割输入,这样做的话最终结果中点或?将不会被打印出来。
>>> import re
>>> s = """Mr. Smith bought cheapsite.com for 1.5 million dollars, i.e. he paid a lot for it. Did he mind? Adam Jones Jr. thinks he didn't. In any case, this isn't true... Well, with a probability of .9 it isn't."""
>>> m = re.split(r'(?<=[^A-Z].[.?]) +(?=[A-Z])', s)
>>> for i in m:
...     print i
... 
Mr. Smith bought cheapsite.com for 1.5 million dollars, i.e. he paid a lot for it.
Did he mind?
Adam Jones Jr. thinks he didn't.
In any case, this isn't true...
Well, with a probability of .9 it isn't.

在格式良好的文本上运行得非常好(即所有句子必须以大写字母开头)。 - Iulius Curt
我最喜欢这个解决方案。在我尝试的所有情况下,它都可以正确地格式化它。我只是在它后面添加了一个感叹号。 (?<=[^A-Z].[.?!]) +(?=[A-Z]) - Ste
有人可以解释上面的正则表达式吗? - scv

2
sent = re.split('(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)(\s|[A-Z].*)',text)
for s in sent:
    print s

这里所使用的正则表达式为:(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)(\s|[A-Z].*) 第一个块:(?<!\w\.\w.):该模式在负反馈循环中搜索所有单词(\w),后跟句点(\.),后跟其他单词(\.) 第二个块:(?<![A-Z][a-z]\.):该模式在负反馈循环中搜索任何以大写字母([A-Z])开头,后跟小写字母([a-z])直到找到句点(\.)
第三个块:(?<=\.|\?):该模式在句点(\.)或问号(\?)的反馈循环中搜索
第四个块:(\s|[A-Z].*):该模式在第三个块中的句点或问号之后搜索。它搜索空格(\s)或以大写字母([A-Z].*)开头的任何字符序列。 如果输入是以下格式,则此块很重要:

Hello world.Hi I am here today.

即如果句点后有空格或没有空格。

1

我不太擅长正则表达式,但以上内容的一种更简单的版本是“暴力破解”

sentence = re.compile("([\'\"][A-Z]|([A-Z][a-z]*\. )|[A-Z])(([a-z]*\.[a-z]*\.)|([A-Za-z0-9]*\.[A-Za-z0-9])|([A-Z][a-z]*\. [A-Za-z]*)|[^\.?]|[A-Za-z])*[\.?]")

这意味着可接受的单元是 '[A-Z] 或 "[A-Z]。
请注意,大多数正则表达式都是贪婪的,因此在使用|(或)时顺序非常重要。也就是说,我先写了正则表达式,然后才是像Inc.这样的表单。


1

针对不以非字母开头且不包含引用部分的英语句子,天真的方法如下:

import re
text = """\
Mr. Smith bought cheapsite.com for 1.5 million dollars, i.e. he paid a lot for it. Did he mind? Adam Jones Jr. thinks he didn't. In any case, this isn't true... Well, with a probability of .9 it isn't.
"""
EndPunctuation = re.compile(r'([\.\?\!]\s+)')
NonEndings = re.compile(r'(?:Mrs?|Jr|i\.e)\.\s*$')
parts = EndPunctuation.split(text)
sentence = []
for part in parts:
  if len(part) and len(sentence) and EndPunctuation.match(sentence[-1]) and not NonEndings.search(''.join(sentence)):
    print(''.join(sentence))
    sentence = []
  if len(part):
    sentence.append(part)
if len(sentence):
  print(''.join(sentence))

通过扩展NonEndings可以减少误报的情况。其他情况需要额外的代码。用明智的方式处理打字错误将会证明这种方法很困难。

使用这种方法永远无法达到完美。但根据任务的不同,它可能只需要“足够”...


0

我在考虑了smci上面的评论后写下了这个。这是一种中庸的方法,不需要外部库,也不使用正则表达式。它允许你提供缩写词列表,并考虑到以终止符为结尾的句子,例如句号和引号:[.", ?',. )]。

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)

我使用了Karl在这篇文章中提到的find_all函数:在Python中查找所有子字符串的出现次数


0

试试这个:

(?<!\b(?:[A-Z][a-z]|\d|[i.e]))\.(?!\b(?:com|\d+)\b)

这个能用split命令吗?我在第一个问号处得到了无效语法。 - user3590149
使用正则表达式模块而不是re模块。 - walid toumi
问题在于它无法处理句子末尾的三个点。它会将最后两个点取出并放在下一句开头。 - user3590149

0

我的示例是基于Ali的示例,适应巴西葡萄牙语。谢谢Ali。

ABREVIACOES = ['sra?s?', 'exm[ao]s?', 'ns?', 'nos?', 'doc', 'ac', 'publ', 'ex', 'lv', 'vlr?', 'vls?',
               'exmo(a)', 'ilmo(a)', 'av', 'of', 'min', 'livr?', 'co?ls?', 'univ', 'resp', 'cli', 'lb',
               'dra?s?', '[a-z]+r\(as?\)', 'ed', 'pa?g', 'cod', 'prof', 'op', 'plan', 'edf?', 'func', 'ch',
               'arts?', 'artigs?', 'artg', 'pars?', 'rel', 'tel', 'res', '[a-z]', 'vls?', 'gab', 'bel',
               'ilm[oa]', 'parc', 'proc', 'adv', 'vols?', 'cels?', 'pp', 'ex[ao]', 'eg', 'pl', 'ref',
               '[0-9]+', 'reg', 'f[ilí]s?', 'inc', 'par', 'alin', 'fts', 'publ?', 'ex', 'v. em', 'v.rev']

ABREVIACOES_RGX = re.compile(r'(?:{})\.\s*$'.format('|\s'.join(ABREVIACOES)), re.IGNORECASE)

        def sentencas(texto, min_len=5):
            # baseado em https://dev59.com/ml8e5IYBdhLWcg3wdqGT
            texto = re.sub(r'\s\s+', ' ', texto)
            EndPunctuation = re.compile(r'([\.\?\!]\s+)')
            # print(NonEndings)
            parts = EndPunctuation.split(texto)
            sentencas = []
            sentence = []
            for part in parts:
                txt_sent = ''.join(sentence)
                q_len = len(txt_sent)
                if len(part) and len(sentence) and q_len >= min_len and \
                        EndPunctuation.match(sentence[-1]) and \
                        not ABREVIACOES_RGX.search(txt_sent):
                    sentencas.append(txt_sent)
                    sentence = []

                if len(part):
                    sentence.append(part)
            if sentence:
                sentencas.append(''.join(sentence))
            return sentencas

完整代码在:https://github.com/luizanisio/comparador_elastic


-2
如果你想在三个句点处分割句子(不确定这是否是你想要的),你可以使用这个正则表达式:
import re

text = """\
Mr. Smith bought cheapsite.com for 1.5 million dollars, i.e. he paid a lot for it. Did he mind? Adam Jones Jr. thinks he didn't. In any case, this isn't true... Well, with a probability of .9 it isn't.
"""
sentences = re.split(r'\.{3}', text)

for stuff in sentences:
     print(stuff)    

1
那不是他想要的。 - Amadan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接