Tăng tốc hàng triệu thay thế regex trong Python 3


127

Tôi đang sử dụng Python 3.5.2

Tôi có hai danh sách

  • một danh sách khoảng 750.000 "câu" (chuỗi dài)
  • một danh sách khoảng 20.000 "từ" mà tôi muốn xóa khỏi 750.000 câu của tôi

Vì vậy, tôi phải lặp lại 750.000 câu và thực hiện khoảng 20.000 thay thế, nhưng CHỈ nếu từ của tôi thực sự là "từ" và không phải là một phần của chuỗi ký tự lớn hơn.

Tôi đang làm điều này bằng cách biên dịch trước các từ của tôi để chúng được bao quanh bởi \bmetacharacter

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

Sau đó, tôi lặp qua "câu" của mình

import re

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

Vòng lặp lồng nhau này đang xử lý khoảng 50 câu mỗi giây , điều này thật tuyệt, nhưng vẫn phải mất vài giờ để xử lý tất cả các câu của tôi.

  • Có cách nào để sử dụng str.replacephương thức (mà tôi tin là nhanh hơn), nhưng vẫn yêu cầu thay thế chỉ xảy ra ở ranh giới từ ?

  • Ngoài ra, có cách nào để tăng tốc re.subphương pháp? Tôi đã cải thiện tốc độ một chút bằng cách bỏ qua re.subnếu độ dài của từ của tôi> hơn độ dài của câu, nhưng nó không cải thiện nhiều.

Cảm ơn bạn cho bất kỳ đề nghị.


1
Câu trả lời đầu tiên ở đây có một số mã mẫu hay: stackoverflow.com/questions/2846653/ chỉ cần chia mảng câu của bạn cho số lõi CPU mà bạn có sau đó chạy nhiều luồng
Mohammad Ali

4
Bạn cũng có thể thử triển khai phi regex - duyệt từng từ đầu vào của bạn và khớp từng từ với một bộ. Đây là pass đơn và tra cứu băm khá nhanh.
pvg

2
Những câu này dài bao lâu, tình cờ? 750k dòng không giống như một bộ dữ liệu cần phải mất hàng giờ để xử lý.
pvg

2
@MohammadAli: Đừng bận tâm với ví dụ đó cho công việc gắn với CPU. Python có một khóa lớn mà nó cần khi thực hiện mã byte (Khóa phiên dịch toàn cầu), vì vậy bạn không thể hưởng lợi từ các luồng cho công việc CPU. Bạn sẽ cần sử dụng multiprocessing(tức là nhiều quy trình Python).
Kevin

1
Bạn cần một công cụ sức mạnh công nghiệp để làm điều này. Một regex trie được tạo ra từ một cây ternary của một danh sách các chuỗi. Không bao giờ có quá 5 bước để thất bại khiến đây là phương pháp nhanh nhất để thực hiện loại kết hợp này. Ví dụ: từ điển 175.000 từ hoặc tương tự như danh sách bị cấm của bạn chỉ với 20.000 từ S
x15

Câu trả lời:


123

Một điều bạn có thể thử là biên dịch một mẫu duy nhất như thế nào "\b(word1|word2|word3)\b".

redựa vào mã C để thực hiện so khớp thực tế, khoản tiết kiệm có thể rất lớn.

Như @pvg đã chỉ ra trong các bình luận, nó cũng được hưởng lợi từ kết hợp vượt qua đơn.

Nếu lời nói của bạn không phải là regex, câu trả lời của Eric sẽ nhanh hơn.


4
Đó không chỉ là hàm C (tạo ra sự khác biệt lớn) mà bạn còn phù hợp với một lượt đi. Các biến thể của câu hỏi này xuất hiện khá thường xuyên, có một chút kỳ lạ không (hoặc có thể có, ẩn ở đâu đó?) Một câu trả lời SO chuẩn cho nó với ý tưởng khá hợp lý này.
pvg

40
@Liteye đề xuất của bạn đã biến một công việc 4 giờ thành một công việc 4 phút! Tôi đã có thể tham gia tất cả hơn 20.000 regex vào một regex khổng lồ duy nhất và máy tính xách tay của tôi không bị rối mắt. Cảm ơn một lần nữa.
pdanese

2
@Bakuriu : s/They actually use/They actually could in theory sometimes use/. Bạn có bất kỳ lý do nào để tin rằng việc triển khai của Python đang làm bất cứ điều gì khác ngoài một vòng lặp ở đây không?
dùng541686

2
@Bakuriu: Tôi thực sự muốn biết nếu đó là trường hợp, nhưng tôi không nghĩ giải pháp regex mất thời gian tuyến tính. Nếu nó không xây dựng Trie ra khỏi liên minh, tôi không thấy nó có thể xảy ra như thế nào.
Eric Duminil

2
@Bakuriu: Đó không phải là lý do. Tôi đã hỏi liệu bạn có lý do để tin rằng việc thực hiện thực sự hành xử theo cách đó không, liệu bạn có lý do để tin rằng nó có thể hành xử theo cách đó không. Cá nhân tôi chưa bắt gặp một triển khai biểu thức chính của ngôn ngữ lập trình chính duy nhất hoạt động theo thời gian tuyến tính giống như cách bạn mong đợi một biểu thức cổ điển, vì vậy nếu bạn biết Python thực hiện điều này, bạn nên đưa ra một số bằng chứng.
dùng541686

123

TLD

Sử dụng phương pháp này (với thiết lập tra cứu) nếu bạn muốn giải pháp nhanh nhất. Đối với một tập dữ liệu tương tự như của OP, nó nhanh hơn khoảng 2000 lần so với câu trả lời được chấp nhận.

Nếu bạn khăng khăng sử dụng regex để tra cứu, hãy sử dụng phiên bản dựa trên bộ ba này , tốc độ này vẫn nhanh hơn 1000 lần so với liên minh regex.

Học thuyết

Nếu câu của bạn không phải là chuỗi hài hước, có thể xử lý nhiều hơn 50 mỗi giây.

Nếu bạn lưu tất cả các từ bị cấm vào một bộ, sẽ rất nhanh để kiểm tra xem một từ khác có được bao gồm trong bộ đó không.

Đóng gói logic vào một hàm, cung cấp hàm này làm đối số re.subvà bạn đã hoàn tất!

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)

Các câu đã chuyển đổi là:

' .  !
  .
GiraffeElephantBoat
sfgsdg sdwerha aswertwe

Lưu ý rằng:

  • tìm kiếm không phân biệt chữ hoa chữ thường (nhờ lower())
  • thay thế một từ bằng ""có thể để lại hai khoảng trắng (như trong mã của bạn)
  • Với python3, \w+cũng khớp các ký tự có dấu (ví dụ "ångström").
  • Bất kỳ ký tự không phải từ nào (tab, dấu cách, dòng mới, dấu, ...) sẽ không bị ảnh hưởng.

Hiệu suất

Có một triệu câu, banned_wordscó gần 100000 từ và kịch bản chạy trong chưa đầy 7 giây.

Để so sánh, câu trả lời của Liteye cần 160 giây cho 10 nghìn câu.

Với ntổng số lượng từ và msố lượng từ bị cấm, mã của OP và Liteye là O(n*m).

Trong so sánh, mã của tôi nên chạy trong O(n+m). Xem xét rằng có nhiều câu hơn các từ bị cấm, thuật toán trở thành O(n).

Kiểm tra công đoàn Regex

Sự phức tạp của tìm kiếm regex với một '\b(word1|word2|...|wordN)\b'mẫu là gì? Là nó O(N)hay O(1)?

Thật khó để nắm bắt cách thức hoạt động của công cụ regex, vì vậy hãy viết một bài kiểm tra đơn giản.

Mã này trích xuất 10**icác từ tiếng Anh ngẫu nhiên vào một danh sách. Nó tạo ra liên kết regex tương ứng và kiểm tra nó bằng các từ khác nhau:

  • một rõ ràng không phải là một từ (nó bắt đầu bằng #)
  • một là từ đầu tiên trong danh sách
  • một là từ cuối cùng trong danh sách
  • một cái trông giống như một từ nhưng không


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

Nó xuất ra:

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

Vì vậy, có vẻ như việc tìm kiếm một từ duy nhất với một '\b(word1|word2|...|wordN)\b'mẫu có:

  • O(1) trường hợp tốt nhất
  • O(n/2) trường hợp trung bình, vẫn còn O(n)
  • O(n) trường hợp xấu nhất

Những kết quả này phù hợp với một tìm kiếm vòng lặp đơn giản.

Một sự thay thế nhanh hơn nhiều cho một liên minh regex là tạo ra mô hình regex từ một trie .


1
Bạn đã đúng. Sự thụt vào của tôi đã sai. Tôi đã sửa nó trong câu hỏi ban đầu. Đối với nhận xét rằng 50 câu / giây là chậm, tất cả những gì tôi có thể nói là tôi đang cung cấp một ví dụ đơn giản. Tập dữ liệu thực phức tạp hơn tôi mô tả, nhưng có vẻ không liên quan. Ngoài ra, việc ghép các "từ" của tôi thành một biểu thức chính duy nhất ồ ạt cải thiện tốc độ. Ngoài ra, tôi đang "vắt kiệt" không gian đôi sau khi thay thế.
pdanese

1
@ user36476 Cảm ơn phản hồi, tôi đã xóa phần tương ứng. Bạn có thể vui lòng thử đề nghị của tôi? Tôi dám nói nó nhanh hơn nhiều so với câu trả lời được chấp nhận.
Eric Duminil

1
Vì bạn đã xóa O(1)yêu cầu gây hiểu lầm đó , câu trả lời của bạn chắc chắn xứng đáng được bình chọn.
idmean

1
@idmean: Đúng vậy, điều đó không rõ ràng lắm. Nó chỉ đề cập đến việc tra cứu: "Từ này có phải là từ bị cấm không?".
Eric Duminil

1
@EricDuminil: Công việc tuyệt vời! Ước gì tôi có thể nâng cấp lần thứ hai.
Matthieu M.

105

TLD

Sử dụng phương pháp này nếu bạn muốn giải pháp dựa trên regex nhanh nhất. Đối với một tập dữ liệu tương tự như của OP, nó nhanh hơn khoảng 1000 lần so với câu trả lời được chấp nhận.

Nếu bạn không quan tâm đến regex, hãy sử dụng phiên bản dựa trên tập hợp này , nhanh hơn 2000 lần so với liên minh regex.

Tối ưu hóa Regex với Trie

Một cách tiếp cận đơn giản của Regex union trở nên chậm chạp với nhiều từ bị cấm, bởi vì công cụ regex không làm tốt công việc tối ưu hóa mẫu.

Có thể tạo Trie với tất cả các từ bị cấm và viết regex tương ứng. Kết quả trie hoặc regex không thực sự có thể đọc được, nhưng chúng cho phép tra cứu và kết hợp rất nhanh.

Thí dụ

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

Công đoàn Regex

Danh sách được chuyển đổi thành một bộ ba:

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

Và sau đó đến mẫu regex này:

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

Regex trie

Ưu điểm rất lớn là để kiểm tra xem có zookhớp hay không, công cụ regex chỉ cần so sánh ký tự đầu tiên (không khớp), thay vì thử 5 từ . Đó là một quá trình tiền xử lý quá mức cho 5 từ, nhưng nó cho thấy kết quả đầy hứa hẹn cho hàng ngàn từ.

Lưu ý rằng (?:)các nhóm không bắt được sử dụng vì:

Đây là một ý chính được sửa đổi một chút , mà chúng ta có thể sử dụng như một trie.pythư viện:

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

Kiểm tra

Đây là một thử nghiệm nhỏ (giống như thử nghiệm này ):

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

Nó xuất ra:

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

Để biết thông tin, regex bắt đầu như thế này:

(?: a (?: (?: \ 's | a (?: \' s | chen | liyah (?: \ 's)? | r (?: dvark (?: (?: \' s | s ))? | trên)) | b (?: \ 's | a (?: c (?: us (?: (?: \' s | es))? | [ik]) | ft | lone (? : (?: \ 's | s))? | ndon (? :(?: ed | ing | ment (?: \' s)? | s))? | s (?: e (? :(?: ment (?: \ 's)? | [ds]))? | h (? :(?: e [ds] | ing))? | ing) | t (?: e (? :(?: ment ( ?: \ 's)? | [DS]))? | ing | toir (?: (?: \' s | s))?)) | b (?: as (?: id)? | e (? : ss (?: (?: \ 's | es))? | y (?: (?: \' s | s))?) | ot (?: (?: \ 's | t (?: \ 's)? | s))? | reviat (?: e [ds]? | i (?: ng | on (?: (?: \' s | s))?)) | y (?: \ ' s)? | \ é (?: (?: \ 's | s))?) | d (?: icat (?: e [ds]? | i (?: ng | on (?: (?: \ 's | s))?)) | Om (?: en (?: (?: \' s | s))? | inal) | u (?: ct (? :(?: ed | i (?: ng | on (?: (?: \ 's | s))?) | hoặc (?: (?: \' s | s))? | s))? | l (?: \ 's)?) ) | e (?: (?: \ 's | am | l (?: (?: \' s | ard | son (?: \ 's)?))? | r (?: deen (?: \ 's)? | nathy (?: \' s)? | ra (?: nt | tion (?: (?: \ 's | s))?)) | t (? :(?: t (?: e (?: r (?: (?: \ 's | s))? | d) | ing | hoặc (?: (?: \'s | s))?) | s))? | yance (?: \ 's)? | d))? | hor (? :(?: r (?: e (?: n (?: ce (? : \ 's)? | t) | d) | ing) | s))? | i (?: d (?: e [ds]? | ing | jan (?: \' s)?) | gail | l (?: ene | it (?: ies | y (?: \ 's)?))) | j (?: ect (?: ly)? | ur (?: ation (?: (?: \' s | s))? | e [ds]? | ing)) | l (?: a (?: tive (?: (?: \ 's | s))? | ze) | e (? :(? : st | r))? | oom | estion (?: (?: \ 's | s))? | y) | m \' s | n (?: e (?: gat (?: e [ds] ? | i (?: ng | on (?: \ 's)?)) | r (?: \' s)?) | ormal (? :(?: it (?: ies | y (?: \ ' s)?) | ly))?) | o (?: ard | de (?: (?: \ 's | s))? | li (?: sh (? :(?: e [ds] | ing ))? | tion (?: (?: \ 's | ist (?: (?: \' s | s))?))?) | mina (?: bl [ey] | t (?: e [ ds]? | i (?: ng | on (?: (?: \ 's | s))?))) | r (?: igin (?: al (?: (?: \' s | s) )? | e (?: (?: \ 's | s))?) | t (? :(?: ed | i (?: ng | on (?: (?: \' s | ist (?: (?: \ 's | s))? | s))? | ve) | s))?) | u (?: nd (? :(?: ed | ing | s))? | t) | ve (?: (?: \ 's | bảng))?) | r (?: a (?: cadabra (?: \' s)? | d (?: e [ds]? | ing) | ham (? : \ 's)? | m (?: (?: \' s | s))? | si (?: on (?: (?: \ 's | s))? | ve (? :(?:\ 's | ly | ness (?: \' s)? | s))?)) | East | idg (?: e (? :(?: ment (?: (?: \ 's | s)) ? | [DS]))? | ing | ment (?: (?: \ 's | s))?) | o (?: ad | gat (?: e [ds]? | i (?: ng | trên (?: (?: \ 's | s))?))) | tăng (? :(?: 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 (?: trên (?: \ '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 (?: trên (?: \ 's)? | sm (?: \' s)?)) | v (?: e [ds]? | ing)) | r (?: b (? :( ?: e (?: n (?: cy (?: \ 's)? | t (?: (?: \' s | s))?) | d) | ing | s))? | pti .. .

Điều này thực sự không thể đọc được, nhưng với danh sách 100000 từ bị cấm, regex Trie này nhanh hơn 1000 lần so với một liên minh regex đơn giản!

Đây là sơ đồ của bộ ba hoàn chỉnh, được xuất với trie-python-graphviz và graphviz twopi:

Nhập mô tả hình ảnh ở đây


Có vẻ như với mục đích ban đầu, không cần phải có một nhóm không bắt giữ. Ít nhất phải đề cập đến ý nghĩa của nhóm không bắt giữ
Xavier Combelle

3
@XavierCombelle: Bạn nói đúng rằng tôi nên đề cập đến nhóm bắt giữ: câu trả lời đã được cập nhật. Mặc dù vậy, tôi thấy nó theo một cách khác: parens là cần thiết cho sự thay thế regex với |nhưng việc bắt các nhóm không cần thiết cho mục đích của chúng ta. Họ chỉ làm chậm quá trình và sử dụng nhiều bộ nhớ hơn mà không có lợi ích.
Eric Duminil

3
@EricDuminil Bài đăng này rất hoàn hảo, cảm ơn bạn rất nhiều :)
Mohamed AL ANI

1
@MohamedALANI: So với giải pháp nào?
Eric Duminil

1
@ PV8: Nó chỉ nên khớp với các từ hoàn chỉnh, vâng, nhờ vào \b( ranh giới từ ). Nếu danh sách là ['apple', 'banana'], nó sẽ thay thế các từ chính xác applehoặc banana, nhưng không nana, banahoặc pineapple.
Eric Duminil

15

Một điều bạn có thể muốn thử là xử lý trước các câu để mã hóa các ranh giới từ. Về cơ bản biến mỗi câu thành một danh sách các từ bằng cách tách trên ranh giới từ.

Việc này sẽ nhanh hơn, vì để xử lý một câu, bạn chỉ cần bước qua từng từ và kiểm tra xem đó có phải là một từ khớp không.

Hiện tại, tìm kiếm regex mỗi lần phải đi qua toàn bộ chuỗi, tìm kiếm các ranh giới từ và sau đó "loại bỏ" kết quả của công việc này trước khi vượt qua lần tiếp theo.


8

Vâng, đây là một giải pháp nhanh chóng và dễ dàng, với bộ thử nghiệm.

Chiến lược chiến thắng:

re.sub ("\ w +", thay thế, câu) tìm kiếm các từ.

"thay thế" có thể là một cuộc gọi. Tôi đã sử dụng một chức năng thực hiện tra cứu dict và dict chứa các từ để tìm kiếm và thay thế.

Đây là giải pháp đơn giản và nhanh nhất (xem hàm thay thế4 trong ví dụ mã bên dưới).

Tốt thứ hai

Ý tưởng là chia các câu thành các từ, sử dụng re.split, trong khi bảo tồn các dấu phân cách để xây dựng lại các câu sau đó. Sau đó, thay thế được thực hiện với một tra cứu dict đơn giản.

(xem chức năng thay thế3 trong mã ví dụ bên dưới).

Thời gian cho các chức năng ví dụ:

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)

... và mã:

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

Chỉnh sửa: Bạn cũng có thể bỏ qua chữ thường khi kiểm tra nếu bạn vượt qua danh sách Câu thường và chỉnh sửa thay thế

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

1
Upvote cho các bài kiểm tra. replace4và mã của tôi có hiệu suất tương tự.
Eric Duminil

Không chắc chắn def repl(m):đang làm gì và cách bạn gán mtrong hàm thay thế4
StatguyUser

Ngoài ra tôi đang gặp lỗi error: unbalanced parenthesischo dòngpatterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
StatguyUser

Trong khi chức năng thay thế 3 và thay thế 4 giải quyết vấn đề ban đầu (để thay thế các từ), thì thay thế 1 và thay thế 2 có mục đích chung hơn, vì chúng hoạt động ngay cả khi kim là một cụm từ (một chuỗi các từ) và không chỉ là một từ.
Zoltan Fedor

7

Có lẽ Python không phải là công cụ phù hợp ở đây. Đây là một với công cụ Unix

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

giả sử tệp danh sách đen của bạn được xử lý trước với các ranh giới từ được thêm vào. Các bước là: chuyển đổi tệp thành khoảng cách gấp đôi, chia mỗi câu thành một từ trên mỗi dòng, xóa hàng loạt các từ trong danh sách đen khỏi tệp và hợp nhất lại các dòng.

Điều này sẽ chạy ít nhất một thứ tự cường độ nhanh hơn.

Để tiền xử lý tệp danh sách đen từ các từ (một từ trên mỗi dòng)

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

4

Còn cái này thì sao:

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

Các giải pháp này phân chia trên ranh giới từ và tìm kiếm từng từ trong một bộ. Chúng phải nhanh hơn re.sub của từ thay thế (giải pháp của Liteyes) vì các giải pháp này O(n)có n là kích thước của đầu vào do amortized O(1)tra cứu tập hợp, trong khi sử dụng thay thế regex sẽ khiến công cụ regex phải kiểm tra xem có khớp từ không trên mỗi ký tự chứ không chỉ trên ranh giới từ. Giải pháp của tôi rất cẩn thận để bảo vệ các khoảng trắng được sử dụng trong văn bản gốc (nghĩa là nó không nén các khoảng trắng và giữ các tab, dòng mới và các ký tự khoảng trắng khác), nhưng nếu bạn quyết định rằng bạn không quan tâm đến nó, thì nó nên khá đơn giản để loại bỏ chúng khỏi đầu ra.

Tôi đã thử nghiệm trên corpus.txt, đó là sự kết hợp của nhiều sách điện tử được tải xuống từ Dự án Gutenberg và cấm_words.txt là 20000 từ được chọn ngẫu nhiên từ danh sách từ của Ubuntu (/ usr / share / dict / American-english). Mất khoảng 30 giây để xử lý 862462 câu (và một nửa số đó trên PyPy). Tôi đã định nghĩa câu là bất cứ điều gì ngăn cách bởi ".".

$ # 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 đặc biệt được hưởng lợi nhiều hơn từ cách tiếp cận thứ hai, trong khi CPython thì tốt hơn ở cách tiếp cận thứ nhất. Đoạn mã trên sẽ hoạt động trên cả Python 2 và 3.


Python 3 là một trong một câu hỏi. Tôi đã ủng hộ điều này nhưng tôi nghĩ rằng có thể đáng để hy sinh một số chi tiết và triển khai 'tối ưu' trong mã này để làm cho nó ít dài dòng hơn.
pvg

Nếu tôi hiểu chính xác, về cơ bản, đó là nguyên tắc giống như câu trả lời của tôi, nhưng dài dòng hơn? Chia tách và tham gia vào \W+cơ bản là giống như subtrên \w+, phải không?
Eric Duminil

Tôi tự hỏi nếu giải pháp của tôi dưới đây (chức năng thay thế 4) nhanh hơn pypy;) Tôi muốn kiểm tra các tệp của bạn!
bobflux

3

Tiếp cận thực tế

Một giải pháp được mô tả dưới đây sử dụng rất nhiều bộ nhớ để lưu trữ tất cả văn bản trong cùng một chuỗi và để giảm mức độ phức tạp. Nếu RAM là một vấn đề hãy suy nghĩ hai lần trước khi sử dụng nó.

Với join/ splitthủ thuật, bạn có thể tránh các vòng lặp để tăng tốc thuật toán.

  • Nối các câu với một dấu phân cách đặc biệt không có trong các câu:
  • merged_sentences = ' * '.join(sentences)

  • Biên dịch một regex duy nhất cho tất cả các từ bạn cần loại bỏ khỏi các câu bằng cách sử dụng |"hoặc" câu lệnh regex:
  • regex = re.compile(r'\b({})\b'.format('|'.join(words)), re.I) # re.I is a case insensitive flag

  • Đăng ký các từ với regex đã biên dịch và phân tách nó bằng ký tự dấu phân cách đặc biệt trở lại các câu tách biệt:
  • clean_sentences = re.sub(regex, "", merged_sentences).split(' * ')

    Hiệu suất

    "".joinđộ phức tạp là O (n). Điều này khá trực quan nhưng dù sao cũng có một trích dẫn rút gọn từ một nguồn:

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

    Do đó, với join/splitbạn có O (từ) + 2 * O (câu) vẫn là độ phức tạp tuyến tính so với 2 * O (N 2 ) với cách tiếp cận ban đầu.


    btw không sử dụng đa luồng. GIL sẽ chặn từng hoạt động vì nhiệm vụ của bạn bị ràng buộc chặt chẽ bởi CPU nên GIL không có cơ hội được phát hành nhưng mỗi luồng sẽ gửi các dấu tick đồng thời gây ra thêm nỗ lực và thậm chí dẫn hoạt động đến vô cùng.


    Trong trường hợp các câu được (được) lưu trữ trong một tệp văn bản, chúng đã được phân tách bằng một dòng mới. Vì vậy, toàn bộ tệp có thể được đọc thành một chuỗi lớn (hoặc bộ đệm), các từ bị xóa và sau đó được viết lại (hoặc điều này có thể được thực hiện trực tiếp trong tệp bằng ánh xạ bộ nhớ). Otoh, để xóa một từ, phần còn lại của chuỗi phải được di chuyển trở lại để lấp đầy khoảng trống, vì vậy đó sẽ là một vấn đề với một chuỗi rất lớn. Một cách khác là viết các phần giữa các từ trở lại một chuỗi hoặc tệp khác (bao gồm các dòng mới) - hoặc chỉ di chuyển các phần đó trong một tệp được dán vào (1) ..
    Danny_ds

    Cách tiếp cận cuối cùng đó (di chuyển / viết các phần giữa các từ) kết hợp với tra cứu tập hợp của Eric Duminil có thể rất nhanh, thậm chí có thể không sử dụng regex. (2)
    Daniel_ds

    .. Hoặc có thể regex đã được tối ưu hóa để chỉ di chuyển các phần đó khi thay thế nhiều từ, tôi không biết.
    Daniel_ds

    0

    Nối tất cả các câu của bạn vào một tài liệu. Sử dụng bất kỳ triển khai thuật toán Aho-Corasick ( đây là một ) để xác định tất cả các từ "xấu" của bạn. Di chuyển tệp, thay thế từng từ xấu, cập nhật phần bù của các từ đã tìm thấy, v.v.

    Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
    Licensed under cc by-sa 3.0 with attribution required.