Używam Pythona 3.5.2
Mam dwie listy
- lista około 750 000 „zdań” (długie ciągi znaków)
- listę około 20 000 „słów”, które chciałbym usunąć z moich 750 000 zdań
Muszę więc przejść przez 750 000 zdań i wykonać około 20 000 podmian, ale TYLKO wtedy, gdy moje słowa są rzeczywiście „słowami” i nie są częścią większego ciągu znaków.
Robię to, wstępnie kompilując moje słowa, tak aby były otoczone \b
metaznakiem
compiled_words = [re.compile(r'\b' + word + r'\b') for word in my20000words]
Następnie przeglądam moje „zdania”
import re
for sentence in sentences:
for word in compiled_words:
sentence = re.sub(word, "", sentence)
# put sentence into a growing list
Ta zagnieżdżona pętla przetwarza około 50 zdań na sekundę , co jest miłe, ale przetwarzanie wszystkich moich zdań nadal zajmuje kilka godzin.
Czy istnieje sposób na użycie tej
str.replace
metody (która moim zdaniem jest szybsza), ale nadal wymaga, aby zamiany miały miejsce tylko na granicach słów ?Alternatywnie, czy istnieje sposób na przyspieszenie tej
re.sub
metody? Już nieznacznie poprawiłem prędkość, pomijając,re.sub
jeśli długość mojego słowa jest>> długość mojego zdania, ale nie jest to duża poprawa.
Dziękuję za wszelkie sugestie.
multiprocessing
(tj. Wielu procesów Pythona).Odpowiedzi:
Jedną rzeczą, którą możesz spróbować, jest skompilowanie jednego pojedynczego wzorca, takiego jak
"\b(word1|word2|word3)\b"
.Ponieważ
re
opiera się na kodzie C, aby dokonać właściwego dopasowania, oszczędności mogą być dramatyczne.Jak @pvg wskazał w komentarzach, korzysta również z dopasowania pojedynczego przebiegu.
Jeśli twoje słowa nie są wyrażeniami regularnymi, odpowiedź Erica jest szybsza.
źródło
s/They actually use/They actually could in theory sometimes use/
. Czy masz jakiś powód, by sądzić, że implementacja Pythona robi tutaj coś innego niż pętla?TLDR
Użyj tej metody (z wyszukiwaniem zestawów), jeśli chcesz uzyskać najszybsze rozwiązanie. W przypadku zbioru danych podobnego do PO jest około 2000 razy szybszy niż zaakceptowana odpowiedź.
Jeśli nalegasz na użycie wyrażenia regularnego do wyszukiwania, użyj tej wersji opartej na trie , która jest nadal 1000 razy szybsza niż unia wyrażeń regularnych.
Teoria
Jeśli twoje zdania nie są wielkimi ciągami znaków, prawdopodobnie możliwe jest przetworzenie znacznie więcej niż 50 na sekundę.
Jeśli zapiszesz wszystkie zabronione słowa w zestawie, bardzo szybko sprawdzisz, czy inne słowo jest zawarte w tym zestawie.
Spakuj logikę do funkcji, podaj tę funkcję jako argument
re.sub
i gotowe!Kod
Zdania konwertowane to:
Zauważ, że:
lower()
)""
może pozostawić dwie spacje (jak w twoim kodzie)\w+
dopasowuje również znaki akcentowane (np"ångström"
.).Występ
Jest milion zdań,
banned_words
prawie 100000 słów, a skrypt działa w mniej niż 7 sekund.Dla porównania, odpowiedź Liteye potrzebowała 160 sekund na 10 tysięcy zdań.
Ponieważ
n
jest to całkowita liczba słów im
liczba zakazanych słów, kod OP i Liteye toO(n*m)
.Dla porównania, mój kod powinien działać
O(n+m)
. Biorąc pod uwagę, że jest o wiele więcej zdań niż zakazanych słów, algorytm staje sięO(n)
.Test związku Regex
Jaka jest złożoność wyszukiwania wyrażeń regularnych ze
'\b(word1|word2|...|wordN)\b'
wzorcem? Czy to jestO(N)
czyO(1)
?Trudno jest pojąć, jak działa silnik wyrażeń regularnych, więc napiszmy prosty test.
Ten kod wyodrębnia
10**i
losowe angielskie słowa do listy. Tworzy odpowiednią sumę wyrażeń regularnych i testuje ją za pomocą różnych słów:#
)Wyprowadza:
Wygląda więc na to, że wyszukiwanie pojedynczego słowa ze
'\b(word1|word2|...|wordN)\b'
wzorem ma:O(1)
najlepszy przypadekO(n/2)
przeciętny przypadek, który nadal jestO(n)
O(n)
najgorszy przypadekTe wyniki są zgodne z prostym wyszukiwaniem w pętli.
Znacznie szybszą alternatywą dla unii wyrażeń regularnych jest utworzenie wzorca wyrażenia regularnego z próby .
źródło
O(1)
twierdzenie, Twoja odpowiedź zdecydowanie zasługuje na pozytywną ocenę.TLDR
Użyj tej metody, jeśli chcesz uzyskać najszybsze rozwiązanie oparte na wyrażeniach regularnych. W przypadku zbioru danych podobnego do PO jest to około 1000 razy szybsze niż zaakceptowana odpowiedź.
Jeśli nie obchodzi Cię regex, użyj tej wersji opartej na zestawie , która jest 2000 razy szybsza niż suma wyrażeń regularnych.
Zoptymalizowany Regex z Trie
Prosty Regex unia podejście staje się powoli z wielu zakazanych słów, ponieważ silnik regex nie robi bardzo dobrą robotę optymalizacji wzoru.
Możliwe jest utworzenie Trie ze wszystkimi zakazanymi słowami i napisanie odpowiedniego wyrażenia regularnego. Wynikowe trie lub regex nie są tak naprawdę czytelne dla człowieka, ale pozwalają na bardzo szybkie wyszukiwanie i dopasowywanie.
Przykład
Lista jest konwertowana na trie:
A potem do tego wzorca wyrażenia regularnego:
Ogromną zaletą jest to, że aby sprawdzić, czy
zoo
pasuje, silnik regex musi porównać tylko pierwszy znak (nie pasuje), zamiast wypróbować 5 słów . Jest to przesada preprocesu dla 5 słów, ale daje obiecujące wyniki dla wielu tysięcy słów.Zwróć uwagę, że grupy
(?:)
nieprzechwytywane są używane, ponieważ:foobar|baz
pasowałabyfoobar
lubbaz
, ale niefoobaz
foo(bar|baz)
zapisałby niepotrzebne informacje w grupie przechwytującej .Kod
Oto nieco zmodyfikowana treść , której możemy użyć jako
trie.py
biblioteki:Test
Oto mały test (taki sam jak ten ):
Wyprowadza:
Aby uzyskać więcej informacji, wyrażenie regularne zaczyna się tak:
To naprawdę nieczytelne, ale dla listy 100000 zabronionych słów to wyrażenie regularne Trie jest 1000 razy szybsze niż proste połączenie wyrażeń regularnych!
Oto diagram kompletnej trie, wyeksportowany za pomocą trie-python-graphviz i graphviz
twopi
:źródło
|
ale grupy przechwytywania nie są w ogóle potrzebne do naszego celu. Po prostu spowolniliby proces i zużyli więcej pamięci bez korzyści.\b
( granica słowa ). Jeśli lista jest['apple', 'banana']
taka, zastąpi słowa, które są dokładnieapple
lubbanana
, ale nienana
,bana
lubpineapple
.Jedną z rzeczy, których możesz chcieć spróbować, jest wstępne przetwarzanie zdań w celu zakodowania granic słów. Zasadniczo zamień każde zdanie w listę słów, dzieląc je na granice słów.
Powinno to być szybsze, ponieważ aby przetworzyć zdanie, wystarczy przejść przez każde ze słów i sprawdzić, czy pasuje.
Obecnie wyszukiwanie wyrażeń regularnych musi za każdym razem przejść przez cały ciąg, szukając granic słów, a następnie „odrzucając” wynik tej pracy przed następnym przebiegiem.
źródło
Oto szybkie i łatwe rozwiązanie z zestawem testowym.
Zwycięska strategia:
re.sub ("\ w +", repl, zdanie) wyszukuje słowa.
„repl” może być wywoływalne. Użyłem funkcji, która wyszukuje słowa, a dykta zawiera słowa do wyszukania i zastąpienia.
Jest to najprostsze i najszybsze rozwiązanie (zobacz funkcję replace4 w przykładowym kodzie poniżej).
Drugi najlepszy
Pomysł polega na podzieleniu zdań na słowa za pomocą funkcji re.split, zachowując jednocześnie separatory do późniejszej rekonstrukcji zdań. Następnie zastąpienia są wykonywane za pomocą prostego wyszukiwania dyktowania.
(zobacz funkcję replace3 w przykładowym kodzie poniżej).
Czasy na przykład funkcje:
... i kod:
Edycja: Możesz także ignorować małe litery podczas sprawdzania, czy przekazujesz listę zdań małymi literami i edytujesz odpowiedź
źródło
replace4
a mój kod ma podobne wyniki.repl(m):
robi def i jak przypisujeszm
w funkcji replace4error: unbalanced parenthesis
dla liniipatterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
Być może Python nie jest tutaj odpowiednim narzędziem. Oto jeden z łańcuchem narzędzi Unix
zakładając, że plik czarnej listy jest wstępnie przetworzony z dodanymi granicami słów. Kroki są następujące: przekonwertować plik do podwójnych odstępów, podzielić każde zdanie na jedno słowo w wierszu, masowo usunąć słowa z czarnej listy z pliku i ponownie scalić wiersze.
Powinno to przebiegać przynajmniej o rząd wielkości szybciej.
Do wstępnego przetwarzania pliku czarnej listy ze słów (jedno słowo w wierszu)
źródło
Co powiesz na to:
Te rozwiązania dzielą się na granice słów i wyszukują każde słowo w zestawie. Powinny być szybsze niż re.sub of word alternates (rozwiązanie Liteyes'a), ponieważ te rozwiązania są
O(n)
tam, gdzie n jest rozmiarem wejścia ze względu naamortized O(1)
wyszukiwania zestawu, podczas gdy użycie alternatywnych wyrażeń regularnych spowodowałoby, że silnik wyrażeń regularnych musiałby sprawdzać dopasowania słów na każdym znaku, a nie tylko na granicach słów. Moje rozwiązanie: zwróć szczególną uwagę, aby zachować białe znaki, które były użyte w oryginalnym tekście (tj. Nie kompresuje białych znaków i zachowuje tabulatory, nowe linie i inne białe znaki), ale jeśli zdecydujesz, że Cię to nie obchodzi, powinno być dość proste, aby usunąć je z wyjścia.Testowałem na corpus.txt, który jest konkatenacją wielu eBooków pobranych z projektu Gutenberg, a banned_words.txt to 20000 słów losowo wybranych z listy słów Ubuntu (/ usr / share / dict / american-english). Przetworzenie 862462 zdań zajmuje około 30 sekund (i połowę tego na PyPy). Zdefiniowałem zdania jako wszystko oddzielone znakiem „.”.
PyPy szczególnie skorzystał na drugim podejściu, podczas gdy CPython wypadł lepiej w pierwszym podejściu. Powyższy kod powinien działać zarówno na Pythonie 2, jak i 3.
źródło
\W+
jest w zasadzie jaksub
na\w+
, prawda?Praktyczne podejście
Rozwiązanie opisane poniżej wykorzystuje dużo pamięci do przechowywania całego tekstu w tym samym ciągu i do zmniejszenia poziomu złożoności. Jeśli problemem jest pamięć RAM, zastanów się dwa razy, zanim go użyjesz.
Dzięki
join
/split
tricks możesz w ogóle uniknąć pętli, co powinno przyspieszyć algorytm.|
wyrażenia „lub” wyrażenia regularnego:Występ
"".join
złożoność jest O (n). Jest to dość intuicyjne, ale i tak jest skrócony cytat ze źródła:Dlatego z
join/split
tobą masz O (słowa) + 2 * O (zdania), co jest nadal liniową złożonością w porównaniu z 2 * O (N 2 ) z początkowym podejściem.przy okazji nie używaj wielowątkowości. GIL zablokuje każdą operację, ponieważ twoje zadanie jest ściśle związane z procesorem, więc GIL nie ma szans na zwolnienie, ale każdy wątek będzie wysyłał tiki jednocześnie, co powoduje dodatkowy wysiłek, a nawet prowadzi do nieskończoności.
źródło
Połącz wszystkie zdania w jeden dokument. Użyj dowolnej implementacji algorytmu Aho-Corasick ( tutaj jeden ), aby zlokalizować wszystkie swoje „brzydkie” słowa. Przeszukuj plik, zastępując każde złe słowo, aktualizując przesunięcia znalezionych słów, które następują itp.
źródło