【问题标题】:Python - RegEx for splitting text into sentences (sentence-tokenizing) [duplicate]Python - 用于将文本拆分为句子的正则表达式(句子标记)[重复]
【发布时间】:2014-11-02 08:16:32
【问题描述】:

我想从一个字符串中创建一个句子列表,然后将它们打印出来。我不想使用 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.

【问题讨论】:

  • 装满猴子的桶:即使不使用“NLTK”,也要使用比单个正则表达式更合适的东西。
  • 您不想使用 NLTK 的任何特殊原因?这是exactly what it does,等等。还有this可以看一下,又是一个小库做这个(其实是用正则表达式做的)。
  • 解析自然的人类语言和人类撰写的文本对于计算机来说非常非常困难,并且有很多细微之处。为什么不想使用专为此类问题设计的 NLTK?
  • 基本的 NLTK tokenize.sent_tokenize() 非常残酷。请参阅我的答案,了解它出错的一大堆事情。不要不尊重 OP 或问题,这实际上是非常重要且有趣的,并且是一个积极研究的主题。
  • @user3590149 试试 virtualenv;这使您可以创建一个沙盒 Python 环境,可以在其中安装您喜欢的任何包

标签: python regex nlp tokenize


【解决方案1】:

好吧,我使用正则表达式、nltk、CoreNLP、spaCy 对句子标记器进行了一些详细的研究。您最终编写自己的,这取决于应用程序。这些东西既棘手又有价值,人们不只是放弃他们的标记器代码。 (最终,标记化不是一个确定性过程,它是概率性的,并且在很大程度上取决于您的语料库或域,例如法律/财务文件、社交媒体帖子、Yelp 评论和生物医学论文......)

一般来说,你不能依赖一个 Great White 无误的正则表达式,你必须编写一个使用多个正则表达式(正则和负)的函数;还有一个缩写字典,以及一些基本的语言解析,例如'I', 'USA', 'FCC', 'TARP' 用英文大写。

为了说明这很容易变得非常复杂,让我们尝试为您编写确定性标记器的功能规范只是来决定是单个句点还是多个句点('.'/'. ..') 表示句尾,或其他:

function isEndOfSentence(leftContext, rightContext)

  1. 为数字或货币中的小数返回 False,例如1.23 , $1.23, "That's just my $.02" 还要考虑像 1.2.A.3.a 这样的部分引用,像 09.07.2014 这样的欧洲日期格式,像 192.168.1.1 这样的 IP 地址,MAC 地址。 ..
  2. 为已知缩写返回 False(并且不要标记为单个字母),例如“美国股市正在下跌”;这需要一本已知缩写词的字典。除非您添加代码来检测未知缩写,例如 A.B.C.,否则该字典之外的任何内容都会出错。并将它们添加到列表中。
  3. 句子末尾的省略号“...”是终结符,但句子中间不是。这并不像您想象的那么容易:您需要查看左侧上下文和右侧上下文,特别是 RHS 是否大写,并再次考虑大写单词,如“I”和缩写。下面是一个模棱两可的例子:她让我留下来……我一小时后离开了。(那是一两句话吗?无法确定)
  4. 您可能还想编写一些模式来检测和拒绝标点符号的杂项非句尾使用:表情符号:-)、ASCII 艺术、间隔省略号。 . .和其他东西特别是。推特。 (使自适应更加困难)。我们如何判断@midnight 是 Twitter 用户、show on Comedy Central、文本速记,还是只是不需要的/垃圾/错字标点符号?非常重要。
  5. 在处理完所有这些否定情况后,您可以任意说任何孤立的句点后跟空格都可能是句子的结尾。 (最终,如果你真的想购买额外的准确性,你最终会编写自己的使用权重的概率句子标记器,并在特定的语料库(例如法律文本、广播媒体、StackOverflow、Twitter、论坛 cmets 等)上进行训练。) ) 然后您必须手动查看示例和训练错误。参见 Manning 和 Jurafsky 的书或 Coursera 课程 [a]。 最终,您将获得尽可能多的正确性。
  6. 以上所有内容显然都特定于英语/缩写、美国数字/时间/日期格式。如果你想让它独立于国家和语言,这是一个更大的提议,你需要语料库、母语人士来标记和 QA 等等。
  7. 以上所有内容仍然只是 ASCII,实际上只有 96 个字符。允许输入为 Unicode,事情变得更加困难(而且训练集必须要么更大要么更稀疏)

在简单(确定性)的情况下,function isEndOfSentence(leftContext, rightContext) 将返回布尔值,但在更一般的意义上,它是概率性的:它返回一个 0.0-1.0 的浮点数(该特定 '.' 是句子结尾的置信度)。

参考:[a] Coursera 视频:“基本文本处理 2-5 - 句子分割 - 斯坦福 NLP - 教授 Dan Jurafsky 和 ​​Chris Manning”[UPDATE: an unofficial version used to be on YouTube, was taken down]

【讨论】:

  • @bootsmaat:Jurafsky 的视频不错,但仅涵盖确定性而非概率性标记化。现实世界的方法应该是概率性的。
  • @smci 你能更新视频链接吗
  • @Sundeep:很遗憾看到精彩视频因版权原因被撤下。我在网上找不到 Jurafsky 第 2-5 课的视频。如果您在 Youtube 上看到任何非官方视频,请告诉我们。
  • 只是想说谢谢你写出一份相对详尽的清单,列出需要注意的事项!我需要用另一种语言来实现它,而你的列表是我见过的最全面的!
  • @rococo:当然。无论如何,近几十年来,NLP 中的标记化已经从基于规则的清晰规则转向我们使用 ML 学习的概率性、特定于上下文的短暂事物。特别是。当您需要处理不完整、不合语法和/或错误标点、多语言、俚语、首字母缩略词、表情符号、表情符号、Unicode...时,目标会不断发展。
【解决方案2】:

我的例子是基于阿里的例子,改编成巴西葡萄牙语。谢谢阿里。

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://stackoverflow.com/questions/25735644/python-regex-for-splitting-text-into-sentences-sentence-tokenizing
            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

【讨论】:

    【解决方案3】:

    我不擅长正则表达式,但上面的一个更简单的版本,实际上是“蛮力”

    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]
    请注意,大多数正则表达式都是贪婪的,所以当我们执行 |(or) 时,顺序非常重要。这就是为什么我先写了 i.e. 正则表达式,然后是像 Inc.

    这样的形式

    【讨论】:

      【解决方案4】:
      sent = re.split('(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)(\s|[A-Z].*)',text)
      for s in sent:
          print s
      

      这里使用的正则表达式是:(?&lt;!\w\.\w.)(?&lt;![A-Z][a-z]\.)(?&lt;=\.|\?)(\s|[A-Z].*)

      第一个块:(?&lt;!\w\.\w.):此模式在负反馈循环 (?&lt;!) 中搜索所有单词 (\w),然后是句号 (\.),然后是其他单词 (\.)

      第二个块:(?&lt;![A-Z][a-z]\.):此模式在负反馈循环中搜索以大写字母 ([A-Z]) 开头的任何内容,然后是小写字母 ([a-z]),直到找到一个点 (\.)

      第三块:(?&lt;=\.|\?):此模式在点 (\.) 或问号 (\?) 的反馈循环中搜索

      第四块:(\s|[A-Z].*):此模式在第三块的点 OR 问号之后搜索。它搜索空格(\s) 或任何以大写字母([A-Z].*) 开头的字符序列。 如果输入为

      ,则此块对于拆分很重要

      你好世界。嗨,我今天在这里。

      即点后是否有空格。

      【讨论】:

        【解决方案5】:

        我写这篇文章时考虑到了上面的 smci 的 cmets。这是一种中间方法,不需要外部库并且不使用正则表达式。它允许您提供缩写列表并说明以包装器中的终止符结尾的句子,例如句点和引号:[.", ?', .)]。

        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 函数:Find all occurrences of a substring in Python

        【讨论】:

          【解决方案6】:
          (?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)\s
          

          试试这个。拆分你的字符串 this.You 也可以查看演示。

          http://regex101.com/r/nG1gU7/27

          【讨论】:

          • 这个问题是它不处理句末的三个句点。它将三个中的最后两个放在下一个句子的开头。
          • 我在上面添加了示例输出。
          • @user3590149 当你拆分它时,一个 . 将丢失。你可以稍后将它应用到每个句子中,你就完成了。
          • (?
          • 我将其扩展为 (?&lt;!\w\.\w.)(?&lt;![A-Z][a-z]\.)(?&lt;=\.|\?|\!\:)\s+|\p{Cc}+|\p{Cf}+ 以也考虑不可见的字符。
          【解决方案7】:

          尝试根据空格而不是点或? 拆分输入,如果您这样做,那么点或? 将不会打印在最终结果中。

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

          【讨论】:

          • 非常适合格式良好的文本(即所有句子必须以大写字母开头)
          • 我最喜欢这个解决方案。在我尝试过的所有情况下,它实际上都会正确格式化它。我只是给它加了一个感叹号。 (?&lt;=[^A-Z].[.?!]) +(?=[A-Z])
          • 有人能解释一下上面的正则表达式吗
          【解决方案8】:

          不以非字母开头且不包含引用的词性的正确英语句子的幼稚方法:

          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 可以减少误报分裂。其他情况将需要额外的代码。用这种方法以合理的方式处理拼写错误是很困难的。

          使用这种方法,您将永远无法达到完美。但根据任务,它可能“足够”工作......

          【讨论】:

            【解决方案9】:

            试试这个:

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

            【讨论】:

            • 这个工作与 split 命令一起使用。一开始我的语法无效?
            • 使用 regex 模块而不是 re 模块。
            • 这个问题是它不处理句末的三个句点。它将三个中的最后两个放在下一个句子的开头。
            【解决方案10】:

            如果您想将句子分成 3 个句点(不确定这是否是您想要的),您可以使用以下正则表达式:

            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)    
            

            【讨论】:

            • 这不是他想要的。
            猜你喜欢
            • 1970-01-01
            • 2014-03-24
            • 1970-01-01
            • 2016-08-04
            • 2013-07-13
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多