Python 3'te milyonlarca normal ifade değişimini hızlandırın


127

Python 3.5.2 kullanıyorum

İki listem var

  • yaklaşık 750.000 "cümleden" oluşan bir liste (uzun dizeler)
  • 750.000 cümleimden silmek istediğim yaklaşık 20.000 "kelimeden" oluşan bir liste

Bu yüzden, 750.000 cümle arasında döngü yapmalı ve yaklaşık 20.000 değiştirme yapmalıyım, ancak YALNIZCA kelimelerim aslında "kelimeler" ise ve daha büyük bir karakter dizisinin parçası değilse.

Bunu , meta karakter tarafından çevrelenmeleri için kelimelerimi önceden derleyerek yapıyorum.\b

compiled_words = [re.compile(r'\b' + word + r'\b') for word in my20000words]

Sonra "cümlelerimi" gözden geçiriyorum

import re

for sentence in sentences:
  for word in compiled_words:
    sentence = re.sub(word, "", sentence)
  # put sentence into a growing list

Bu iç içe döngü saniyede yaklaşık 50 cümle işliyor , bu güzel bir şey, ancak yine de tüm cümlelerimi işlemek birkaç saat sürüyor.

  • str.replaceYöntemi kullanmanın bir yolu var mı (daha hızlı olduğuna inanıyorum), ancak yine de değiştirmelerin yalnızca kelime sınırlarında olmasını gerektiriyor mu?

  • Alternatif olarak, re.subyöntemi hızlandırmanın bir yolu var mı? Kelimemin re.subuzunluğu cümlenin uzunluğundan> daha fazlaysa atlayarak hızı marjinal olarak iyileştirdim , ama bu pek bir gelişme değil.

Önerileriniz için teşekkürler.


1
Buradaki ilk cevabın bazı iyi örnek kodları var: stackoverflow.com/questions/2846653/… cümle dizinizi, sahip olduğunuz CPU çekirdeği sayısına bölün ve ardından bu kadar çok iş parçacığı çalıştırın
Mohammad Ali

4
Ayrıca normal ifade olmayan bir uygulamayı da deneyebilirsiniz - girişinizi kelime kelime çaprazlayın ve her birini bir küme ile eşleştirin. Bu tek geçiş ve hash aramaları oldukça hızlı.
pvg

2
Bu cümleler tesadüfen ne kadar uzun? 750 bin satır, işlenmesi saatler sürmesi gereken bir veri kümesi gibi görünmüyor.
pvg

2
@MohammadAli: CPU'ya bağlı çalışma için bu örnekle uğraşmayın. Python, bayt kodunu (Global Yorumlayıcı Kilidi) çalıştırırken aldığı büyük bir kilide sahiptir, bu nedenle CPU çalışması için iş parçacıklarından yararlanamazsınız. Kullanmanız gerekir multiprocessing(yani birden çok Python işlemi).
Kevin

1
Bunu yapmak için bir endüstriyel güç aracına ihtiyacınız var . Bir dizge listesinin üçlü bir ağacından bir normal ifade üçlüsü oluşturulur. Başarısızlığın 5 adımdan fazla olmaması, bunu bu tür eşleştirmeyi yapmak için en hızlı yöntem haline getirir. Örnekler: 175.000 kelimelik sözlük veya yasaklanmış listenize benzer sadece 20.000 S kelimesi
x15

Yanıtlar:


123

Deneyebileceğiniz bir şey, tek bir kalıbı derlemektir "\b(word1|word2|word3)\b".

Çünkü regerçek eşleştirme yapmak C kodunu kullanır, tasarruf dramatik olabilir.

@Pvg yorumlarda da belirttiği gibi tek geçiş eşleştirmesinden de yararlanıyor.

Sözleriniz normal ifade değilse, Eric'in cevabı daha hızlıdır.


4
Bu sadece C impl (büyük bir fark yaratan) değil, aynı zamanda tek bir geçişle de eşleştiriyorsunuz. Bu sorunun varyantları oldukça sık ortaya çıkıyor, bu oldukça mantıklı fikirle bunun için kanonik bir SO cevabı olmaması (veya belki bir yerde saklanması var mı?) Biraz tuhaf.
pvg

40
@Liteye öneriniz 4 saatlik bir işi 4 dakikalık bir işe dönüştürdü! 20.000'den fazla regex'i tek bir devasa regex'te birleştirebildim ve dizüstü bilgisayarım gözünü kırpmadı. Tekrar teşekkürler.
pdanca

2
@Bakuriu: s/They actually use/They actually could in theory sometimes use/. Python uygulamasının burada bir döngüden başka bir şey yaptığına inanmak için herhangi bir nedeniniz var mı?
user541686

2
@Bakuriu: Durumun bu olup olmadığını gerçekten merak ediyorum, ancak normal ifade çözümünün doğrusal zaman aldığını düşünmüyorum. Sendikadan bir Trie inşa etmezse, nasıl olabileceğini anlamıyorum.
Eric Duminil

2
@Bakuriu: Bu bir sebep değil. Eğer uygulama inanmak için bir neden varsa soruyordu aslında bunu inanmak için bir neden var olmasın, bu şekilde davranır olabilir bu şekilde davranırlar. Şahsen ben, klasik bir normal ifadeyi beklediğiniz gibi doğrusal zamanda çalışan tek bir ana akım programlama dilinin düzenli ifade uygulamasına henüz rastlamadım, bu yüzden Python'un bunu yaptığını biliyorsanız, bazı kanıtlar göstermelisiniz.
user541686

123

TLDR

En hızlı çözümü istiyorsanız bu yöntemi (set aramalı) kullanın. OP'lere benzer bir veri kümesi için, kabul edilen cevaptan yaklaşık 2000 kat daha hızlıdır.

Arama için bir normal ifade kullanmakta ısrar ediyorsanız , hala bir normal ifade birleşiminden 1000 kat daha hızlı olan bu üçlü tabanlı sürümü kullanın .

teori

Cümleleriniz çok büyük dizeler değilse, saniyede 50'den fazlasını işlemek muhtemelen mümkündür.

Tüm yasaklanmış kelimeleri bir sete kaydederseniz, o sette başka bir kelimenin bulunup bulunmadığını kontrol etmek çok hızlı olacaktır.

Mantığı bir işleve paketleyin, bu işlevi bağımsız değişken olarak verin re.subve bitirdiniz!

kod

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)

Dönüştürülen cümleler:

' .  !
  .
GiraffeElephantBoat
sfgsdg sdwerha aswertwe

Bunu not et:

  • arama büyük / küçük harfe duyarlı değildir (sayesinde lower())
  • bir kelimeyi değiştirmek ""iki boşluk bırakabilir (kodunuzda olduğu gibi)
  • Python3 ile \w+ayrıca aksanlı karakterlerle eşleşir (örneğin "ångström").
  • Sözcük olmayan herhangi bir karakter (sekme, boşluk, satırsonu, işaretler, ...) dokunulmadan kalacaktır.

Verim

Bir milyon cümle var, banned_wordsneredeyse 100000 kelime var ve senaryo 7 saniyeden daha kısa sürede çalışıyor.

Buna karşılık, Liteye'nin cevabı 10 bin cümle için 160'lara ihtiyaç duyuyordu.

İle nkelimelerin toplam yıkama çözeltisi ve varlık myasaklı kelimelerin miktarı, OP adlı ve Liteye kodu vardır O(n*m).

Buna karşılık, kodum çalışmalı O(n+m). Yasaklı sözcüklerden çok daha fazla cümle olduğu düşünüldüğünde algoritma olur O(n).

Normal ifade birleşim testi

Bir '\b(word1|word2|...|wordN)\b'kalıpla normal ifade aramasının karmaşıklığı nedir ? Öyle mi O(N)yoksa O(1)?

Normal ifade motorunun çalışma şeklini kavramak oldukça zor, bu yüzden basit bir test yazalım.

Bu kod, 10**irastgele İngilizce kelimeleri bir listeye çıkarır . Karşılık gelen normal ifade birleşimini oluşturur ve farklı kelimelerle test eder:

  • açıkça bir kelime değil (ile başlar #)
  • listedeki ilk kelime
  • listedeki son kelime
  • bir kelime gibi görünüyor ama değil


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))

Çıktıları:

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

Öyleyse, bir '\b(word1|word2|...|wordN)\b'kalıba sahip tek bir kelime için yapılan arama şuna sahip gibi görünüyor :

  • O(1) en iyi senaryo
  • O(n/2) ortalama durum, ki hala O(n)
  • O(n) En kötü durumda

Bu sonuçlar, basit bir döngü aramasıyla tutarlıdır.

Bir normal ifade birleşimine çok daha hızlı bir alternatif, bir üçlüden normal ifade kalıbı oluşturmaktır .


1
Haklıydın Girintim yanlıştı. Orijinal soruda düzelttim. 50 cümle / saniyenin yavaş olduğu yorumuna gelince, söyleyebileceğim tek şey basitleştirilmiş bir örnek veriyorum. Gerçek veri seti anlattığımdan daha karmaşık, ancak alakalı görünmüyordu. Ayrıca, "kelimelerimin" tek bir normal ifadede birleştirilmesi hızı büyük ölçüde artırdı. Ayrıca, değiştirmelerden sonra çift boşlukları "sıkıştırıyorum".
pdanca

1
@ user36476 Geri bildirim için teşekkürler, ilgili kısmı kaldırdım. Önerimi deneyebilir misin lütfen? Kabul edilen cevaptan çok daha hızlı olduğunu söyleyebilirim.
Eric Duminil

1
Bu yanıltıcı O(1)iddiayı kaldırdığınıza göre , cevabınız kesinlikle olumlu bir oylamayı hak ediyor.
idmean

1
@idmean: Doğru, bu çok net değildi. Sadece aramaya atıfta bulunuyordu: "Bu kelime yasaklanmış bir kelime mi?".
Eric Duminil

1
@EricDuminil: Harika iş! Keşke ikinci kez yükseltebilseydim.
Matthieu M.

105

TLDR

En hızlı regex tabanlı çözümü istiyorsanız bu yöntemi kullanın. OP'lere benzer bir veri kümesi için, kabul edilen cevaptan yaklaşık 1000 kat daha hızlıdır.

Normal ifadeyi önemsemiyorsanız, bir normal ifade birleşiminden 2000 kat daha hızlı olan bu set tabanlı sürümü kullanın .

Trie ile Optimize Edilmiş Normal İfade

Bir basit Regex birlik regex motoru çünkü yaklaşım, birçok yasaklı kelime ile yavaş olur çok iyi bir iş yapmaz deseni optimize.

Tüm yasaklanmış kelimelerle bir Trie oluşturmak ve ilgili normal ifadeyi yazmak mümkündür. Ortaya çıkan trie veya regex gerçekten insan tarafından okunabilir değildir, ancak çok hızlı arama ve eşleşmeye izin verirler.

Misal

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

Normal ifade birliği

Liste bir trie'ye dönüştürülür:

{
    'f': {
        'o': {
            'o': {
                'x': {
                    'a': {
                        'r': {
                            '': 1
                        }
                    }
                },
                'b': {
                    'a': {
                        'r': {
                            '': 1
                        },
                        'h': {
                            '': 1
                        }
                    }
                },
                'z': {
                    'a': {
                        '': 1,
                        'p': {
                            '': 1
                        }
                    }
                }
            }
        }
    }
}

Ve sonra bu normal ifade kalıbına:

r"\bfoo(?:ba[hr]|xar|zap?)\b"

Normal ifade trie

En büyük avantajı, zooeşleşip eşleşmediğini test etmek için , normal ifade motorunun 5 kelimeyi denemek yerine yalnızca ilk karakteri (eşleşmiyor) karşılaştırması gerektiğidir . Bu, 5 kelimelik bir ön işlem aşırılığıdır, ancak binlerce kelime için umut verici sonuçlar verir.

O Not (?:)olmayan yakalama grupları nedeniyle kullanılır:

kod

Kitaplık olarak kullanabileceğimiz, biraz değiştirilmiş bir özettrie.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())

Ölçek

İşte küçük bir test (aynı şey bu bir ):

# 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))

Çıktıları:

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

Bilgi için normal ifade şu şekilde başlar:

(A (: (: \ 'ın | a (:? \'???? S | chen | Liyah (: \ 'ler) | r (?:? Dvark (: (:?? \' In | s )) | üzerinde)) | b? (?: \ 'ın | a (:? c (: us (: (: \??' s | es)) | [ik]) | ft | (yalnız? : (?: \ 'un | s)?) | ndon (?? :( ?: ed | ing | gelebilecektir (: \' ler) |??)) s | s (: e (:( ?:? ment: | [ds])) | h (:( ?: e [ds] |) ing | |)) ing t ((\ 's):??? e (:( ?: ment ( ?: \ 'ler) | [dS])) | ing | toir (?:? (: \?' ın | s)))) | b (?: (gibidir:??? id) | e (? : p ((:? \ 's | ler)) |? y ((: \?' |) | yerine (ler s)): (:? \ 's | t (: \ Var) | s)) | reviat? (: e [ds] | i (:? ng | üzerinde (: (: \? 'nin | s)))) | y (:? \' ? s) | \ e ((:? \ 's | s))) | D (: icat (e [ds] | i (:? ng | üzerinde (:? (: \ 'ler |)) s)) | om (?: tr (: (: \??' s | s)) | inal) | u (?:? ct (:( ?: ed | i (?: ng | üzerinde ((: \? 'nin | s))) | veya B (: (:? \' s | s)??) | s)) | l (: \ 'ler)) ) | e (: (:?? \ 'ın | AM | l (: (:?? \' ın | ard | oğul (?:?? \ 'ler))) | r (?:? deen (: \ 'ler) | nathy? (?: \' ler) | ra (:??? nt | siyon (: (:? \ 'un | s))??)) | t (:( ?: t (?: e (: r ((: \? 'nin | s)) | D?) | ing | ya da (:? (: \'s | s))) | s)) | yance (:? \ 's) | d)) | hor (:( ?: R (:?? e (n (: ce (?? : \ 's) | t) | d) |)) s | |) ing i (:? d (e [ds] | ing | Jan (:? \'? s)) | gail | l (: ene | o (?:? ler | y (: \ 'ın))?) | j | ur ((: vb (ly?):???? tirme (: (: \?)' s | s)) | e [dS] | ing)) | l (:?? a (: tive ((:?? \ 's | s)) | ze) | e (:(? : st | r)) | oom | Katkı (:? (:?? \ '? s | s)) | y) | m \' s | n (: e (: gat (e [ds] ? | i (: ng | üzerine (: \ 's)) | r (: \?)' ler)) | ormal (:( ?: bu (:? ler | y (:? \' ler)) | ly))) | o (?:? ard | de (: (:???? \ 'ın | s)) | li (?: sh (:( ?: e [dS] | ing )) | tion? (:? (: \ 'nin | ist ((: \?' nin | s))))) | mina (:? bl [ey] | t (: e [ dS] | i? (: ng | üzerine (:? (: \ 's |)) s?))) | r (:?? igin (: al ((: \' s | s) ) | e? (:? (: \ '|) s s)) | t (:( ?: ed | i (:? ng | üzerinde (: (:? \' s | ist (?: ) |)) s | ve) |)) s) | u (|: (\ 'ın s?):?????? nd (:( ?: ed | ing | s)) | t) | ettik ((:? \ 's | kurulu))) | r (:? a (: cadabra (: \?' ler) | D (:? e [ds] | (ham |) ing? : \ '? lar) | m (: (:?? \' un | s)?) | si (: üzerinde (: (:??? \ 'ın |) s) |? (:( ettik ?:\ 'In | ly | lık (: \?' |)) S)) | doğu | IDG (ler):??? E (:( ?: gelebilecektir (: (:??? \ 'In | s)) ? | [ds])) | ing | ment (:? (: \ 's | s))) | o (:? reklam | gat (e [ds] | i (:? ng | (??: (: \ üzerinde? '??? | |))) upt (:( ?: e (s) ler):? st | r) | ly | ness (: \' ler)))) | s (?:? Alom | c (: ess (: (: \ 'ın | e [dS] | ing)) | issa (?:? (?: \'??? s | [es])) | ond)) | tr (:( ?: ed | | ing s?)? (?: ce (: (:?? \ 'un | s)???) | t (:( ?: e (: e ( ?: (?: \ 'un | izm (: \?' ler) | s?)) | d) | ing | ly | s))) | inş (:? (?:? \ 'ın | e ( : \ 's))) | o? (:? l (: ut (: e (: (: \?' nin | ly | st)) | i (:? (ilgili ?: \ '? s) | sm (: \'? s))) | v (e [ds] | ing)) | r (:? b (:( ?: e (n (?? : cy (: \? '| t ler)? (: (:?? \' un | s))??) | d) | ing | s)) | pti ...s | [es])) | ond (:( ?: ed | ing | s))) | tr (?:??? ce (: (:??? \ 'ın | s)) | t (?: ? (: e (: e (: (:???? \ '? s | izm (: \'?? lar) | s)) | d) | ing | ly | s))) | inş (?: (?: \ 'un | e (: \?' ler)?)) | o (?:? l (: ut (: e (: (: \ 'un | ly | st))?????? | i (on: (: \ 'ler) | sm (:? \'?? s))) | v (e [ds] | ing)) | r (:?? b (:( : e (n (Cy (: \ 'ler) | t (: (:? \'?? s | s))) | d) |? ing | s)) | pti .. .s | [es])) | ond (:( ?: ed | ing | s))) | tr (?:??? ce (: (:??? \ 'ın | s)) | t (?: ? (: e (: e (: (:???? \ '? s | izm (: \'?? lar) | s)) | d) | ing | ly | s))) | inş (?: (?: \ 'un | e (: \?' ler)?)) | o (?:? l (: ut (: e (: (: \ 'un | ly | st))?????? | i (on: (: \ 'ler) | sm (:? \'?? s))) | v (e [ds] | ing)) | r (:?? b (:( : e (n (Cy (: \ 'ler) | t (: (:? \'?? s | s))) | d) |? ing | s)) | pti .. .

Gerçekten okunamaz, ancak 100000 yasaklanmış kelimeden oluşan bir liste için, bu Trie regex, basit bir regex birleşiminden 1000 kat daha hızlıdır!

İşte trie-python-graphviz ve graphviz ile dışa aktarılan tam trie diyagramı twopi:

Buraya resim açıklamasını girin


Görünüşe göre orijinal amaç için, yakalamayan bir gruba gerek yok. En azından yakalamayan grubun anlamı belirtilmelidir
Xavier Combelle

3
@XavierCombelle: Yakalama grubundan bahsetmem gerektiği konusunda haklısın: cevap güncellendi. Yine de tam tersini görüyorum: normal ifade değişimi için parens gerekli, |ancak bizim amacımız için grupları yakalamak gerekli değil. Sadece süreci yavaşlatır ve fayda olmadan daha fazla hafıza kullanırlar.
Eric Duminil

3
@EricDuminil Bu yazı mükemmel, çok teşekkür ederim :)
Mohamed AL ANI

1
@MohamedALANI: Hangi çözümle karşılaştırıldığında?
Eric Duminil

1
@ PV8: Evet, \b( kelime sınırı ) sayesinde yalnızca tam kelimelerle eşleşmelidir . Liste ise ['apple', 'banana'], tam olarak kelimeleri yerini alacak appleveya bananadeğil nana, banaya pineapple.
Eric Duminil

15

Denemek isteyebileceğiniz bir şey, sözcük sınırlarını kodlamak için cümleleri önceden işlemektir. Temel olarak, kelime sınırlarına göre her cümleyi bir kelime listesine dönüştürün.

Bu daha hızlı olmalı çünkü bir cümleyi işlemek için kelimelerin her birine adım atmalı ve eşleşip eşleşmediğini kontrol etmelisin.

Şu anda normal ifade araması her seferinde tüm dizeyi gözden geçirmek, kelime sınırlarını aramak ve ardından bir sonraki geçişten önce bu çalışmanın sonucunu "atmak" zorundadır.


8

İşte test seti ile hızlı ve kolay bir çözüm.

Kazanma stratejisi:

re.sub ("\ w +", repl, cümle) kelimeleri arar.

"repl" çağrılabilir olabilir. Dikte araması yapan bir işlev kullandım ve dikte, aranacak ve değiştirilecek sözcükleri içeriyor.

Bu, en basit ve en hızlı çözümdür (aşağıdaki örnek kodda işlev replace4'e bakın).

En iyi ikinci

Buradaki fikir, cümleleri daha sonra yeniden yapılandırmak için ayırıcıları korurken, yeniden bölmeyi kullanarak cümleleri kelimelere bölmektir. Ardından, basit bir dikt aramayla değiştirmeler yapılır.

(aşağıdaki örnek kodda işlev replace3'e bakın).

Örnek işlevler için zamanlamalar:

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)

... ve kod:

#! /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 )

Düzenleme: Küçük harfli bir Cümleler listesi geçirip geçirmediğinizi kontrol ederken ve repl'yi düzenlerken de küçük harfleri yoksayabilirsiniz.

def replace4( sentences ):
pd = patterns_dict.get
def repl(m):
    w = m.group()
    return pd(w.lower(),w)

1
Testler için oy verin. replace4ve kodumun benzer performansları var.
Eric Duminil

Def'in repl(m):ne yaptığından ve mişlevde nasıl atadığınızdan emin değilim replace4
StatguyUser

Ayrıca error: unbalanced parenthesissatır için hata alıyorumpatterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
StatguyUser

Değiştir3 ve değiştir4 işlevi orijinal sorunu ele alırken (sözcükleri değiştirmek için), değiştir1 ve değiştir2 daha genel amaçlıdır, çünkü bunlar iğne yalnızca tek bir sözcük değil, bir kelime öbeği (sözcük dizisi) olsa bile çalışır.
Zoltan Fedor

7

Belki de Python burada doğru araç değildir. İşte Unix araç zinciriyle bir tane

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

Kara liste dosyanızın kelime sınırları eklenmiş olarak önceden işlendiği varsayılır. Adımlar şunlardır: dosyayı çift aralıklı hale dönüştürmek, her cümleyi satır başına bir kelimeye bölmek, kara liste kelimelerini dosyadan toplu olarak silmek ve satırları geri birleştirmek.

Bu, en azından bir kat daha hızlı çalışmalıdır.

Kara liste dosyasını kelimelerden önişlemek için (her satırda bir kelime)

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

4

Buna ne dersin:

#!/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)

Bu çözümler kelime sınırlarını ayırır ve her kelimeyi bir sette arar. Normal ifade alternatiflerini kullanmak normal ifade motorunun kelime eşleşmelerini kontrol etmek zorunda kalmasına neden olurken, bu çözümler O(n)n'nin ayarlanan aramadan dolayı girişin boyutu olduğu yerlerde kelime alternatiflerinin re.sub'ından (Liteyes çözümü) daha hızlı olmalıdır. amortized O(1)sadece kelime sınırları yerine her karakterde. Benim çözümüm, orijinal metinde kullanılan beyaz boşlukları korumak için ekstra özen gösterir (yani, beyaz boşlukları sıkıştırmaz ve sekmeleri, satırsonu satırlarını ve diğer boşluk karakterlerini korur), ancak umursamadığınıza karar verirseniz, bunları çıktıdan çıkarmak oldukça basit olmalıdır.

Gutenberg Projesi'nden indirilen birden fazla e-Kitabın bir araya getirilmesi olan corpus.txt üzerinde test ettim ve banned_words.txt Ubuntu'nun kelime listesinden (/ usr / share / dict / amerikan-ingilizce) rastgele seçilen 20000 kelimedir. 862462 cümleyi (ve PyPy'de bunun yarısı) işlemek yaklaşık 30 saniye sürer. Cümleleri "." İle ayrılmış herhangi bir şey olarak tanımladım.

$ # 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 özellikle ikinci yaklaşımdan daha fazla yararlanırken, CPython ilk yaklaşımda daha iyi sonuç verdi. Yukarıdaki kod hem Python 2 hem de 3 üzerinde çalışmalıdır.


Python 3 soruda verilmiştir. Buna oy verdim, ancak daha az ayrıntılı hale getirmek için bu koddaki bazı ayrıntılardan ve 'optimal' uygulamadan ödün vermeye değer olduğunu düşünüyorum.
pvg

Eğer doğru anlarsam, temelde cevabımla aynı prensip, ama daha ayrıntılı mı? Yarma ve üzerinde katılmadan \W+temelde gibidir subüzerine \w+sağ?
Eric Duminil

Aşağıdaki çözümümün (function replace4) pypy'den daha hızlı olup olmadığını merak ediyorum;) Dosyalarınızı test etmek istiyorum!
bobflux

3

Pratik yaklaşım

Aşağıda açıklanan bir çözüm, tüm metni aynı dizede depolamak ve karmaşıklık düzeyini azaltmak için çok fazla bellek kullanır. RAM bir sorunsa, kullanmadan önce iki kez düşünün.

join/ splitTricks ile algoritmayı hızlandırması gereken döngülerden kaçınabilirsiniz.

  • Cümlelerde bulunmayan bir cümleyi özel bir sınırlayıcı ile birleştirin:
  • merged_sentences = ' * '.join(sentences)

  • |"Veya" regex ifadesini kullanarak cümlelerden kurtulmanız gereken tüm kelimeler için tek bir normal ifade derleyin :
  • regex = re.compile(r'\b({})\b'.format('|'.join(words)), re.I) # re.I is a case insensitive flag

  • Kelimeleri derlenmiş normal ifadeyle alt simge haline getirin ve özel ayırıcı karakterle ayrılmış cümlelere ayırın:
  • clean_sentences = re.sub(regex, "", merged_sentences).split(' * ')

    Verim

    "".joinkarmaşıklık O (n). Bu oldukça sezgisel, ancak yine de bir kaynaktan kısaltılmış bir alıntı var:

    for (i = 0; i < seqlen; i++) {
        [...]
        sz += PyUnicode_GET_LENGTH(item);

    Bu nedenle , ilk yaklaşımla join/split2 * O (N 2 ) karşısında hala doğrusal karmaşıklık olan O (kelimeler) + 2 * O (cümleler) var .


    btw multithreading kullanmayın. GIL her işlemi engelleyecektir, çünkü göreviniz kesinlikle CPU'ya bağlıdır, bu nedenle GIL'in serbest bırakılma şansı yoktur, ancak her iş parçacığı eşzamanlı olarak işaretler göndererek ekstra çabaya neden olur ve hatta işlemi sonsuza götürür.


    Cümlelerin bir metin dosyasında saklanması (saklanması) durumunda, zaten bir satırsonu ile ayrılmışlardır. Böylece tüm dosya tek bir büyük dizge (veya tampon) olarak okunabilir, sözcükler kaldırılabilir ve sonra tekrar yazılabilir (veya bu, dosyada doğrudan bellek eşlemesi kullanılarak yapılabilir). Otoh, bir kelimeyi kaldırmak için dizenin geri kalanının boşluğu doldurmak için geri taşınması gerekir, böylece çok büyük bir dizeyle ilgili bir sorun olur. Bir alternatif, sözcükler arasındaki parçaları başka bir dizgeye veya dosyaya geri yazmak olabilir (yeni satırları içerecektir) - veya bu parçaları bir mmapped dosyaya (1)
    taşıyın

    .. Eric Duminil'in ayar aramasıyla birleştirilen bu son yaklaşım (sözcükler arasındaki bölümleri taşımak / yazmak) gerçekten hızlı olabilir, belki de hiç normal ifade kullanmadan. (2)
    Danny_ds

    .. Ya da belki normal ifade, birden çok kelimeyi değiştirirken yalnızca bu parçaları taşımak için zaten optimize edilmiştir, bilmiyorum.
    Danny_ds

    0

    Tüm cümlelerinizi tek bir belgede birleştirin. Tüm "kötü" kelimelerinizi bulmak için Aho-Corasick algoritmasının herhangi bir uygulamasını ( işte bir tane ) kullanın. Dosyayı çaprazlayın, her kötü kelimeyi değiştirin, takip eden bulunan kelimelerin ofsetlerini güncelleyin vb.

    Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
    Licensed under cc by-sa 3.0 with attribution required.