Пришвидшити мільйони заміни регулярних виразів в Python 3


127

Я використовую 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якщо довжина мого слова> більше, ніж довжина мого речення, але це не дуже покращило.

Дякую за будь-які пропозиції.


1
Перша відповідь тут має хороший зразок коду: stackoverflow.com/questions/2846653/… просто розділіть свій масив речень на кількість ядер CPU, у яких ви запустили стільки потоків
Мохаммад Алі

4
Ви також можете спробувати не-зворотну форму - перейдіть слово в слово за словом та порівнюйте кожного з набором. Це однократний прохід і пошук хешу досить швидкий.
пвг

2
Скільки тривають ці речення, до речі? 750k рядків не схожі на набір даних, на який потрібно обробляти години.
пвг

2
@MohammadAli: Не турбуйтеся з цим прикладом для роботи з процесором. У Python є великий замок, який потрібно робити при виконанні байт-коду (Global Interpreter Lock), тому ви не можете скористатися потоками для роботи процесора. Вам потрібно буде використовувати multiprocessing(тобто кілька процесів Python).
Кевін

1
Для цього вам потрібен інструмент для промислової міцності . З трійкового дерева списку рядків генерується трійчастий вираз. Ніколи не існує більше ніж 5 кроків до невдачі, що робить цей найшвидший метод для відповідності цього типу. Приклади: словник 175000 слів або подібний до списку заборонених лише 20 000 S-слів
x15

Відповіді:


123

Одне, що можна спробувати, - це скласти один єдиний зразок "\b(word1|word2|word3)\b".

Оскільки reпокладається на код C для фактичного узгодження, економія може бути драматичною.

Як в коментарях зазначав @pvg, він також виграє від співпадіння одного проходу.

Якщо ваші слова не регулярною вираз, Ерік відповідь швидше.


4
Це не лише імпульс C (що робить велику різницю), але ви також співпадаєте з одним проходом. Варіанти цього питання виникають досить часто, трохи дивно, що немає (а може, є, ховається десь?) Канонічної відповіді ТА на цю досить розумну ідею.
пвг

40
@Liteye ваша пропозиція перетворила 4-годинну роботу на 4-х хвилинну роботу! Мені вдалося об'єднати всі 20 000+ регексів в один гігантський регулярний вираз, і мій ноутбук не кинув очей. Знову дякую.
пданес

2
@Bakuriu : s/They actually use/They actually could in theory sometimes use/. Чи є у вас будь-які підстави вважати, що реалізація Python робить тут щось, крім циклу?
користувач541686

2
@Bakuriu: Мені б дуже цікаво дізнатись, чи це так, але я не думаю, що рішення регулярного вираження займає лінійний час. Якщо це не створить трійця з союзу, я не бачу, як це могло статися.
Ерік Дюмініл

2
@Bakuriu: Це не причина. Я запитав, чи є у вас підстави вважати, що реалізація насправді так поводиться, а не у вас є підстави вважати, що вона може вести себе таким чином. Особисто я ще не стикався з реагексацією єдиної мови програмування, яка працює в лінійний час так само, як ви б очікували від класичного регулярного вираження, тому якщо ви знаєте, що Python це робить, ви повинні показати деякі докази.
користувач541686

123

TLDR

Скористайтеся цим методом (з набором пошуку), якщо хочете найшвидшого рішення. Для набору даних, аналогічних ОП, це приблизно в 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має майже 100000 слів, а сценарій працює менше ніж за 7 секунд.

Для порівняння, відповідь Литея потребувала 160-х років за 10 тисяч речень.

Що nстосується загальної кількості слів і mкількості заборонених слів, код ОП та Литея є O(n*m).

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

Тест на союз Regex

Яка складність пошуку регулярних виразів з '\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 речень в секунду повільно, то все, що я можу сказати, - це я наведення спрощеного прикладу. Справжній набір даних є складнішим, ніж я описую, але це не здавалося доречним. Крім того, об'єднання моїх "слів" в єдиний регулярний вираз масово покращило швидкість. Також я "витісняю" подвійні пробіли після заміни.
пданес

1
@ user36476 Дякую за відгук, я видалив відповідну частину. Не могли б ви спробувати мою пропозицію? Смію сказати, що це набагато швидше, ніж прийнята відповідь.
Ерік Дюмініл

1
Оскільки ви усунули цю оманливу O(1)претензію, ваша відповідь, безумовно, заслуговує на підсумковий голос.
ідмеан

1
@idmean: Правда, це було не дуже зрозуміло. Він просто мав на увазі пошук: "Це слово заборонене слово?".
Ерік Думініл

1
@EricDuminil: Чудова робота! Бажаю, щоб я міг подати пропозицію вдруге.
Маттьє М.

105

TLDR

Використовуйте цей метод, якщо хочете найшвидше рішення на основі регулярних виразів. Для набору даних, аналогічних ОП, це приблизно в 1000 разів швидше, ніж прийнята відповідь.

Якщо ви не піклуєтесь про регулярний вираз, використовуйте цю набір версій , яка в 2000 разів швидша, ніж об'єднання регулярних виразів.

Оптимізований Regex з Trie

Простий Regex союз підхід стає повільним з багатьма забороненими словами, тому що движок регулярних виразів робить дуже хорошу роботу по оптимізації шаблону.

Можна створити Trie з усіма забороненими словами та написати відповідний регулярний вираз. Отриманий трійник або регекс насправді не читаються людиною, але вони дозволяють зробити дуже швидкий пошук і збіг.

Приклад

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

Союз Regex

Список перетворюється на трійку:

{
    '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())

Тест

Ось невеликий тест (такий же, як цей ):

# 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 ))? | на)) | b (?: \ 's | a (?: c (?: us (?: (?: \' s | es))? | [ik]) | ft | самотній (? : (?: \ '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))? | переродити (?: e [ds]? | i (?: ng | on (?: (?: \' s | s))?)) | y (?: \ ' s)? | \ é (?: (?: \ 's | s))?) | d (?: icat (?: e [ds]? | i)? (n: ng | on (?: (?: \ 's | s))?)) | om (?: en (?: (?: \' s | s))? | inal) | u (?: ct (? :( ?: ed | i (?: ng | on (?: (?: \ 's | s))?) | або (?: (?: \' s | s))? | s))? | l (?: \ 's)?) ) | е (?: (?: \ 's | am | l (?: (?: \' s | ard | син (?: \ 's)?))? | r (?: deen (?: \) 's)? | nathy (?: \' s)? | ra (?: nt | ція (?: (?: \ 's | s))?)) | t (? :( ?: t (?: e (?: r (?: (?: \ 's | s))? | d) | ing | або (?: (?: \'s | s))?) | s))? | yance (?: \ 's)? | d))? | hor (? :( ?: r (?: e (?: n (?: ce (?: 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 (?: 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) ))? | ції (?: (?: \ 's | ist (?: (?: \' s | s))?))?) | міна (?: 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 | дошка))?) | r (?: a (?: кадабра (?: \' s)? | d (?: e [ds]? | ing) | шинка (? : \ '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 | на (?: (?: \ '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 разів швидший, ніж простий союз регулярних виразів!

Ось діаграма повного тріє, експортованого за допомогою трие-пітон-графвізу та графвізу twopi:

Введіть тут опис зображення


Здається, що для початкової мети немає потреби в групі, яка не захоплює. Принаймні слід згадати значення групи, яка не захоплює,
Xavier Combelle

3
@XavierCombelle: Ти маєш рацію, що я повинен згадати групу захоплення: відповідь оновлена. Я вважаю, що все навпаки: потрібні парони для чергування регулярних виразів, |але групи захоплення взагалі не потрібні для нашої мети. Вони просто сповільнить процес і використають більше пам'яті без користі.
Ерік Думініл

3
@EricDuminil Ця публікація ідеальна, велике спасибі :)
Мохамед АЛ АНІ

1
@MohamedALANI: У порівнянні з яким рішенням?
Ерік Думініл

1
@ PV8: Він повинен відповідати лише повним словам, так, завдяки \b( межа слова ). Якщо список є ['apple', 'banana'], він замінить слова, які є точно appleабо banana, але ні nana, banaабо pineapple.
Ерік Думініл

15

Одне, що ви можете спробувати, це попередня обробка речень для кодування меж слова. В основному перетворіть кожне речення на перелік слів, розділяючи на межі слів.

Це повинно бути швидше, тому що для обробки речення вам просто потрібно переглядати кожне зі слів і перевірити, чи збігається він.

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


8

Ну ось швидке та просте рішення з тестовим набором.

Стратегія виграшу:

re.sub ("\ w +", repl, речення) шукає слова.

"repl" може бути зателефонованим. Я використовував функцію, яка виконує пошук dict, і dict містить слова для пошуку та заміни.

Це найпростіше і найшвидше рішення (див. Функцію заміни4 у прикладі коду нижче).

Другий кращий

Ідея полягає в тому, щоб розділити речення на слова, використовуючи re.split, зберігаючи роздільники, щоб реконструювати речення пізніше. Потім заміни проводяться за допомогою простого пошуку дикту.

(див. функцію zamje3 в прикладі коду нижче).

Приклад часу, наприклад, функції:

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 )

Редагувати: Ви також можете ігнорувати малі регістри під час перевірки, чи передаєте ви нижній регістр речень і редагуєте відбиття

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

1
Резюме для тестів. replace4і мій код мають подібні дії.
Ерік Думініл

Не впевнений, що repl(m):робить def і як ви призначаєте mфункцію zamje4
StatguyUser

Також я отримую помилку error: unbalanced parenthesisдля лініїpatterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
StatguyUser

У той час, як функція substitu3 та substitu4 вирішує оригінальну проблему (для заміни слів), заміна1 та заміна2 мають більш загальне призначення, оскільки вони працюють, навіть якщо голка - це фраза (послідовність слів), а не лише одне слово.
Золтан Федір

7

Можливо, Python тут не є правильним інструментом. Ось один із інструментальної мережі Unix

sed G file         |
tr ' ' '\n'        |
grep -vf blacklist |
awk -v RS= -v OFS=' ' '{$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)

Ці рішення розбиваються на межі слів і шукають кожне слово в наборі. Вони повинні бути швидшими, ніж перезапис альтернативних слів (рішення Liteyes), оскільки ці рішення O(n)де n - розмір вхідного сигналу внаслідок amortized O(1)встановленого пошуку, в той час як використання альтернативних регекс-замінників призведе до того, що двигуну регулярних вирівнювань доведеться перевіряти відповідність слів на всіх символах, а не лише на межі слів. Моє рішення займає особливу увагу, щоб зберегти пробіли, які були використані в оригінальному тексті (тобто він не стискає пробіли та зберігає вкладки, нові рядки та інші символи пробілу), але якщо ви вирішите, що вам це не цікаво, це повинно бути досить простим, щоб видалити їх з виводу.

Я перевірив на corpus.txt, який є об'єднанням декількох електронних книг, завантажених з проекту Гутенберга, і 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.


Python 3 - це дане питання. Я підтримав це, але думаю, що варто було б пожертвувати деякими деталями та «оптимальною» реалізацією в цьому коді, щоб зробити його менш багатослівним.
пвг

Якщо я правильно це розумію, це в основному той же принцип, що і моя відповідь, але більш багатослівний? Розщеплення і приєднання до них \W+- це, як subна \w+, правда?
Ерік Дюмініл

Цікаво, чи моє рішення нижче (функція заміни4) швидше, ніж pypy;) Я хотів би перевірити ваші файли!
бобфлюкс

3

Практичний підхід

Описане нижче рішення використовує багато пам'яті для зберігання всього тексту в одному рядку та для зниження рівня складності. Якщо оперативна пам’ять - це проблема, подумайте двічі перед її використанням.

За допомогою join/ splitхитрощів ви можете взагалі уникнути циклів, які повинні прискорити алгоритм.

  • Об’єднайте речення із спеціальним деліметром, який не міститься у реченнях:
  • 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/splitвас є O (слова) + 2 * O (речення), що все ще є лінійною складністю проти 2 * O (N 2 ) з початковим підходом.


    btw не використовують багатопотокові. GIL блокує кожну операцію, оскільки ваше завдання суворо пов'язане з процесором, тому GIL не має шансів звільнятися, але кожен потік посилатиме клічки одночасно, що спричиняє додаткові зусилля і навіть призводить до нескінченності.


    Якщо речення (були) збережені у текстовому файлі, вони вже відокремлюються новим рядком. Таким чином, весь файл можна прочитати як один великий рядок (або буфер), слова видалити, а потім знову записати (або це можна зробити у файлі безпосередньо за допомогою відображення пам'яті). Ото, щоб видалити слово, решту рядка потрібно перемістити назад, щоб заповнити пробіл, так що це буде проблемою з однією дуже великою строкою. Альтернативою було б записати частини між словами назад в інший рядок або файл (до якого входитимуть нові рядки) - або просто перемістити ці частини у файл mmapped (1) ..
    Danny_ds

    .. Цей останній підхід (переміщення / запис частин між словами) у поєднанні з набором Еріка Думініла може бути дуже швидким, можливо, навіть не використовуючи регулярного вираження. (2)
    Danny_ds

    .. А може, регулярний вираз вже оптимізований для переміщення лише тих частин при заміні кількох слів, я не знаю.
    Danny_ds

    0

    З’єднайте всі свої пропозиції в один документ. Використовуйте будь-яку реалізацію алгоритму Aho-Corasick ( ось один ), щоб знайти всі ваші "погані" слова. Перейдіть по файлу, замінюючи кожне невдале слово, оновлюючи зсуви знайдених слів, які випливають і т.д.

    Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
    Licensed under cc by-sa 3.0 with attribution required.