Ускорьте миллионы регулярных выражений в Python 3

Я использую Python 3.5.2

У меня есть два списка

  • список из примерно 750 000 "предложений" (длинные строки)
  • список из 20 000 слов, которые я хотел бы удалить из моих 750 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, если длина моего словa > больше, чем длина моего предложения, но это не очень улучшилось.

Спасибо за любые предложения.

Ответ 1

Одна вещь, которую вы можете попробовать, состоит в том, чтобы скомпилировать один шаблон, например "\b(word1|word2|word3)\b".

Поскольку re полагается на код C для фактического сопоставления, экономия может быть значительной.

Как отметил @pvg в комментариях, он также выигрывает от однопроходного сопоставления.

Если ваши слова не являются регулярными выражениями, Eric answer работает быстрее.

Ответ 2

TL;DR

Используйте этот метод (с поиском по множеству), если вы хотите самое быстрое решение. Для набора данных, аналогичного OP, он примерно в 2000 раз быстрее принятого ответа.

Если вы настаиваете на использовании регулярного выражения для поиска, используйте эту основанную на trie версию, которая все еще в 1000 раз быстрее, чем объединение регулярных выражений.

теория

Если ваши предложения не являются огромными строками, возможно, выполнимо обрабатывать намного больше, чем 50 в секунду.

Если вы сохраните все запрещенные слова в наборе, будет очень быстро проверить, включено ли другое слово в этот набор.

re.sub логику в функцию, передайте эту функцию в качестве аргумента для 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 содержит почти 100000 слов, а сценарий выполняется менее чем за 7 секунд.

Для сравнения, на Liteye ответ потребовалось 160 секунд на 10 тысяч предложений.

Если n - общее количество слов, а m - количество запрещенных слов, то OP и код Liteye равны O(n*m).

Для сравнения мой код должен работать в O(n+m). Учитывая, что предложений намного больше, чем запрещенных слов, алгоритм становится O(n).

Regex union test

Какова сложность регулярного выражения с '\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) худший случай

Эти результаты согласуются с простым циклом поиска.

Гораздо более быстрой альтернативой объединению регулярных выражений является создание шаблона регулярных выражений из дерева.

Ответ 3

TL;DR

Используйте этот метод, если вы хотите самое быстрое решение на основе регулярных выражений. Для набора данных, аналогичного OP, он примерно в 1000 раз быстрее принятого ответа.

Если вас не интересует регулярное выражение, используйте эту версию на основе множеств, которая в 2000 раз быстрее, чем объединение регулярных выражений.

Оптимизированное регулярное выражение с Trie

Простой подход объединения регулярных выражений становится медленным со многими запрещенными словами, потому что механизм регулярных выражений не очень хорошо справляется с оптимизацией шаблона.

Можно создать Trie со всеми запрещенными словами и написать соответствующее регулярное выражение. Получившиеся в результате tree или regex на самом деле не читаются человеком, но они позволяют очень быстро искать и сопоставлять.

пример

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

Regex union

Список преобразуется в три:

{
    '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"

Regex trie

Огромное преимущество заключается в том, что для проверки соответствия 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())

Тестовое задание

Вот небольшой тест (такой же, как этот):

# 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

Для информации, регулярное выражение начинается так:

(: А (: (:\'s | а (: \??? S | чен | liyah (: \? S) | г (:? Dvark (: (:? \' S | s )) | на)) | б? (: \? s | а (? с (: нас (: (: \?? s | эс)) | [ик]) | фт | одинокий (?: (?:\'s | s)?) | ndon (:(?: изд | ИНГ | Мент (?: \? s) | s)) | s (:??? е (:(?: Мент: | [DS])) | ч (:(?: е [DS] | луг)) | ю) | т ((\ 's?)???? е (:(?: Мент ( :\'s) | [DS])) | ИНГ | ТОиР (?:? (: \?' s | s)))) | Ь (: а (:? ид) | е (?: сс (: (:\'у s | | ы)? ((: \??)' |) | а (s s)): (:\'s | т (?:?\'s) | s)) | reviat? (: е [DS] | я (?:? нг | на (: (: \?' s | s)))) | у (:? \"? s) |\е (: (:?\'s | s)?)) | d (: ICAT (: е [DS] | я (???? нг | на (: (:\'s | s)))) | ом (?:? еп (: (: \?' s | s)) | Инал) | и (??? а (:(?: изд | я (?: нг | на (: (: \? 's | s))) | или? (: (: \? s | s)) | s)) | л (???? \' s)) ) | е (: (:?\'s | ч | л (: (: \?' s | ARD | сын (?:\'s))) | г (??? Deen (:\'s) | Nathy? (?: \' s) | ра (:? нт | Тион (: (:?\'s | s?)))) | т (:(?: т (?: е (? г ((:?\'? s | з)) | d) | ИНГ | или ((:? \' s | s))) | s)) | Янс (?? :\'? ы) | г)) | Hor (:(?: г (: е (: п (: се (????? \'? ы) | т) | г) | луг) | s)) | я? (: d (: е [DS] | ING | Январе (?:?\'s?)) | Гейл | л (:? ена | это (:? е годы | у (?:\'? s)))) | J (: ЭСТ (: LY) | ур (??? ция ((:? \'? s | s)) | е [DS] | луг)) | л? (:??? а (: TIVE (: (:?\'ы | ы)) | г) | е (:(?: й | г)) | ООМ | социологическое загрязнение (:(? :\'s | s)) | у) | т\'ы | п (:? е (: гат (: е [DS] | я (:? нг | на (:? \'? ы))) | г (: \"? s)) | НПУ (:(?: это (: IES | у (:? \? s)) | ют))) | о (?:? ARD | де (:? (:\'? s | s)) | ли (: ш (:(?: е [DS] | луг)) | Тион (?:? (:?\'s | IST (: (:? \' s | s))))) | Mina (:? бл [еу] | т (: е [DS] | я (???? нг | на ((?:\'s | s))) )) | г (:? igin (: аль ((:?\'s | s)) |? е (: (:? \' s | s))) | т (:(??: изд | я (:? нг | на (: (:\'s | IST (: (:? \?? s | s)) | s)) | ве) |? s))) | и: т) | | ве (й (:(?: изд | ИНГ | s)?)? ((:?\'s | доска)??)) | г (: а (: Cadabra ( :\'? s) | d (:? е [DS] | луг) | ветчина (: \? s) | м (?:? (?: \' s | s)) | си (?: по (: (: \? 's | s)) | ве? (:? (: \' s | LY | Несс (:?\'s) | s?)))) | восток | IDG (: е (:(?: Мент (: (:??\'s | s)) | [DS])?) | ИНГ | Мент? ((:? \' s | s))? ) | о (: объявления | гат (: е [DS] | я (???? нг | на ((?:\'s | s))))) | УПТ (:( ?: е: LY | | ность (й | г?) (: \? '))) | s (ы):? АЛОМА | с (???? ESS (: (: \' ы | е [DS] | луг)) | МАСО (: (:\'s | [исп])) | Зонд (:(?: изд | ИНГ | s))) | еп (????? с (:( :\'? s | s)) | т (:(?: е (: е (: (: \??? s | изма (:? \' s) | s)) |? d) | ИНГ | LY | s))) | INTH (?: (?:?\'? s | е (: \' s))) | о (:? л (: ут (: е (??: (?:\'s | LY | й?)) | я? (: на (: \?' s) | см?:)) | V ((\ 's?):? е [DS]? | луг)) | г (: Ь (:(?: е (: п (: Cy (????? \? s) | т (: (:?\'s | s))? ) | d) | ИНГ | s)) |? пт я...

Это действительно нечитаемо, но для списка из 100000 запрещенных слов это регулярное выражение Trie в 1000 раз быстрее, чем простое объединение регулярных выражений!

Вот диаграмма всего дерева, экспортированного с помощью trie-python-graphviz и graphviz twopi:

Enter image description here

Ответ 4

Одна вещь, которую вы, возможно, захотите попробовать, - предварительная обработка предложений для кодирования границ слов. По сути, каждое предложение в список слов разбивается на границы слов.

Это должно быть быстрее, потому что для обработки предложения вам просто нужно выполнить каждое из слов и проверить, соответствует ли оно.

В настоящее время поиск по регулярному выражению должен проходить всю цепочку снова каждый раз, ища границы слова, а затем "отбрасывая" результат этой работы до следующего прохода.

Ответ 5

Ну, здесь быстрое и простое решение с набором тестов.

Стратегия выигрышей:

re.sub( "\ w +", repl, предложение) ищет слова.

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

Ответ 6

Возможно, Python здесь не подходит. Вот один из инструментов Unix toolchain

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

Предполагая, что ваш файл черного списка предварительно обработан с добавленными границами слов. Шаги: конвертировать файл в двойное разнесение, разбивать каждое предложение на одно слово в строке, массово удалять слова черного списка из файла и объединять строки.

Это должно работать как минимум на порядок быстрее.

Для предварительной обработки файла черного списка из слов (по одному слову на строку)

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

Ответ 7

Как насчет этого:

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

Эти решения расщепляются на границах слов и просматривают каждое слово в наборе. Они должны быть быстрее, чем re_sub альтернативных слов (решение Liteyes), поскольку эти решения O(n), где n - размер ввода, связанный с поиском набора amortized O(1), в то время как использование переменных regex приведет к тому, должны проверять совпадения слов на каждом символе, а не только на границах слов. Мое решение уделяет особое внимание сохранению пробелов, которые использовались в исходном тексте (т.е. Не сжимает пробелы и сохраняет вкладки, символы новой строки и другие символы пробелов), но если вы решите, что вам это неинтересно, должно быть достаточно простым, чтобы удалить их с выхода.

Я тестировал на corpus.txt, который представляет собой объединение нескольких электронных книг, загруженных из проекта Gutenberg, а banned_words.txt - 20000 слов, случайно выбранных из списка слов Ubuntu (/usr/share/dict/american-english). Для обработки 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.

Ответ 8

Практический подход

В описанном ниже решении используется много памяти для хранения всего текста в одной строке и для снижения уровня сложности. Если ОЗУ является проблемой, подумайте дважды, прежде чем использовать ее.

С помощью трюков join/split вы можете избежать циклов на всех, что должно ускорить алгоритм.

Объедините предложения со специальным разделителем, который не содержится в предложениях:
merged_sentences = ' * '.join(sentences)

Скомпилируйте одно регулярное выражение для всех слов, которые нужно избавиться от предложений, используя | "или" выражение regex:
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 (слова) + 2 * O (предложения), который по-прежнему является линейной сложностью vs 2 * O (N 2) с начальным подходом.


b.t.w. не используйте многопоточность. GIL блокирует каждую операцию, потому что ваша задача строго связана с процессором, поэтому GIL не имеет возможности быть выпущенным, но каждый поток будет отправлять тики одновременно, что вызовет дополнительные усилия и даже приведет к бесконечности.

Ответ 9

Объедините все ваши предложения в один документ. Используйте любую реализацию алгоритма Aho-Corasick (здесь один), чтобы найти все ваши "плохие" слова. Перемещайте файл, заменяя каждое плохое слово, обновляя смещения найденных слов, которые следуют и т.д.