我正在使用Python 3.5.2
我有两个列表
所以,我必须循环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
,已经略微提高了速度。而不是我的句子的长度,但它没有太大的改善。
感谢您提出任何建议。
答案 0 :(得分:106)
您可以尝试的一件事是编译一个单独的模式,如"\b(word1|word2|word3)\b"
。
因为re
依赖于C代码来进行实际匹配,所以节省的费用可能非常大。
正如@pvg在评论中指出的那样,它也受益于单次传递匹配。
如果你的单词不是正则表达式,那么Eric的answer会更快。
答案 1 :(得分:103)
如果您想要最快的解决方案,请使用此方法(使用set lookup)。对于类似于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()
)""
替换单词可能会留下两个空格(如代码中所示)\w+
也匹配重音字符(例如"ångström"
)。有一百万个句子,banned_words
有近100000个单词,脚本运行时间少于7个。
相比之下,Liteye的answer需要160秒才能获得1万个句子。
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 pattern from a trie。
答案 2 :(得分:81)
如果您想要最快速的基于正则表达式的解决方案,请使用此方法。对于类似于OP的数据集,它比接受的答案快约1000倍。
如果你不关心正则表达式,请使用this set-based version,这比正则表达式联合快2000倍。
由于优化模式的正则表达式引擎simple Regex union,doesn't do a very good job方法会因许多被禁止的单词而变得缓慢。
可以使用所有禁用的单词创建Trie并编写相应的正则表达式。由此产生的特里结构或正则表达式并不是人类可读的,但它们确实允许非常快速的查找和匹配。
['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 是因为:
foobar|baz
符合foobar
或baz
,but not foobaz
foo(bar|baz)
会将不需要的信息保存到capturing group。这是一个稍加修改的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 |陈| liyah(:\ 'S)| R(:????dvark(:(:\'? S | S))| ON))| b'(:\? 'S | A(:C(:我们(:(?:??\' S | ES)?)| [IK])|英尺|独行(:(?:?:\ 'S | S))|?NDON(:( ?:版| ING |换货(?:\?' S)| S'))| S(?:?E(?: (:MENT(:???\的)| [DS])???)| H(:( ?: E [DS] |荷兰国际集团))|荷兰国际集团)| T(:E(:(???? (?:\:换货的)| [DS]))|荷兰国际集团| toir(?:?(:\?的| S))))| b(:?如(?:?编号)| E(:SS(:(:\的| ES))| Y(?:(:\?'???S | S))?)| OT(:(:?\'S | T( ?:\的)| S))| reviat?(:E [DS] | I(:???纳克|上(:(:?\的| S))))| Y(? :\'S)| \ E'(:(:?\的| S)?))| d(:ICAT(:E [DS] | I(:????纳克|上(:( ?:\ 'S | S))))| OM(?:恩?(:(:\?' S | S))|伊纳勒)| U(?:CT(:( ?:版|我? (:NG |上(?:(:\? 'S | S))?)|或(?:(:\?' S | S))| S))| L(?:??\的?)))| E(:(?:?:\ 'S |上午| L(:(:\?' S | ARD |子(?:?:\'S))?)| R(:DEEN( ?:\ 'S)| nathy?(?:\'?S)| RA(:NT |重刑(?:(?:?:\'S | S))?))| T(:( ?:ŧ (:E(:R(:(:????\ '?S | S))| d)| ING |或(:(:?\'?S | S)))|?S))| yance(?:\ '?S)| d))| HOR(:( ?: R(:E(为:n(:CE(:?????\'?S)| T)| d)|荷兰国际集团)| S))| I(?:d(:E [DS] |荷兰国际集团|一月(:???\的))|盖尔| L(:???烯|它(?:IES | Y (?:\的)?)))|百灵(:ECT(:LY)| UR(:??????通货膨胀(:(:\的| S)?)| E [DS] |荷兰国际集团))| L(:一(:略去(:(?:???\'S | S))|?ZE)| E(:( ?: ST | R))| OOM | ution(? :(?:\'S | S) ?)| Y)|米\的| N(?:ΔE(:GAT(:E [DS] | I(:????纳克|上(:\'S))?)| R(α :\ 'S))| ormal?(:( ?:它(?:?IES | Y(:\?' S))| LY)))| O(?:???ARD |德(:(? :\ '?S | S))|李(:SH(:( ?: E [DS] |荷兰国际集团))|和灰(:???(?:?\'?S | IST(:(:\的| S)))))|米娜(:???BL [EY] | T(:E [DS] | I(:????纳克|上(:(?:\的| S) ))))| R(:???igin(:人(:(:?\'?S | S))| E(:(:?\的| S)))| T(? :( ?:版| I(?:?纳克|上(:(?:\ 'S | IST(:(?:?:\'?S | S))| S))|阳离子)| S)) ?)| U:)| T)| VE((ND(:( ?:编| | ING 3 S):(:???\的|板)))| R(?:一个(? :cadabra(:\?'| d S)?(?:E [DS] |荷兰国际集团?)|火腿(?:\的)|?米(:(:?\的| S))?| SI(:在(:(:\??? 'S | S))| VE(:???(:\' S | LY |岬(?:??\'S)| S))))|东| IDG(?:E(:( ?:换货(:(:\的| S))| [DS])?| ING |换货(?:(?:?\???)的| S )))| O?(?:广告| GAT(:E [DS] | I(:?????纳克|上(:(:\的| S))?)))| UPT(?: (:E(:ST | R)| LY |岬(?:?\?的)?)))| S(?:ALOM | C(:???ESS(:(?:\的|电子[DS] |荷兰国际集团))| ISSA(?:(:\的| [ES]))| OND(:( ?:编| ING | S)))| EN(:??????CE( ?:(?:\'?S | S))| T(:( ?: E(:E(:(:????\的| ISM(:?\的)| S))? ?| d)| ING | LY | S)))| INTH(:(:?\的| E(:\?的)))| O(:????L(:UT(?: E(:(?:?:\ 'S | LY | ST)?)|我?(:在(:\?' S)| SM(?:\'S)?))| v(?ê [DS] |荷兰国际集团))| R(:???b(:( ?: E(为:n(:CY(:\????的)| T(:?(?:?\的| S )))|?d)| ING | S))? | PTI ...
这真的难以理解,但是对于一个包含10万个被禁词的列表,这个Trie正则表达式比简单的正则表达式联盟快1000倍!
以下是使用trie-python-graphviz和graphviz twopi
导出的完整线索图:
答案 3 :(得分:11)
您可能想要尝试的一件事是预处理句子以编码单词边界。基本上通过分割单词边界将每个句子转换为单词列表。
这应该更快,因为要处理一个句子,你只需要逐步检查每个单词并检查它是否匹配。
目前,正则表达式搜索每次都必须再次查看整个字符串,查找字边界,然后"丢弃"这项工作的结果在下一次通过之前。
答案 4 :(得分:8)
嗯,这是一个快速简便的解决方案,带有测试集。
获胜策略:
re.sub(" \ w +",repl,sentence)搜索单词。
" REPL"可以是可赎回的。我使用了一个执行dict查找的函数,dict包含要搜索和替换的单词。
这是最简单,最快速的解决方案(请参阅下面示例代码中的函数replace4)。
次佳
这个想法是使用re.split将句子分成单词,同时保留分隔符以便稍后重构句子。然后,用简单的字典查找完成替换。
(参见下面示例代码中的函数replace3)。
时间示例功能:
RewriteCond %{REQUEST_URI} !^/himp/public
...和代码:
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)
答案 5 :(得分: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
答案 6 :(得分:4)
这个怎么样:
(def c (atom (chan))
(defn reset []
(close! @c)
(reset! c (chan)))
这些解决方案在字边界上分割并查找集合中的每个单词。它们应该比re.sub of word alternates(Liteyes的解决方案)更快,因为这些解决方案是#!/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)
,其中n是由O(n)
集查找引起的输入大小,而使用正则表达式替换将导致正则表达式引擎必须检查每个字符上的字匹配,而不仅仅是字边界。我的解决方案需要特别注意保留原始文本中使用的空格(即它不压缩空格并保留制表符,换行符和其他空白字符),但是如果你决定不关心它,那么它从输出中删除它们应该相当简单。
我测试了corpus.txt,这是从Gutenberg项目下载的多个电子书的串联,banned_words.txt是从Ubuntu的词表(/ usr / share / dict / american-english)中随机挑选的20000个单词。处理862462个句子大约需要30秒(其中一半是PyPy)。我将句子定义为以“。”分隔的任何内容。
amortized O(1)
PyPy特别受益于第二种方法,而CPython在第一种方法上的表现更好。上面的代码应该适用于Python 2和3。
答案 7 :(得分: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(单词)+ 2 * O(句子),这仍然是线性复杂度与2 * O(N 2 )的初始方法。
b.t.w。不要使用多线程。 GIL将阻止每个操作,因为你的任务严格受CPU约束,因此GIL没有机会被释放,但每个线程将同时发送滴答,这会导致额外的工作,甚至导致操作无限。
答案 8 :(得分:0)
将所有句子连接成一个文档。使用Aho-Corasick算法(here's one)的任何实现来定位您所有的&#34;坏&#34;话。遍历文件,替换每个坏词,更新后面找到的单词的偏移等。