在Python 3中加速数百万个正则表达式的替换


127

我正在使用Python 3.5.2

我有两个清单

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

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

我这样做是通过预编译我的单词,使它们位于\b元字符的侧面

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如果单词的长度大于句子的长度,我已经略微提高了速度,但这并没有太大的改进。

感谢您的任何建议。


1
这里的第一个答案有一些不错的示例代码:stackoverflow.com/questions/2846653/…只需将句子数组除以您已运行了那么多线程的CPU核心数即可
Mohammad Ali

4
您也可以尝试使用非正则表达式实现-逐字遍历输入的输入并将每个输入与一组匹配。这是单次通过,哈希查找非常快。
pvg

2
顺便说一句,这些句子要持续多久?750k行听起来不像是需要花费数小时才能处理的数据集。
pvg

2
@MohammadAli:不要为CPU限制的工作而烦恼该示例。Python具有执行字节码时需要的一个大锁(全局解释器锁),因此您无法从线程中受益于CPU工作。您需要使用multiprocessing(即多个Python进程)。
凯文(Kevin)

1
您需要具有工业实力的工具来执行此操作。regex trie是从字符串列表的三叉树生成的。失败的步骤不超过5个,这使其成为进行此类匹配的最快方法。例如:175,000个单词的词典或与您被禁止的列表类似的仅20,000个S单词
x15

Answers:


123

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

由于re依靠C代码进行实际匹配,因此节省的费用可观。

正如@pvg在评论中指出的,它也受益于单遍匹配。

如果您的单词不是正则表达式,那么Eric的答案会更快。


4
这不仅是C impl(有很大的不同),而且您还可以通过一次匹配。这个问题的变体经常出现,有点奇怪的是,没有(或者也许有,藏在某个地方?)用这个非常明智的想法为它提供了一个规范的SO答案。
pvg

40
@Liteye的建议将4小时的工作变成了4分钟的工作!我能够将所有20,000多个正则表达式加入到一个巨大的正则表达式中,而我的笔记本电脑并没有引起人们的注意。再次感谢。
pdanese

2
@Bakuriu: s/They actually use/They actually could in theory sometimes use/。您是否有任何理由相信Python的实现除了在这里执行循环外没有做其他事情?
user541686 '17

2
@Bakuriu:我真的很想知道是否如此,但是我认为正则表达式解决方案不会花费线性时间。如果它不能在工会之外建立特里,我不知道它怎么可能发生。
Eric Duminil

2
@Bakuriu:那不是原因。我问,如果你有一个有理由相信实现真正的行为是这样,你不是是否有理由相信它表现得这样。就我个人而言,我还没有遇到过一种主流编程语言的regex实现,该实现在线性时间内的运行方式与您期望经典regex的运行方式相同,因此,如果您知道Python能够做到这一点,则应展示一些证据。
user541686 '17

123

TLDR

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

如果您坚持使用正则表达式进行查找,请使用此基于Trie的版本,该版本仍比正则表达式联合快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近十万个单词,脚本运行时间不到7秒。

相比之下,Liteye的答案需要1万个句子需要160秒。

由于n是单词的总数和m被禁止的单词的数量,OP和Liteye的代码为O(n*m)

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

正则表达式联合测试

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

很难了解正则表达式引擎的工作方式,因此让我们编写一个简单的测试。

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

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


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联合的一种更快的替代方法是从trie创建regex模式


1
你说得对 我的缩进是错误的。我将其固定在原始问题中。至于50句/秒的评论很慢,我只能说是一个简化的例子。真实的数据集比我描述的要复杂,但是似乎并不相关。另外,将我的“单词”串联成一个正则表达式也极大地提高了速度。另外,我在替换后会“挤压”出两个空格。
pdanese

1
@ user36476感谢您的反馈,我删除了相应的部分。你可以试一下我的建议吗?我敢说这比公认的答案要快得多。
埃里克·杜米尼尔

1
由于您删除了该误导性O(1)声明,因此您的答案当然值得一票。
idmean

1
@idmean:是的,还不太清楚。它只是指查询:“这个词是被禁止的词吗?”。
艾里克·杜米尼尔

1
@EricDuminil:太好了!希望我能再投票一次。
Matthieu M.

105

TLDR

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

如果您不关心正则表达式,请使用此基于集合的版本,它比正则表达式联合快2000倍。

使用Trie优化正则表达式

一个简单的正则表达式工会的做法与许多禁用词语变得缓慢,这是因为正则表达式引擎不会做了很好的工作优化格局。

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

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

正则表达式联盟

该列表将转换为特里:

{
    '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匹配,正则表达式引擎只需比较第一个字符(不匹配),而无需尝试5个单词。这是5个单词的预处理过大杀伤力,但它显示了成千上万个单词的有希望的结果。

请注意,使用(?:)非捕获组是因为:

  • foobar|baz将匹配foobarbaz但不匹配foobaz
  • foo(bar|baz)将不需要的信息保存到捕获组

这是一个经过稍微修改的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())

测试

这是一个小测试(与测试相同):

# 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 | lone(? :(?:\'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)?)|盖尔| 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))?))| east | 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( ?: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))?| pti ...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))?| pti。 。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))?| pti。 。

这确实让人难以理解,但是对于100000个禁用词的列表而言,此Trie regex比简单的regex联合快1000倍!

这是完整的trie的图,并通过trie-python-graphviz和graphviz 导出twopi

在此处输入图片说明


看起来出于原始目的,不需要非捕获组。至少非捕获组的含义应该mentionned
泽维尔Combelle

3
@XavierCombelle:对的,我应该提到捕获小组:答案已经更新。我反过来却看到了:与正则表达式进行交互时需要括号,|但对于我们的目的根本不需要捕获组。他们只是放慢了速度,使用了更多的内存而没有收益。
埃里克·杜米尼尔

3
@EricDuminil这篇文章很完美,非常感谢:)
Mohamed AL ANI

1
@MohamedALANI:与哪种解决方案相比?
埃里克·杜米尼尔

1
@ PV8:应该只匹配完整的单词,是的,多亏了\b单词边界)。如果列表['apple', 'banana'],它将替换的话这是完全applebanana,而不是nanabanapineapple
埃里克·杜米尼尔

15

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

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

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


8

好吧,这是一个快速简单的解决方案,带有测试仪。

取胜策略:

re.sub(“ \ w +”,repl,sentence)搜索单词。

“ repl”可以是可调用的。我使用了一个执行字典查找的函数,该字典包含要搜索和替换的单词。

这是最简单,最快的解决方案(请参见下面的示例代码中的函数replace4)。

次好的

想法是使用re.split将句子拆分为单词,同时保留分隔符以稍后重建句子。然后,通过简单的字典查找完成替换。

(请参见下面的示例代码中的函数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)

1
支持测试。replace4和我的代码具有相似的性能。
埃里克·杜米尼尔

不确定def repl(m):在做什么以及如何m在功能replace4 中分配
StatguyUser

另外,我error: unbalanced parenthesis在线patterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
路上

尽管replace3和replace4函数解决了原始问题(替换单词),但是replace1和replace2更具通用性,因为即使针是短语(单词序列)而不是单个单词,它们也可以工作。
Zoltan Fedor

7

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

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

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

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

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

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

4

这个怎么样:

#!/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)设置查找而导致的,而使用正则表达式替代项将导致regex引擎必须检查单词是否匹配在每个字符上,而不仅仅是在单词边界上。我的解决方案a格外小心,以保留原始文本中使用的空格(即,它不压缩空格,并保留制表符,换行符和其他空格字符),但是如果您决定不关心它,则可以从输出中删除它们应该非常简单。

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

$ # 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和Python 3上都可以使用。


Python 3是该问题中的给定对象。我对此表示赞同,但我认为可能有必要牺牲此代码中的某些细节和“最佳”实现,以使其不那么冗长。
pvg

如果我理解正确,它的原理与我的回答基本相同,但是更冗长?拆分和加入\W+基本上就像subon \w+,对吗?
Eric Duminil

我想知道我下面的解决方案(函数replace4)是否比pypy更快;)我想对您的文件进行测试!
bobflux

3

实用方法

下述解决方案使用大量内存将所有文本存储在同一字符串中,并降低了复杂度。如果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(N 2)。


    顺便说一句,不要使用多线程。GIL将阻止每个操作,因为您的任务严格地受CPU限制,因此GIL没有机会被释放,但是每个线程将同时发送滴答声,这会导致额外的工作量,甚至导致操作达到无穷大。


    如果句子被(存储)在文本文件中,则它们之间已经用换行符隔开。因此,整个文件可以作为一个大字符串(或缓冲区)读取,删除单词,然后再次写回(或者可以使用内存映射直接在文件中完成)。Otoh,要删除一个单词,必须将字符串的其余部分移回以填补空白,因此,对于一个非常大的字符串,这将是一个问题。一种替代方法是将单词之间的部分写回到另一个字符串或文件(其中包括换行符)–或仅将这些部分移动到映射文件中(1)..
    Danny_ds

    ..最后一种方法(在单词之间移动/书写部分)与Eric Duminil的设置查找结合起来可能非常快,甚至根本不用正则表达式。(2)
    Danny_ds

    ..也许正则表达式已经进行了优化,只能在替换多个单词时移动那些部分,我不知道。
    Danny_ds

    0

    将所有句子连接到一个文档中。使用Aho-Corasick算法的任何实现(这里是)来查找所有“不好”的单词。遍历文件,替换每个坏词,更新后跟的发现词的偏移量等。

    By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
    Licensed under cc by-sa 3.0 with attribution required.