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 바운드 작업에 대한 예제를 귀찮게하지 마십시오. 파이썬은 바이트 코드 (글로벌 인터프리터 잠금)를 실행할 때 걸리는 큰 잠금을 가지고 있으므로 CPU 작업에 대한 스레드의 이점을 누릴 수 없습니다. 당신은 multiprocessing(즉, 여러 파이썬 프로세스) 를 사용해야 합니다.
Kevin

1
이를 위해서는 산업 강도 도구 가 필요합니다. 정규식 트리는 문자열 목록의 3 진 트리에서 생성됩니다. 이 유형의 일치를 수행하는 가장 빠른 방법으로 만들려면 실패 할 단계가 5 단계를 넘지 않아야합니다. 예 : 175,000 단어 사전 또는 금지 된 목록과 비슷한 20,000 개의 S 단어
x15

답변:


123

시도 할 수있는 한 가지는 같은 단일 패턴을 컴파일하는 것 "\b(word1|word2|word3)\b"입니다.

re실제 매칭을 수행하기 위해 C 코드를 사용 하기 때문에 절감 효과가 크게 향상 될 수 있습니다.

주석에서 @pvg가 지적했듯이 단일 패스 일치의 이점도 있습니다.

당신의 말이 정규 표현식이 아니면 Eric의 답변 이 더 빠릅니다.


4
그것은 C impl (큰 차이를 만드는 것) 일뿐 만 아니라 단일 패스와도 일치합니다. 이 질문의 변형은 꽤 자주 나타납니다.이 합리적 인 아이디어로 정식 SO 답변이없는 (또는 어딘가에 숨어있을 수도 있습니까) 조금 이상합니다.
pvg

40
@Liteye 제안은 4 시간 작업을 4 분 작업으로 바꿨습니다! 20,000 개 이상의 정규식을 하나의 거대한 정규식에 가입시킬 수 있었고 랩톱은 눈을 bat 지 않았습니다. 다시 감사합니다.
pdanese

2
@Bakuriu : s/They actually use/They actually could in theory sometimes use/. 파이썬의 구현이 루프 이외의 일을하고 있다고 믿을만한 이유가 있습니까?
user541686

2
@ Bakuriu : 나는 그것이 사실인지 알고 정말로 관심이 있지만 정규식 솔루션에 선형 시간이 걸리지 않는다고 생각합니다. 그것이 노조에서 Trie를 만들지 않으면 어떻게 될 수 있는지 알 수 없습니다.
Eric Duminil

2
@Bakuriu : 이유가 아닙니다. 구현이 실제로 그렇게 행동 한다고 믿을만한 이유가 있는지, 당신이 그렇게 행동 할 있다고 믿을 이유가 있는지 묻지 않았습니다 . 개인적으로 나는 고전적인 정규 표현식과 동일한 방식으로 선형 시간으로 작동하는 단일 주류 프로그래밍 언어의 정규 표현식 구현을 아직 보지 못했습니다. 따라서 파이썬이 이것을 알고 있다면 몇 가지 증거를 제시해야합니다.
user541686

123

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) 최악의 경우

이 결과는 간단한 루프 검색과 일치합니다.

정규식 조합에 대한 훨씬 빠른 대안 은 트라이에서 정규식 패턴 을 만드는 것 입니다.


1
당신이 맞았습니다. 들여 쓰기가 잘못되었습니다. 나는 원래 질문으로 고쳤습니다. 50 문장 / 초가 느리다는 의견은 간단한 예를 제공한다는 것입니다. 실제 데이터 세트는 내가 설명하는 것보다 더 복잡하지만 관련성이 없어 보입니다. 또한 내 "단어"를 단일 정규식으로 연결하면 속도가 크게 향상되었습니다. 또한 교체 후 이중 공간을 "압착하고 있습니다".
pdanese

1
@ user36476 의견을 보내 주셔서 감사합니다. 해당 부분을 제거했습니다. 내 제안을 시도해 주시겠습니까? 나는 그것이 대답보다 훨씬 빠르다고 감히 말했다.
Eric Duminil

1
오해의 소지가있는 O(1)주장 을 삭제 했으므로 귀하의 답변은 확실히 투표권이 있습니다.
idmean

1
@idmean : 사실, 그다지 명확하지 않았습니다. "이 단어는 금지 된 단어입니까?"
Eric Duminil

1
@EricDuminil : 훌륭합니다! 두 번째로 투표 할 수 있으면 좋겠다.
Matthieu M.

105

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.

여기에 이미지 설명을 입력하십시오


원래 목적을 위해 캡처하지 않은 그룹이 필요하지 않습니다. 적어도 비 포획 그룹의 의미를 언급해야합니다
Xavier Combelle

3
@XavierCombelle : 캡처 그룹에 대해 언급해야합니다. 답변이 업데이트되었습니다. 그래도 다른 방법으로 볼 수 있습니다 : 정규식 대체에는 parens가 필요 |하지만 캡처 그룹은 우리의 목적에 전혀 필요하지 않습니다. 그들은 프로세스 속도를 늦추고 아무런 이점없이 더 많은 메모리를 사용합니다.
Eric Duminil

3
@EricDuminil이 게시물은 완벽합니다. 정말 감사합니다 :)
Mohamed AL ANI

1
@MohamedALANI : 어떤 솔루션에 비해?
Eric Duminil

1
@ PV8 : \b( 단어 경계 ) 덕분에 완전한 단어와 일치해야합니다 . 목록이 경우 ['apple', 'banana'], 정확히 단어 대체합니다 apple또는 banana아니지만 nana, bana또는 pineapple.
Eric Duminil

15

시도 할 수있는 한 가지는 단어 경계를 인코딩하기 위해 문장을 사전 처리하는 것입니다. 기본적으로 단어 경계를 분할하여 각 문장을 단어 목록으로 바꿉니다.

문장을 처리하려면 각 단어를 단계별로 살펴보고 일치하는 단어인지 확인하면됩니다.

현재 정규 표현식 검색은 매번 전체 문자열을 다시 거쳐 단어 경계를 찾은 다음 다음 작업 전에이 작업의 결과를 "삭제"해야합니다.


8

테스트 세트가 포함 된 빠르고 쉬운 솔루션이 있습니다.

우승 전략 :

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)

1
시험에 대한 찬성 replace4내 코드는 비슷한 성능을 가지고 있습니다.
Eric Duminil

def repl(m):가 무엇 을하고 있는지 m, replace4 함수에서 어떻게 할당하는지 확실하지 않습니다
StatguyUser

또한 나는 error: unbalanced parenthesis라인에 대한 오류가 발생 합니다patterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
StatguyUser

replace3 및 replace4 함수는 단어를 대체하기 위해 원래의 문제를 해결하는 반면, replace1 및 replace2는 단일 단어가 아니라 바늘 (구문 시퀀스) 인 경우에도 작동하므로보다 일반적인 용도입니다.
Zoltan Fedor

7

아마도 파이썬은 여기서 올바른 도구가 아닙니다. 여기에 유닉스 툴체인이 있습니다

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

블랙리스트 파일이 단어 경계가 추가되어 사전 처리되었다고 가정합니다. 단계는 파일을 이중 간격으로 변환하고 각 문장을 한 줄에 한 단어로 나누고 파일에서 블랙리스트 단어를 대량 삭제 한 다음 다시 병합합니다.

이것은 최소한 10 배 빠르게 실행되어야합니다.

단어에서 블랙리스트 파일을 전처리하는 경우 (한 줄에 한 단어)

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)

이 솔루션은 단어 경계로 분할되어 각 단어를 한 세트로 찾습니다. 정규식 대체를 사용하면 정규식 엔진이 단어 일치를 확인 해야하는 반면 이러한 솔루션은 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 모두에서 작동해야합니다.


파이썬 3은 질문에 주어진 것입니다. 나는 이것을 찬성했지만 덜 자세하게하기 위해이 코드에서 세부 사항과 '최적의'구현을 희생 할 가치가 있다고 생각합니다.
pvg

올바르게 이해하면 기본적으로 내 대답과 동일한 원칙이지만 더 자세한 정보입니까? 분할 및 가입 \W+은 기본적으로 subon 과 같습니다 \w+.
Eric Duminil

아래의 솔루션 (function replace4)이 pypy보다 빠르면 궁금합니다.) 파일을 테스트하고 싶습니다!
bobflux 2015 년

3

실용적인 접근

아래 설명 된 솔루션은 많은 텍스트를 사용하여 모든 텍스트를 동일한 문자열로 저장하고 복잡성을 줄입니다. RAM이 문제라면 사용하기 전에 두 번 생각하십시오.

join/ split트릭을 사용 하면 알고리즘 속도를 높이는 루프를 피할 수 있습니다.

  • 문장에 포함되지 않은 특수 delimeter로 문장을 연결하십시오.
  • 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/splitO (words) + 2 * O (sentences) 는 초기 접근 방식의 선형 복잡성 대 2 * O (N 2 )입니다.


    btw는 멀티 스레딩을 사용하지 않습니다. GIL은 작업이 엄격하게 CPU에 바인딩되어 있기 때문에 각 작업을 차단하므로 GIL을 해제 할 기회는 없지만 각 스레드는 틱을 동시에 보내 추가적인 노력을 유발하고 작업을 무한대로 이끌 수 있습니다.


    문장이 텍스트 파일에 저장된 경우 이미 줄 바꿈으로 구분됩니다. 따라서 전체 파일을 하나의 큰 문자열 (또는 버퍼)로 읽고 단어를 제거한 다음 다시 다시 쓸 수 있습니다 (또는 메모리 매핑을 사용하여 파일에서 직접 수행 할 수 있음). Otoh, 단어를 제거하려면 틈을 메우기 위해 줄의 나머지 부분을 다시 이동해야하므로 매우 큰 줄로 인해 문제가 될 수 있습니다. 대안은 단어 사이의 부분을 다른 문자열이나 파일에 다시 쓰거나 (줄 바꿈 포함) – 그 부분을
    mmapped

    .. Eric Duminil의 설정 조회 와 결합 된 마지막 접근 방식 (단어 사이의 부분 이동 / 쓰기)은 정규식을 전혀 사용하지 않고도 실제로 빠를 수 있습니다. (2)
    Danny_ds

    또는 여러 단어를 바꿀 때 정규식이 해당 부분 만 이동하도록 이미 최적화되어있을 수도 있습니다.
    Danny_ds

    0

    모든 문장을 하나의 문서로 연결하십시오. 모든 "나쁜"단어를 찾으 려면 Aho-Corasick 알고리즘 ( 여기서는 하나 )을 구현 하십시오 . 파일을 탐색하여 각각의 잘못된 단어를 바꾸고 뒤 따르는 찾은 단어의 오프셋을 업데이트합니다.

    당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
    Licensed under cc by-sa 3.0 with attribution required.