【问题标题】:Speed up millions of regex replacements in Python 3在 Python 3 中加速数百万个正则表达式替换
【发布时间】:2017-08-02 05:45:39
【问题描述】:

我有两个列表:

  • 大约 750K “句子”(长字符串)的列表
  • 我想从我的 750K 句子中删除的大约 2 万个“单词”列表

因此,我必须遍历 750K 句子并执行大约 20K 替换,但前提是我的单词实际上是 “单词” 并且不属于更大的字符串。

我通过预编译我的单词来做到这一点,以便它们的两侧是\b word-boundary 元字符:

compiled_words = [re.compile(r'\b' + word + r'\b') for word in my20000words]

然后我遍历我的“句子”

import re

for sentence in sentences:
  for word in compiled_words:
    sentence = re.sub(word, "", sentence)
  # put sentence into a growing list

这个嵌套循环每秒处理大约 50 个句子,这很好,但处理我的所有句子仍然需要几个小时。

  • 有没有办法使用str.replace 方法(我认为它更快),但仍然要求替换只发生在单词边界

  • 或者,有没有办法加快re.sub 方法的速度?如果我的单词长度大于我的句子长度,我已经通过跳过re.sub 略微提高了速度,但这并没有太大的改进。

我正在使用 Python 3.5.2

【问题讨论】:

  • 这里的第一个答案有一些很好的示例代码:stackoverflow.com/questions/2846653/… 只需将您的句子数组除以您运行那么多线程的 CPU 内核数
  • 您也可以尝试非正则表达式实现 - 逐字遍历您的输入,并将每一个与一组匹配。这是单通道,哈希查找非常快。
  • 顺便说一句,这些句子有多长? 750k 行听起来不像是需要数小时才能处理的数据集。
  • @MohammadAli:对于受 CPU 限制的工作,不要为那个示例而烦恼。 Python 在执行字节码时需要一个大锁(全局解释器锁),因此您无法从 CPU 工作的线程中受益。您需要使用 multiprocessing(即多个 Python 进程)。
  • 您需要一个工业strength tool 来执行此操作。正则表达式树是从字符串列表的三叉树生成的。失败的步骤永远不会超过 5 个,这使其成为进行此类匹配的最快方法。示例:175,000 word dictionary 或类似于您的禁止列表只是 20,000 S-words

标签: python regex string performance replace


【解决方案1】:

TLDR

如果您想要最快的基于正则表达式的解决方案,请使用此方法。对于类似于 OP 的数据集,它比接受的答案快大约 1000 倍。

如果您不关心正则表达式,请使用this set-based version,它比正则表达式联合快 2000 倍。

使用 Trie 优化正则表达式

simple Regex union 方法会因为许多禁用词而变得缓慢,因为优化模式的正则表达式引擎 doesn't do a very good job

可以用所有被禁止的词创建一个Trie,并编写相应的正则表达式。生成的 trie 或 regex 并不是真正的人类可读的,但它们确实允许非常快速的查找和匹配。

示例

['foobar', 'foobah', 'fooxar', 'foozap', 'fooza']

列表被转换为trie:

{
    'f': {
        'o': {
            'o': {
                'x': {
                    'a': {
                        'r': {
                            '': 1
                        }
                    }
                },
                'b': {
                    'a': {
                        'r': {
                            '': 1
                        },
                        'h': {
                            '': 1
                        }
                    }
                },
                'z': {
                    'a': {
                        '': 1,
                        'p': {
                            '': 1
                        }
                    }
                }
            }
        }
    }
}

然后到这个正则表达式模式:

r"\bfoo(?:ba[hr]|xar|zap?)\b"

巨大的优势在于,如果要测试zoo 是否匹配,正则表达式引擎只需要needs to compare the first character(它不匹配),而不是trying the 5 words。这是 5 个单词的预处理过度,但它显示了数千个单词的有希望的结果。

请注意,使用 (?:) non-capturing groups 是因为:

代码

这是一个稍作修改的gist,我们可以将其用作trie.py 库:

import re


class Trie():
    """Regex::Trie in Python. Creates a Trie out of a list of words. The trie can be exported to a Regex pattern.
    The corresponding Regex should match much faster than a simple Regex union."""

    def __init__(self):
        self.data = {}

    def add(self, word):
        ref = self.data
        for char in word:
            ref[char] = char in ref and ref[char] or {}
            ref = ref[char]
        ref[''] = 1

    def dump(self):
        return self.data

    def quote(self, char):
        return re.escape(char)

    def _pattern(self, pData):
        data = pData
        if "" in data and len(data.keys()) == 1:
            return None

        alt = []
        cc = []
        q = 0
        for char in sorted(data.keys()):
            if isinstance(data[char], dict):
                try:
                    recurse = self._pattern(data[char])
                    alt.append(self.quote(char) + recurse)
                except:
                    cc.append(self.quote(char))
            else:
                q = 1
        cconly = not len(alt) > 0

        if len(cc) > 0:
            if len(cc) == 1:
                alt.append(cc[0])
            else:
                alt.append('[' + ''.join(cc) + ']')

        if len(alt) == 1:
            result = alt[0]
        else:
            result = "(?:" + "|".join(alt) + ")"

        if q:
            if cconly:
                result += "?"
            else:
                result = "(?:%s)?" % result
        return result

    def pattern(self):
        return self._pattern(self.dump())

测试

这是一个小测试(同this one):

# Encoding: utf-8
import re
import timeit
import random
from trie import Trie

with open('/usr/share/dict/american-english') as wordbook:
    banned_words = [word.strip().lower() for word in wordbook]
    random.shuffle(banned_words)

test_words = [
    ("Surely not a word", "#surely_NöTäWORD_so_regex_engine_can_return_fast"),
    ("First word", banned_words[0]),
    ("Last word", banned_words[-1]),
    ("Almost a word", "couldbeaword")
]

def trie_regex_from_words(words):
    trie = Trie()
    for word in words:
        trie.add(word)
    return re.compile(r"\b" + trie.pattern() + r"\b", re.IGNORECASE)

def find(word):
    def fun():
        return union.match(word)
    return fun

for exp in range(1, 6):
    print("\nTrieRegex of %d words" % 10**exp)
    union = trie_regex_from_words(banned_words[:10**exp])
    for description, test_word in test_words:
        time = timeit.timeit(find(test_word), number=1000) * 1000
        print("  %s : %.1fms" % (description, time))

它输出:

TrieRegex of 10 words
  Surely not a word : 0.3ms
  First word : 0.4ms
  Last word : 0.5ms
  Almost a word : 0.5ms

TrieRegex of 100 words
  Surely not a word : 0.3ms
  First word : 0.5ms
  Last word : 0.9ms
  Almost a word : 0.6ms

TrieRegex of 1000 words
  Surely not a word : 0.3ms
  First word : 0.7ms
  Last word : 0.9ms
  Almost a word : 1.1ms

TrieRegex of 10000 words
  Surely not a word : 0.1ms
  First word : 1.0ms
  Last word : 1.2ms
  Almost a word : 1.2ms

TrieRegex of 100000 words
  Surely not a word : 0.3ms
  First word : 1.2ms
  Last word : 0.9ms
  Almost a word : 1.6ms

有关信息,正则表达式的开头如下:

(?:a(?:(?:\'s|a(?:\'s|chen|liyah(?:\'s)?|r(?:dvark(?:(?:\' s|s))?|on))|b(?:\'s|a(?:c(?:us(?:(?:\'s|es))?|[ik])|ft|孤独(?:(?:\'s|s))?|ndon(?:(?:ed|ing|ment(?:\'s)?|s))?|s(?:e(?: (?:ment(?:\'s)?|[ds]))?|h(?:(?:e[ds]|ing))?|ing)|t(?:e(?:(? :ment(?:\'s)?|[ds]))?|ing|toir(?:(?:\'s|s))?))|b(?:as(?:id)?| e(?:ss(?:(?:\'s|es))?|y(?:(?:\'s|s))?)|ot(?:(?:\'s|t( ?:\'s)?|s))?|reviat(?:e[ds]?|i(?:ng|on(?:(?:\'s|s))?))|y(? :\'s)?|\é(?:(?:\'s|s))?)|d(?:icat(?:e[ds]?|i(?:ng|on(?:( ?:\'s|s))?))|om(?:en(?:(?:\'s|s))?|inal)|u(?:ct(?:(?:ed|i (?:ng|on(?:(?:\'s|s))?)|或(?:(?:\'s|s))?|s))?|l(?:\'s )?))|e(?:(?:\'s|am|l(?:(?:\'s|ard|son(?:\'s)?))?|r(?:deen( ?:\'s)?|nathy(?:\'s)?|ra(?:nt|tion(?:(?:\'s|s))?))|t(?:(?:t (?:e(?:r(?:(?:\'s|s))?|d)|ing|or(?:(?:\'s|s))?)|s))?| yance(?:\'s)?|d))?|hor(?:(?:r(?:e(?:n(?:ce(?:\'s)?|t)|d)| ing)|s))?|i(?:d(?:e[ds]?|ing|jan(?:\'s)?)|gail|l(?:ene|it(?:ies|y (?:\'s)?)))|j(?:ect(?:ly)?|ur(?:ation(?:(?:\'s|s))?|e[ds]?| ing))|l(?:a(?:tive(?:(?:\'s|s))?|ze)|e(?:(?:st|r))?|oom|ution(? :(?:\'s|s)) ?|y)|m\'s|n(?:e(?:gat(?:e[ds]?|i(?:ng|on(?:\'s)?))|r(?: \'s)?)|ormal(?:(?:it(?:ies|y(?:\'s)?)|ly))?)|o(?:ard|de(?:(?: \'s|s))?|li(?:sh(?:(?:e[ds]|ing))?|tion(?:(?:\'s|ist(?:(?:\' s|s))?))?)|mina(?:bl[ey]|t(?:e[ds]?|i(?:ng|on(?:(?:\'s|s)) ?)))|r(?:igin(?:al(?:(?:\'s|s))?|e(?:(?:\'s|s))?)|t(?: (?:ed|i(?:ng|on(?:(?:\'s|ist(?:(?:\'s|s))?|s))?|ve)|s))? )|u(?:nd(?:(?:ed|ing|s))?|t)|ve(?:(?:\'s|board))?)|r(?:a(?: cadabra(?:\'s)?|d(?:e[ds]?|ing)|ham(?:\'s)?|m(?:(?:\'s|s))?|si (?:on(?:(?:\'s|s))?|ve(?:(?:\'s|ly|ness(?:\'s)?|s))?))|东|idg(?:e(?:(?:ment(?:(?:\'s|s))?|[ds]))?|ing|ment(?:(?:\'s|s) )?)|o(?:ad|gat(?:e[ds]?|i(?:ng|on(?:(?:\'s|s))?)))|upt(?:( ?:e(?:st|r)|ly|ness(?:\'s)?))?)|s(?:alom|c(?:ess(?:(?:\'s|e[ ds]|ing))?|issa(?:(?:\'s|[es]))?|ond(?:(?:ed|ing|s))?)|en(?:ce(? :(?:\'s|s))?|t(?:(?:e(?:e(?:(?:\'s|ism(?:\'s)?|s))?| d)|ing|ly|s))?)|inth(?:(?:\'s|e(?:\'s)?))?|o(?:l(?:ut(?:e (?:(?:\'s|ly|st?))?|i(?:on(?:\'s)?|sm(?:\'s)?))|v(?:e[ ds]?|ing))|r(?:b(?:(?:e(?:n(?:cy(?:\'s)?|t(?:(?:\'s|s)) )?)|d)|ing|s))?|点...

真的不可读,但是对于 100000 个禁用词的列表,这个 Trie 正则表达式比简单的正则表达式联合快 1000 倍!

这是完整的 trie 图表,使用 trie-python-graphviz 和 graphviz twopi 导出:

【讨论】:

  • 看起来,出于原始目的,不需要非捕获组。至少应该提到非捕获组的含义
  • @XavierCombelle:你说得对,我应该提到捕获组:答案已更新。不过,我认为它是另一种方式:使用| 进行正则表达式交替需要括号,但我们的目的根本不需要捕获组。他们只会减慢进程并使用更多内存而没有任何好处。
  • @EricDuminil 这个帖子很完美,非常感谢:)
  • @MohamedALANI:与哪个解决方案相比?
  • @PV8:它应该只匹配完整的单词,是的,感谢\b (word boundary)。如果列表是['apple', 'banana'],它将替换正好是applebanana,但不是nanabanapineapple
【解决方案2】:

TLDR

如果您想要最快的解决方案,请使用此方法(使用集合查找)。对于类似于 OP 的数据集,它比接受的答案快大约 2000 倍。

如果您坚持使用正则表达式进行查找,请使用this trie-based version,它仍然比正则表达式联合快 1000 倍。

理论

如果您的句子不是巨大的字符串,每秒处理超过 50 个可能是可行的。

如果你将所有被禁止的单词保存到一个集合中,那么检查该集合中是否包含另一个单词会非常快。

将逻辑打包成一个函数,将此函数作为参数传递给re.sub,就完成了!

代码

import re
with open('/usr/share/dict/american-english') as wordbook:
    banned_words = set(word.strip().lower() for word in wordbook)


def delete_banned_words(matchobj):
    word = matchobj.group(0)
    if word.lower() in banned_words:
        return ""
    else:
        return word

sentences = ["I'm eric. Welcome here!", "Another boring sentence.",
             "GiraffeElephantBoat", "sfgsdg sdwerha aswertwe"] * 250000

word_pattern = re.compile('\w+')

for sentence in sentences:
    sentence = word_pattern.sub(delete_banned_words, sentence)

转换后的句子是:

' .  !
  .
GiraffeElephantBoat
sfgsdg sdwerha aswertwe

注意:

  • 搜索不区分大小写(感谢lower()
  • "" 替换单词可能会留下两个空格(如您的代码中所示)
  • 对于 python3,\w+ 还匹配重音字符(例如 "ångström")。
  • 任何非单词字符(制表符、空格、换行符、标记...)都将保持不变。

性能

有一百万个句子,banned_words 有近 100000 个单词,脚本运行时间不到 7 秒。

相比之下,Liteye 的 answer 需要 160s 才能写 10000 句。

n是总字数,m是禁字数,OP和Liteye的代码是O(n*m)

相比之下,我的代码应该在O(n+m) 中运行。考虑到句子比禁词多很多,算法变成O(n)

正则表达式联合测试

使用'\b(word1|word2|...|wordN)\b' 模式进行正则表达式搜索的复杂性是什么?是O(N) 还是O(1)

很难掌握正则表达式引擎的工作方式,所以让我们编写一个简单的测试。

此代码将10**i 随机英文单词提取到一个列表中。它创建相应的正则表达式联合,并用不同的词对其进行测试:

  • one 显然不是一个词(它以# 开头)
  • one 是列表中的第一个单词
  • 一个是列表中的最后一个单词
  • 一个看起来像一个词,但不是


import re
import timeit
import random

with open('/usr/share/dict/american-english') as wordbook:
    english_words = [word.strip().lower() for word in wordbook]
    random.shuffle(english_words)

print("First 10 words :")
print(english_words[:10])

test_words = [
    ("Surely not a word", "#surely_NöTäWORD_so_regex_engine_can_return_fast"),
    ("First word", english_words[0]),
    ("Last word", english_words[-1]),
    ("Almost a word", "couldbeaword")
]


def find(word):
    def fun():
        return union.match(word)
    return fun

for exp in range(1, 6):
    print("\nUnion of %d words" % 10**exp)
    union = re.compile(r"\b(%s)\b" % '|'.join(english_words[:10**exp]))
    for description, test_word in test_words:
        time = timeit.timeit(find(test_word), number=1000) * 1000
        print("  %-17s : %.1fms" % (description, time))

它输出:

First 10 words :
["geritol's", "sunstroke's", 'fib', 'fergus', 'charms', 'canning', 'supervisor', 'fallaciously', "heritage's", 'pastime']

Union of 10 words
  Surely not a word : 0.7ms
  First word        : 0.8ms
  Last word         : 0.7ms
  Almost a word     : 0.7ms

Union of 100 words
  Surely not a word : 0.7ms
  First word        : 1.1ms
  Last word         : 1.2ms
  Almost a word     : 1.2ms

Union of 1000 words
  Surely not a word : 0.7ms
  First word        : 0.8ms
  Last word         : 9.6ms
  Almost a word     : 10.1ms

Union of 10000 words
  Surely not a word : 1.4ms
  First word        : 1.8ms
  Last word         : 96.3ms
  Almost a word     : 116.6ms

Union of 100000 words
  Surely not a word : 0.7ms
  First word        : 0.8ms
  Last word         : 1227.1ms
  Almost a word     : 1404.1ms

所以看起来搜索带有'\b(word1|word2|...|wordN)\b' 模式的单个单词有:

  • O(1) 最佳案例
  • O(n/2) 平均情况,仍然是 O(n)
  • O(n) 最坏的情况

这些结果与简单的循环搜索一致。

一个比正则表达式联合更快的替代方法是创建regex pattern from a trie

【讨论】:

  • 你是对的。我的缩进是错误的。我在原始问题中修复了它。至于50句/秒慢的评论,我只能说我提供了一个简化的例子。真实的数据集比我描述的要复杂,但它似乎并不相关。此外,将我的“单词”连接成一个正则表达式大大提高了速度。另外,我在替换后“挤出”了双空格。
  • @user36476 感谢反馈,我删除了相应的部分。你能试试我的建议吗?我敢说这比公认的答案要快得多。
  • 由于您删除了具有误导性的 O(1) 声明,因此您的回答绝对值得一票。
  • @idmean:没错,这不是很清楚。它只是指查找:“这个词是被禁止的词吗?”。
  • @EricDuminil:干得好!希望我能再次投票。
【解决方案3】:

您可以尝试的一件事是编译一个单一的模式,例如 "\b(word1|word2|word3)\b"

因为re 依赖 C 代码来进行实际匹配,所以节省的费用会非常可观。

正如@pvg 在 cmets 中指出的那样,它也受益于单通道匹配。

如果你的话不是正则表达式,Eric 的answer 更快。

【讨论】:

  • 这不仅仅是 C impl(这会产生很大的不同),而且您还可以通过单次传递进行匹配。这个问题的变体经常出现,这有点奇怪,没有(或者可能有,隐藏在某个地方?)一个规范的 SO 答案,这个非常明智的想法。
  • @Liteye 您的建议将 4 小时的工作变成了 4 分钟的工作!我能够将所有 20,000 多个正则表达式加入到一个巨大的正则表达式中,而我的笔记本电脑并没有眨眼。再次感谢。
  • @Bakuriu:s/They actually use/They actually could in theory sometimes use/。你有什么理由相信 Python 的实现在这里除了循环之外还做其他事情?
  • @Bakuriu:我很想知道是否是这种情况,但我认为正则表达式解决方案不需要线性时间。如果它不从联合中构建一个 Trie,我不知道它会如何发生。
  • @Bakuriu:这不是原因。我问的是你是否有理由相信实现实际上会这样,而不是你是否有理由相信它可以那样表现。就我个人而言,我还没有遇到过一种主流编程语言的正则表达式实现,它在线性时间内工作的方式与你期望的经典正则表达式相同,所以如果你知道 Python 这样做,你应该提供一些证据。
【解决方案4】:

您可能想尝试的一件事是预处理句子以编码单词边界。基本上通过在单词边界上拆分将每个句子变成单词列表。

这应该会更快,因为要处理一个句子,你只需要逐步检查每个单词并检查它是否匹配。

目前正则表达式搜索每次都必须再次遍历整个字符串,寻找单词边界,然后在下一次遍历之前“丢弃”这项工作的结果。

【讨论】:

    【解决方案5】:

    嗯,这是一个快速简单的解决方案,带有测试集。

    最佳策略:

    • re.sub("\w+",repl,sentence) 搜索字词。
    • “repl”可以是可调用的。我使用了一个执行 dict 查找的函数,该 dict 包含要搜索和替换的单词。
    • 这是最简单、最快的解决方案(参见下面示例代码中的函数replace4)。

    次优策略:

    • 这个想法是使用re.split将句子分成单词,同时保留分隔符以便稍后重建句子。然后,通过简单的 dict 查找来完成替换。
    • 实现:(参见下面示例代码中的函数replace3)。

    示例函数的时间安排:

    replace1:     0.62 sentences/s
    replace2:     7.43 sentences/s
    replace3: 48498.03 sentences/s
    replace4: 61374.97 sentences/s (...and 240,000/s with PyPy)
    

    ...和代码:

    #! /bin/env python3
    # -*- coding: utf-8
    
    import time, random, re
    
    def replace1( sentences ):
        for n, sentence in enumerate( sentences ):
            for search, repl in patterns:
                sentence = re.sub( "\\b"+search+"\\b", repl, sentence )
    
    def replace2( sentences ):
        for n, sentence in enumerate( sentences ):
            for search, repl in patterns_comp:
                sentence = re.sub( search, repl, sentence )
    
    def replace3( sentences ):
        pd = patterns_dict.get
        for n, sentence in enumerate( sentences ):
            #~ print( n, sentence )
            # Split the sentence on non-word characters.
            # Note: () in split patterns ensure the non-word characters ARE kept
            # and returned in the result list, so we don't mangle the sentence.
            # If ALL separators are spaces, use string.split instead or something.
            # Example:
            #~ >>> re.split(r"([^\w]+)", "ab céé? . d2eéf")
            #~ ['ab', ' ', 'céé', '? . ', 'd2eéf']
            words = re.split(r"([^\w]+)", sentence)
            
            # and... done.
            sentence = "".join( pd(w,w) for w in words )
    
            #~ print( n, sentence )
    
    def replace4( sentences ):
        pd = patterns_dict.get
        def repl(m):
            w = m.group()
            return pd(w,w)
        
        for n, sentence in enumerate( sentences ):
            sentence = re.sub(r"\w+", repl, sentence)
    
    
    
    # Build test set
    test_words = [ ("word%d" % _) for _ in range(50000) ]
    test_sentences = [ " ".join( random.sample( test_words, 10 )) for _ in range(1000) ]
    
    # Create search and replace patterns
    patterns = [ (("word%d" % _), ("repl%d" % _)) for _ in range(20000) ]
    patterns_dict = dict( patterns )
    patterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
    
    
    def test( func, num ):
        t = time.time()
        func( test_sentences[:num] )
        print( "%30s: %.02f sentences/s" % (func.__name__, num/(time.time()-t)))
    
    print( "Sentences", len(test_sentences) )
    print( "Words    ", len(test_words) )
    
    test( replace1, 1 )
    test( replace2, 10 )
    test( replace3, 1000 )
    test( replace4, 1000 )
    

    编辑:您也可以在检查是否传递小写的句子列表并编辑 repl 时忽略小写

    def replace4( sentences ):
    pd = patterns_dict.get
    def repl(m):
        w = m.group()
        return pd(w.lower(),w)
    

    【讨论】:

    • 支持测试。 replace4 和我的代码有相似的表现。
    • 不确定 def repl(m): 在做什么以及如何在函数 replace4 中分配 m
    • 我也收到错误 error: unbalanced parenthesis 的行 patterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
    • 虽然 replace3 和 replace4 函数解决了原始问题(替换单词),但 replace1 和 replace2 更通用,因为即使针是一个短语(单词序列)而不是就一个字。
    【解决方案6】:

    也许 Python 在这里不是正确的工具。这是一个带有 Unix 工具链的工具

    sed G file         |
    tr ' ' '\n'        |
    grep -vf blacklist |
    awk -v RS= -v OFS=' ' '{$1=$1}1'
    

    假设您的黑名单文件经过预处理并添加了单词边界。步骤是:将文件转换为双倍行距,将每个句子拆分为每行一个单词,从文件中批量删除黑名单单词,然后将行合并。

    这应该至少快一个数量级。

    用于从单词中预处理黑名单文件(每行一个单词)

    sed 's/.*/\\b&\\b/' words > blacklist
    

    【讨论】:

      【解决方案7】:

      这个怎么样:

      #!/usr/bin/env python3
      
      from __future__ import unicode_literals, print_function
      import re
      import time
      import io
      
      def replace_sentences_1(sentences, banned_words):
          # faster on CPython, but does not use \b as the word separator
          # so result is slightly different than replace_sentences_2()
          def filter_sentence(sentence):
              words = WORD_SPLITTER.split(sentence)
              words_iter = iter(words)
              for word in words_iter:
                  norm_word = word.lower()
                  if norm_word not in banned_words:
                      yield word
                  yield next(words_iter) # yield the word separator
      
          WORD_SPLITTER = re.compile(r'(\W+)')
          banned_words = set(banned_words)
          for sentence in sentences:
              yield ''.join(filter_sentence(sentence))
      
      
      def replace_sentences_2(sentences, banned_words):
          # slower on CPython, uses \b as separator
          def filter_sentence(sentence):
              boundaries = WORD_BOUNDARY.finditer(sentence)
              current_boundary = 0
              while True:
                  last_word_boundary, current_boundary = current_boundary, next(boundaries).start()
                  yield sentence[last_word_boundary:current_boundary] # yield the separators
                  last_word_boundary, current_boundary = current_boundary, next(boundaries).start()
                  word = sentence[last_word_boundary:current_boundary]
                  norm_word = word.lower()
                  if norm_word not in banned_words:
                      yield word
      
          WORD_BOUNDARY = re.compile(r'\b')
          banned_words = set(banned_words)
          for sentence in sentences:
              yield ''.join(filter_sentence(sentence))
      
      
      corpus = io.open('corpus2.txt').read()
      banned_words = [l.lower() for l in open('banned_words.txt').read().splitlines()]
      sentences = corpus.split('. ')
      output = io.open('output.txt', 'wb')
      print('number of sentences:', len(sentences))
      start = time.time()
      for sentence in replace_sentences_1(sentences, banned_words):
          output.write(sentence.encode('utf-8'))
          output.write(b' .')
      print('time:', time.time() - start)
      

      这些解决方案在单词边界上拆分并查找集合中的每个单词。它们应该比单词替换的 re.sub 更快(Liteyes 的解决方案),因为这些解决方案是 O(n) 其中 n 是由于amortized O(1) 设置查找而导致的输入大小,而使用正则表达式替换会导致正则表达式引擎必须检查每个字符上的单词匹配,而不仅仅是单词边界。我的解决方案特别注意保留原始文本中使用的空格(即它不压缩空格并保留制表符、换行符和其他空格字符),但如果您决定不关心它,它从输出中删除它们应该相当简单。

      我在 corpus.txt 上进行了测试,它是从 Gutenberg 项目下载的多本电子书的串联,banned_words.txt 是从 Ubuntu 的单词表 (/usr/share/dict/american-english) 中随机挑选的 20000 个单词。处理 862462 个句子大约需要 30 秒(在 PyPy 上是一半)。我将句子定义为以“。”分隔的任何内容。

      $ # replace_sentences_1()
      $ python3 filter_words.py 
      number of sentences: 862462
      time: 24.46173644065857
      $ pypy filter_words.py 
      number of sentences: 862462
      time: 15.9370770454
      
      $ # replace_sentences_2()
      $ python3 filter_words.py 
      number of sentences: 862462
      time: 40.2742919921875
      $ pypy filter_words.py 
      number of sentences: 862462
      time: 13.1190629005
      

      PyPy 尤其受益于第二种方法,而 CPython 在第一种方法中表现更好。上面的代码应该适用于 Python 2 和 3。

      【讨论】:

      • Python 3 是在问题中给出的。我对此表示赞同,但我认为在此代码中牺牲一些细节和“最佳”实现可能是值得的,以使其不那么冗长。
      • 如果我理解正确的话,基本上和我的回答是一样的原理,但是更啰嗦?在\W+ 上拆分和加入基本上就像在\w+ 上的sub,对吧?
      • 我想知道我下面的解决方案(函数 replace4)是否比 pypy 快;)我想测试你的文件!
      【解决方案8】:

      实用方法

      下面描述的解决方案使用大量内存来将所有文本存储在同一字符串中并降低复杂度。如果 RAM 是个问题,请在使用前三思。

      使用join/split 技巧,您可以完全避免循环,从而加快算法速度。

    • 用句子中不包含的特殊分隔符连接句子:
    • merged_sentences = ' * '.join(sentences)
      

    • 使用|“或”正则表达式语句为您需要从句子中删除的所有单词编译一个正则表达式:
    • regex = re.compile(r'\b({})\b'.format('|'.join(words)), re.I) # re.I is a case insensitive flag
      

    • 用编译的正则表达式为单词下标,并用特殊分隔符将其拆分回分隔的句子:
    • clean_sentences = re.sub(regex, "", merged_sentences).split(' * ')
      

      性能

      "".join 复杂度为 O(n)。这很直观,但无论如何都有一个来源的简短引用:

      for (i = 0; i < seqlen; i++) {
          [...]
          sz += PyUnicode_GET_LENGTH(item);
      

      因此,使用 join/split 你有 O(words) + 2*O(sentences) 与初始方法的 2*O(N2) 相比,这仍然是线性复杂度。


      b.t.w.不要使用多线程。 GIL 将阻塞每个操作,因为您的任务严格受 CPU 限制,因此 GIL 没有机会被释放,但每个线程会同时发送滴答声,这会导致额外的工作,甚至导致操作无穷大。

      【讨论】:

      • 如果句子(被)存储在文本文件中,它们已经用换行符分隔。所以整个文件可以作为一个大字符串(或缓冲区)读入,删除单词,然后再次写回(或者这可以直接使用内存映射在文件中完成)。 Otoh,要删除一个单词,必须将字符串的其余部分移回以填补空白,因此对于一个非常大的字符串来说这将是一个问题。另一种方法是将单词之间的部分写回另一个字符串或文件(包括换行符) - 或者只是将这些部分移动到一个映射文件中(1)..
      • .. 最后一种方法(在单词之间移动/写入部分)结合Eric Duminil’s set lookup 可能非常快,甚至可能根本不使用正则表达式。 (2)
      • .. 或者可能正则表达式已经优化为仅在替换多个单词时移动这些部分,我不知道。
      【解决方案9】:

      将所有句子连接到一个文档中。使用 Aho-Corasick 算法 (here's one) 的任何实现来定位所有“坏”词。遍历文件,替换每个坏词,更新找到的词的偏移量等。

      【讨论】:

        猜你喜欢
        • 2021-12-06
        • 2015-01-06
        • 2019-01-12
        • 1970-01-01
        • 2013-06-13
        • 2012-02-16
        • 2012-06-17
        • 1970-01-01
        相关资源
        最近更新 更多