Python 3에서 수백만 개의 정규식 대체 속도 향상
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
내 단어의 길이가 문장의 길이보다 길면 건너 뛰면서 속도를 약간 향상 시켰지만 크게 개선 되지는 않았습니다.
제안 해 주셔서 감사합니다.
시도 할 수있는 한 가지는 같은 단일 패턴을 컴파일하는 것 "\b(word1|word2|word3)\b"
입니다.
re
실제 매칭을 수행하기 위해 C 코드를 사용 하기 때문에 절감 효과가 크게 향상 될 수 있습니다.
주석에서 @pvg가 지적했듯이 단일 패스 일치의 이점도 있습니다.
당신의 말이 정규 표현식이 아니면 Eric의 답변 이 더 빠릅니다.
TLDR
가장 빠른 솔루션을 원하면이 방법 (조회 설정 사용)을 사용하십시오. OP와 유사한 데이터 세트의 경우 허용되는 답변보다 약 2000 배 빠릅니다.
조회에 정규식을 사용해야한다고 주장하는 경우이 트라이 기반 버전을 사용하십시오. 이 버전 은 정규식 조합보다 여전히 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
있고 거의 10 만 단어가 있으며 스크립트는 7 초 이내에 실행됩니다.
이에 비해 Liteye의 답변 에는 1 만 개의 문장에 160이 필요했습니다.
함께 n
단어의 총 amound하고있는 m
금지 된 단어의 양, 영업 이익의 및 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)
최악의 경우
이 결과는 간단한 루프 검색과 일치합니다.
정규식 조합에 대한 훨씬 빠른 대안 은 트라이에서 정규식 패턴 을 만드는 것 입니다.
TLDR
가장 빠른 정규식 기반 솔루션을 원하는 경우이 방법을 사용하십시오. OP와 유사한 데이터 세트의 경우 허용되는 답변보다 약 1000 배 빠릅니다.
정규식에 신경 쓰지 않으면 이 정규식 조합보다 2000 배 빠른 이 세트 기반 버전을 사용 하십시오 .
Trie로 최적화 된 정규식
간단한 정규식 조합 정규식 엔진이 있기 때문에 접근 방식은 많은 금지 된 단어로 느려집니다 아주 좋은 일하지 않는 패턴을 최적화합니다.
모든 금지 된 단어 로 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
일치 여부를 테스트하기 위해 정규 표현식 엔진 이 5 단어 를 시도하는 대신 첫 번째 문자 (일치하지 않음) 만 비교하면 된다는 것입니다 . 그것은 5 단어에 대한 전처리 과잉이지만 수천 단어에 대한 유망한 결과를 보여줍니다.
참고 (?:)
비 캡처 그룹이 있기 때문에 사용된다 :
암호
다음 은 라이브러리 로 사용할 수 있는 약간 수정 된 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))?) | or (? : (? : \'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))? yy | m \'s | n (? : e (? : gat (? : e [ds] ? | i (? : ng | on (? : \ 's)?)) | r (? : \'s)?) | oror (? :( ?: 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 ( ? : \ '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 .. .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 정규 표현식은 간단한 정규 표현식 조합보다 1000 배 빠릅니다!
다음은 trie-python-graphviz 및 graphviz로 내 보낸 완전한 trie의 다이어그램입니다 twopi
.
시도 할 수있는 한 가지는 단어 경계를 인코딩하기 위해 문장을 사전 처리하는 것입니다. 기본적으로 단어 경계를 분할하여 각 문장을 단어 목록으로 바꿉니다.
문장을 처리하려면 각 단어를 단계별로 살펴보고 일치하는 단어인지 확인하면됩니다.
현재 정규 표현식 검색은 매번 전체 문자열을 다시 거쳐 단어 경계를 찾은 다음 다음 작업 전에이 작업의 결과를 "삭제"해야합니다.
테스트 세트가 포함 된 빠르고 쉬운 솔루션이 있습니다.
우승 전략 :
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 )
아마도 파이썬은 여기서 올바른 도구가 아닙니다. 여기에 유닉스 툴체인이 있습니다
sed G file |
tr ' ' '\n' |
grep -vf blacklist |
awk -v RS= -v OFS=' ' '{$1=$1}1'
블랙리스트 파일이 단어 경계가 추가되어 사전 처리되었다고 가정합니다. 단계는 파일을 이중 간격으로 변환하고 각 문장을 한 줄에 한 단어로 나누고 파일에서 블랙리스트 단어를 대량 삭제 한 다음 다시 병합합니다.
이것은 최소한 10 배 빠르게 실행되어야합니다.
단어에서 블랙리스트 파일을 전처리하는 경우 (한 줄에 한 단어)
sed 's/.*/\\b&\\b/' words > blacklist
이건 어때요:
#!/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)
이 솔루션은 단어 경계로 분할되어 각 단어를 한 세트로 찾습니다. 정규식 대체를 사용하면 정규식 엔진이 단어 일치를 확인 해야하는 반면 이러한 솔루션은 O(n)
n이 amortized O(1)
설정된 조회 로 인해 입력의 크기이므로 여기에서는 대체 단어 (Reyesyes 솔루션)의 재 대체보다 빠릅니다. 단어 경계가 아닌 모든 문자에 적용됩니다. 내 솔루션은 원본 텍스트에 사용 된 공백을 보존하기 위해 특히주의를 기울입니다 (즉, 공백을 압축하지 않고 탭, 줄 바꿈 및 기타 공백 문자를 보존합니다). 출력에서 그것들을 제거하는 것은 매우 간단해야합니다.
Gutenberg Project에서 다운로드 한 여러 eBook을 연결 한 corpus.txt를 테스트했으며 banned_words.txt는 우분투의 단어 목록 (/ 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와 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 )입니다.
btw는 멀티 스레딩을 사용하지 않습니다. GIL은 작업이 엄격하게 CPU에 바인딩되어 있기 때문에 각 작업을 차단하므로 GIL을 해제 할 기회는 없지만 각 스레드는 틱을 동시에 보내 추가적인 노력을 유발하고 작업을 무한대로 이끌 수 있습니다.
모든 문장을 하나의 문서로 연결하십시오. 모든 "나쁜"단어를 찾으 려면 Aho-Corasick 알고리즘 ( 여기서는 하나 )을 구현 하십시오 . 파일을 탐색하여 각각의 잘못된 단어를 바꾸고 뒤 따르는 찾은 단어의 오프셋을 업데이트합니다.
참고 URL : https://stackoverflow.com/questions/42742810/speed-up-millions-of-regex-replacements-in-python-3
'IT박스' 카테고리의 다른 글
토큰 인증 및 쿠키 (0) | 2020.07.26 |
---|---|
jQuery UI 대화 상자 위치 (0) | 2020.07.26 |
안드로이드에서 벨소리 / 알람 사운드를 재생하는 방법 (0) | 2020.07.26 |
Vim : 비주얼 블록 모드에서 삽입하는 방법? (0) | 2020.07.25 |
HTTPS / SSL을 통한 Java 클라이언트 인증서 (0) | 2020.07.25 |