Python 3で数百万の正規表現置換を高速化


127

Python 3.5.2を使用しています

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

このネストされたループは1秒あたり50文を処理します。これはすばらしいことですが、すべての文を処理するにはまだ数時間かかります。

  • このstr.replace方法を使用する方法はありますか(私はより速いと思います)、それでも置換が単語の境界でのみ発生することを要求しますか?

  • または、re.subメソッドを高速化する方法はありますか?re.sub私の単語の長さが私の文の長さよりも長い場合はスキップして、速度をわずかに改善しましたが、それほど改善されていません。

ご提案ありがとうございます。


1
ここでの最初の答えはいくつかの良いサンプルコードです:stackoverflow.com/questions/2846653/…文の配列をCPUコアの数で除算し、その数のスレッドを実行しました
Mohammad Ali

4
また、非正規表現の実装を試すこともできます-入力ワードを単語単位でトラバースし、すべてをセットと照合します。これはシングルパスであり、ハッシュルックアップはかなり高速です。
pvg 2017年

2
ちなみにこれらの文章はどれくらいの長さですか?750k行は、処理に数時間かかるはずのデータセットのようには聞こえません。
pvg 2017年

2
@MohammadAli:CPUにバインドされた作業のその例を気にしないでください。Pythonには、バイトコード(グローバルインタープリターロック)を実行するときにかかる大きなロックがあるため、CPU作業のスレッドから利益を得ることができません。使用する必要がありますmultiprocessing(つまり、複数のPythonプロセス)。
ケビン

1
これを行うには、産業用強度ツールが必要です。正規表現トライは、文字列のリストの三分木から生成されます。失敗するまでのステップは5つを超えることはなく、このタイプのマッチングを実行する最も速い方法です。例:175,000語の辞書または禁止リストと同様の20,000 S語
x15

回答:


123

試すことができる1つのことは、のような1つのパターンをコンパイルすることです"\b(word1|word2|word3)\b"

re実際のマッチングはCコードに依存しているため、大幅な節約が可能です。

@pvgがコメントで指摘したように、シングルパスマッチングの利点もあります。

あなたの言葉が正規表現でない場合、エリックの答えはより速くなります。


4
これはCの実装だけではなく(大きな違いになります)、単一のパスと照合することもできます。この質問の変形はかなり頻繁に出てきます、これはかなり賢明なアイデアでそれに対する正規のSOの答えがない(または、どこかにあるのでしょうか?)とは少し奇妙です。
pvg

40
@Liteyeあなたの提案は、4時間の仕事を4分の仕事に変えました!私は20,000以上の正規表現すべてを1つの巨大な正規表現に結合することができ、私のラップトップは目をそらしませんでした。再度、感謝します。
pdanese 2017年

2
@ばくりう:s/They actually use/They actually could in theory sometimes use/。Pythonの実装がここでループ以外のことをしていると信じる理由はありますか?
user541686

2
@Bakuriu:それが事実であるかどうかを知りたいので本当に興味がありますが、正規表現の解決には線形の時間がかかるとは思いません。それが組合からトライを構築しないならば、私はそれがどのように起こり得るかわかりません。
Eric Duminil 2017年

2
@Bakuriu:それは理由ではありません。私は、実装が実際にそのよう動作すると信じる理由があるかどうかを尋ねました。実装がそのよう動作する可能性があると信じる理由があるかどうかではありません。個人的には、古典的な正規表現を期待するのと同じように線形時間で機能する単一の主流プログラミング言語の正規表現実装にまだ出会っていないので、Pythonがこれを行うことを知っている場合は、いくつかの証拠を示す必要があります。
user541686 2017年

123

TLDR

最速のソリューションが必要な場合は、このメソッドを(set lookupを使用して)使用します。OPに似たデータセットの場合、受け入れられた回答よりも約2000倍高速です。

検索に正規表現を使用する場合は、このトライベースのバージョンを使用してください。これは、正規表現の和集合よりも1000倍高速です。

理論

あなたの文章が巨大な文字列でない場合は、1秒あたり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()
  • 単語を置き換えると、""(コードのように)2つのスペースが残る場合があります
  • python3では、\w+アクセント付き文字(例:)にも一致し"ångström"ます。
  • 単語以外の文字(タブ、スペース、改行、マークなど)はそのまま残ります。

パフォーマンス

100万の文章があり、banned_wordsほぼ100,000の単語があり、スクリプトは7秒未満で実行されます。

比較すると、Liteyeの回答では1万文に160 秒必要でした。

n言葉の合計amoundとされてm禁止された単語の量、OPのとLiteyeのコードがありますO(n*m)

比較すると、私のコードはで実行されるはずO(n+m)です。禁止されている単語よりも多くの文があることを考えると、アルゴリズムはになりO(n)ます。

正規表現の結合テスト

と正規表現検索の複雑さは何ですか '\b(word1|word2|...|wordN)\b'パターンですか?それはありますO(N)O(1)

正規表現エンジンの動作を把握するのはかなり難しいので、簡単なテストを作成しましょう。

このコードは、10**iランダムな英語の単語をリストに抽出します。対応する正規表現共用体を作成し、別の単語でテストします。

  • 1つは明らかに単語ではない(それはで始まる#
  • 1つはリストの最初の単語です
  • 1つはリストの最後の単語です
  • 単語のように見えますが、そうではありません


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文/秒が遅いというコメントは、簡単な例を挙げているだけです。実際のデータセットは、私が説明しているよりも複雑ですが、関連性があるとは思われませんでした。また、私の「単語」を1つの正規表現に連結すると、速度が大幅に向上しました。また、私は置換後にダブルスペースを「絞り出し」ています。
pdanese 2017年

1
@ user36476フィードバックありがとうございます、対応する部分を削除しました。私の提案を試していただけませんか?私はあえてそれが受け入れられた答えよりもはるかに速いと言います。
エリックドゥミニル

1
その誤解を招くO(1)主張を削除したので、あなたの答えは間違いなく賛成票に値します。
idmean 2017年

1
@idmean:確かに、それはあまり明確ではありませんでした。それは単にルックアップを参照していました:「この単語は禁止された単語ですか?」。
Eric Duminil 2017年

1
@EricDuminil:よくできました!私がもう一度賛成できることを望みます。
Matthieu M.17年

105

TLDR

最速の正規表現ベースのソリューションが必要な場合は、この方法を使用してください。OPに似たデータセットの場合、受け入れられた回答よりも約1000倍高速です。

正規表現を気にしない場合は、このセットベースのバージョンを使用してください。これは、正規表現の和集合より2000倍高速です。

Trieを使用した最適化された正規表現

簡単な正規表現組合正規表現エンジンがためのアプローチは、多くの禁止用語で遅くなり、非常に良い仕事しませんパターンを最適化します。

トライを作成することは可能です禁止されているすべての単語で、対応する正規表現を書く。結果として得られるトライまたは正規表現は実際には人間が読める形式ではありませんが、非常に高速な検索と照合が可能です。

['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ワードの前処理過剰ですが、数千ワードの有望な結果を示しています。

次の理由により、(?:)非キャプチャグループが使用されることに注意してください。

コード

ライブラリとして使用できる、わずかに変更された要点を次に示し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())

テスト

ここでは、小さなテストでは、(同じだ、この1):

# 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))?| 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( ?:\ '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 .. 。

読みにくいですが、禁止されている10万語のリストの場合、このTrie正規表現は単純な正規表現の和集合よりも1000倍高速です。

以下は、trie-python-graphvizおよびgraphviz でエクスポートされた完全なトライの図ですtwopi

ここに画像の説明を入力してください


本来の目的では、非捕獲グループは必要ないようです。少なくとも非捕獲グループの意味に
言及する

3
@XavierCombelle:私は捕獲グループについて言及すべきだとあなたは正しい:答えは更新された。私はそれを逆に見ます:正規表現の代替には括弧が必要です|が、キャプチャーグループは私たちの目的にはまったく必要ありません。彼らはただプロセスを遅くして、利益なしでより多くのメモリを使います。
Eric Duminil 2017年

3
@EricDuminilこの投稿は完璧です。どうもありがとうございました:)
Mohamed AL ANI

1
@MohamedALANI:どのソリューションと比較して?
エリックドゥミニル

1
@ PV8:それは唯一の完全な言葉を一致させる必要があり、はい、おかげで\bワード境界)。リストがの場合は['apple', 'banana']、正確にappleor bananaであるがnanabanaorではない単語を置き換えます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 2017年

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

おそらく、Pythonはここでは適切なツールではありません。これは、Unixツールチェーンの1つです。

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

ブラックリストファイルが単語の境界を追加して前処理されていると仮定します。手順は次のとおりです。ファイルをダブルスペースに変換し、各文を1行あたり1ワードに分割し、ファイルからブラックリストワードを一括削除し、行をマージします。

これは、少なくとも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)

これらのソリューションは、単語の境界で分割し、セット内の各単語を検索します。これらの解決策はO(n)nが入力のサイズであるので、これらは単語の代替(Liteyesの解決策)のre.subよりも高速である必要があります。amortized O(1)設定された検索正規表現の代替を使用すると、正規表現エンジンが単語の一致をチェックする必要があるためです。単語の境界だけでなく、すべての文字について。私の解決策は、元のテキストで使用されていた空白を保持するように細心の注意を払っています(つまり、空白を圧縮せず、タブ、改行、およびその他の空白文字を保持します)が、気にならない場合は、それらを出力から削除するのはかなり簡単なはずです。

私は、Gutenbergプロジェクトからダウンロードされた複数のeBookを連結したcorpus.txtでテストしました。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は2番目のアプローチからより多くの利益を得ますが、CPythonは最初のアプローチよりもうまくいきました。上記のコードはPython 2と3の両方で動作するはずです。


質問ではPython 3が指定されています。私はこれに賛成しましたが、このコードの詳細と「最適な」実装の一部を犠牲にして、冗長性を少なくすることは価値があると思います。
pvg

私がそれを正しく理解すれば、それは基本的に私の答えと同じ原則ですが、より冗長ですか?分割との参加は\W+基本的に似ているsub\w+、右?
Eric Duminil 2017年

以下の私の解決策(関数replace4)はpypyよりも速いのでしょうか;)私はあなたのファイルでテストしたいと思います!
bobflux 2017年

3

実用的なアプローチ

以下で説明するソリューションは、大量のメモリを使用してすべてのテキストを同じ文字列に格納し、複雑さのレベルを減らします。RAMに問題がある場合は、使用する前によく考えてください。

ではjoin/ splitトリックあなたはアルゴリズムをスピードアップする必要のあるすべてのループを避けることができます。

  • 文章に含まれていない特別な区切り文字で文章を連結します。
  • merged_sentences = ' * '.join(sentences)

  • |"or"正規表現ステートメントを使用して、文から取り除く必要があるすべての単語に対して単一の正規表現をコンパイルします。
  • 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)に対して線形の複雑さです。


    ところでマルチスレッドを使用しないでください。タスクが厳密にCPUにバインドされているためGILが解放される機会がないため、GILは各操作をブロックしますが、各スレッドはティックを同時に送信し、余分な労力を引き起こし、操作を無限に導きます。


    センテンスがテキストファイルに保存されている場合は、改行で区切られています。そのため、ファイル全体を1つの大きな文字列(またはバッファー)として読み取り、単語を削除してから、再度書き込むことができます(または、これは、メモリマッピングを使用してファイルで直接行うことができます)。音、単語を削除するには、残りの文字列を移動してギャップを埋める必要があるため、1つの非常に大きな文字列では問題になります。別の方法は、単語間の部分を別の文字列またはファイル(改行を含む)に書き戻すか、または
    mmapped

    ..この最後のアプローチ(単語間の部分の移動/書き込み)とEric Duminilのセットルックアップを組み合わせると、おそらく正規表現をまったく使用しなくても、本当に高速になる可能性があります。(2)
    Danny_ds 2017年

    ..または、正規表現はすでに、複数の単語を置き換えるときにそれらの部分を移動するように最適化されているだけかもしれません。
    Danny_ds 2017年

    0

    すべての文を1つのドキュメントに連結します。Aho-Corasickアルゴリズムの任意の実装(ここでは1つ)を使用して、すべての「悪い」単語を見つけます。ファイルをトラバースし、各悪い単語を置き換え、見つかった単語のオフセットを更新します。

    弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
    Licensed under cc by-sa 3.0 with attribution required.