เร่งความเร็วในการแทนที่ regex หลายล้านครั้งใน 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 ที่คุณเรียกใช้เธรดจำนวนมากนั้น
Mohammad Ali

4
คุณยังสามารถลองใช้งานที่ไม่ใช่ regex - สำรวจคำที่คุณป้อนทีละคำและจับคู่ทุกคำด้วยชุด นี่คือการผ่านเพียงครั้งเดียวและการค้นหาแฮชทำได้ค่อนข้างรวดเร็ว
pvg

2
ประโยคเหล่านี้ยาวแค่ไหนโดยบังเอิญ? 750k บรรทัดไม่เหมือนชุดข้อมูลที่ควรใช้เวลาดำเนินการหลายชั่วโมง
pvg

2
@MohammadAli: อย่ากังวลกับตัวอย่างนั้นสำหรับการทำงานที่เชื่อมต่อกับ CPU Python มีการล็อกขนาดใหญ่ที่ต้องใช้เมื่อเรียกใช้ bytecode (Global Interpreter Lock) ดังนั้นคุณจึงไม่ได้รับประโยชน์จากเธรดสำหรับการทำงานของ CPU คุณจำเป็นต้องใช้multiprocessing(เช่นกระบวนการ Python หลายขั้นตอน)
Kevin

1
คุณต้องมีเครื่องมือสร้างความแข็งแกร่งทางอุตสาหกรรมเพื่อทำสิ่งนี้ regex trie ถูกสร้างขึ้นจาก ternary tree ของรายการสตริง มีขั้นตอนในการล้มเหลวไม่เกิน 5 ขั้นตอนทำให้วิธีนี้เป็นวิธีที่เร็วที่สุดในการจับคู่ประเภทนี้ ตัวอย่าง: พจนานุกรมคำ 175,000 คำหรือคล้ายกับรายการต้องห้ามของคุณเพียง20,000 S-words
x15

คำตอบ:


123

"\b(word1|word2|word3)\b"สิ่งหนึ่งที่คุณสามารถลองคือการรวบรวมรูปแบบหนึ่งเดียวเช่น

เนื่องจากการreใช้รหัส C ในการจับคู่จริงจึงสามารถประหยัดได้มาก

ดังที่ @pvg ได้ระบุไว้ในความคิดเห็นนอกจากนี้ยังได้รับประโยชน์จากการจับคู่บัตรเดียว

หากคำพูดของคุณไม่ใช่นิพจน์ทั่วไปคำตอบของ Eric ก็เร็วกว่า


4
ไม่ใช่แค่ C im (ซึ่งสร้างความแตกต่างอย่างมาก) แต่คุณยังจับคู่กับบัตรผ่านเดียว รูปแบบของคำถามนี้เกิดขึ้นบ่อยครั้งเป็นเรื่องแปลกเล็กน้อยที่ไม่มี (หรืออาจจะมีซ่อนอยู่ที่ไหนสักแห่ง?) คำตอบ SO ที่เป็นที่ยอมรับด้วยแนวคิดที่สมเหตุสมผลนี้
pvg

40
@Liteye คำแนะนำของคุณเปลี่ยนงาน 4 ชั่วโมงให้กลายเป็นงาน 4 นาที! ฉันสามารถรวม regexes ทั้งหมด 20,000+ รายการไว้ใน regex ขนาดมหึมาเพียงเครื่องเดียวและแล็ปท็อปของฉันก็ไม่สะดุดสายตา ขอบคุณอีกครั้ง.
pdanese

2
@ บาคุริว: s/They actually use/They actually could in theory sometimes use/. คุณมีเหตุผลที่จะเชื่อว่าการใช้งาน Python กำลังทำสิ่งอื่นนอกเหนือจากการวนซ้ำที่นี่หรือไม่?
user541686

2
@ บาคุริว: ฉันสนใจจริงๆที่จะรู้ว่าเป็นเช่นนั้นหรือไม่ แต่ฉันไม่คิดว่าโซลูชัน regex ต้องใช้เวลาเชิงเส้น ถ้ามันไม่สร้าง Trie ออกจากสหภาพฉันก็ไม่เห็นว่ามันจะเกิดขึ้นได้อย่างไร
Eric Duminil

2
@ บาคุริว: นั่นไม่ใช่เหตุผล ฉันถามว่าคุณมีเหตุผลที่จะเชื่อว่าการติดตั้งใช้งานได้จริงหรือไม่ไม่ใช่ว่าคุณมีเหตุผลที่จะเชื่อว่ามันสามารถทำงานได้ โดยส่วนตัวแล้วฉันยังไม่พบการใช้ regex ภาษาการเขียนโปรแกรมหลักเดียวที่ทำงานในเวลาเชิงเส้นแบบเดียวกับที่คุณคาดหวังว่าจะมี regex คลาสสิกดังนั้นหากคุณรู้ว่า Python ทำสิ่งนี้คุณควรแสดงหลักฐานบางอย่าง
user541686

123

TLDR

ใช้วิธีนี้ (พร้อมการค้นหาชุด) หากคุณต้องการวิธีแก้ปัญหาที่เร็วที่สุด สำหรับชุดข้อมูลที่คล้ายกับ OP จะเร็วกว่าคำตอบที่ยอมรับประมาณ 2000 เท่า

หากคุณยืนยันที่จะใช้ regex ในการค้นหาให้ใช้เวอร์ชันที่ใช้trieซึ่งยังเร็วกว่าการรวม regex ถึง 1,000 เท่า

ทฤษฎี

หากประโยคของคุณไม่ใช่สตริงที่น่าฟังก็อาจเป็นไปได้ที่จะประมวลผลมากกว่า 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 วินาที

ในการเปรียบเทียบคำตอบของ Liteye ต้องการ 160 วินาทีสำหรับ 10,000 ประโยค

ด้วยnการเป็น amound รวมของคำและmจำนวนของคำห้ามของ OP และรหัส Liteye O(n*m)เป็น

O(n+m)ในการเปรียบเทียบรหัสของฉันควรทำงานใน พิจารณาว่ามีประโยคอื่น ๆ O(n)อีกมากมายกว่าคำสั่งห้ามอัลกอริทึมจะกลายเป็น

การทดสอบสหภาพ Regex

ความซับซ้อนของการค้นหา regex ด้วย'\b(word1|word2|...|wordN)\b'รูปแบบคืออะไร? มันO(N)หรือO(1)?

ค่อนข้างยากที่จะเข้าใจวิธีการทำงานของเอนจิ้น regex ดังนั้นเรามาเขียนการทดสอบง่ายๆ

รหัสนี้แยก10**iคำภาษาอังกฤษแบบสุ่มลงในรายการ สร้างสหภาพ regex ที่สอดคล้องกันและทดสอบด้วยคำที่แตกต่างกัน:

  • ชัดเจนว่าไม่ใช่คำ (ขึ้นต้นด้วย#)
  • คำแรกคือคำแรกในรายการ
  • หนึ่งคือคำสุดท้ายในรายการ
  • คำหนึ่งดูเหมือนคำ แต่ไม่ใช่


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) กรณีที่เลวร้ายที่สุด

ผลลัพธ์เหล่านี้สอดคล้องกับการค้นหาแบบวนซ้ำอย่างง่าย

เร็วทางเลือกให้กับสหภาพ regex คือการสร้างรูปแบบการ regex จาก Trie


1
คุณถูกต้อง การเยื้องของฉันไม่ถูกต้อง ฉันแก้ไขในคำถามเดิม สำหรับความคิดเห็นที่ว่า 50 ประโยค / วินาทีช้าทั้งหมดที่ฉันพูดได้คือฉันกำลังยกตัวอย่างให้เข้าใจง่าย ชุดข้อมูลจริงซับซ้อนกว่าที่ฉันอธิบาย แต่ดูเหมือนจะไม่เกี่ยวข้อง นอกจากนี้การต่อ "คำ" ของฉันให้เป็นนิพจน์เดียวช่วยเพิ่มความเร็วอย่างมาก นอกจากนี้ฉันกำลัง "บีบ" ช่องว่างสองครั้งหลังจากการเปลี่ยน
pdanese

1
@ user36476 ขอบคุณสำหรับคำติชมฉันลบส่วนที่เกี่ยวข้องออก คุณช่วยลองคำแนะนำของฉันได้ไหม ฉันกล้าพูดว่ามันเร็วกว่าคำตอบที่ยอมรับมาก
Eric Duminil

1
เนื่องจากคุณลบคำO(1)กล่าวอ้างที่ทำให้เข้าใจผิดคำตอบของคุณจึงสมควรได้รับการโหวต
idmean

1
@idmean: จริงไม่ชัดเจนมาก มันหมายถึงการค้นหา: "คำนี้เป็นคำต้องห้ามหรือไม่".
Eric Duminil

1
@EricDuminil: เยี่ยมมาก! หวังว่าฉันจะเพิ่มคะแนนเป็นครั้งที่สอง
Matthieu M.

105

TLDR

ใช้วิธีนี้หากคุณต้องการโซลูชันที่ใช้ regex ที่เร็วที่สุด สำหรับชุดข้อมูลที่คล้ายกับ OP จะเร็วกว่าคำตอบที่ยอมรับประมาณ 1000 เท่า

หากคุณไม่สนใจเกี่ยวกับ regex ให้ใช้เวอร์ชันที่ใช้ชุดนี้ซึ่งเร็วกว่าสหภาพ regex ถึง 2,000 เท่า

ปรับปรุง Regex ด้วย Trie

ง่าย Regex สหภาพวิธีการจะช้าด้วยคำพูดต้องห้ามจำนวนมากเนื่องจากเครื่องยนต์ regex ไม่ไม่ได้ทำงานที่ดีมากของการเพิ่มประสิทธิภาพรูปแบบ

เป็นไปได้ที่จะสร้างTrieด้วยคำที่ต้องห้ามทั้งหมดและเขียน regex ที่เกี่ยวข้อง Trie หรือ regex ที่เป็นผลลัพธ์นั้นไม่สามารถอ่านได้โดยมนุษย์จริง ๆ แต่มันช่วยให้สามารถค้นหาและจับคู่ได้อย่างรวดเร็ว

ตัวอย่าง

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

สหภาพ Regex

รายการถูกแปลงเป็น trie:

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

จากนั้นไปยังรูปแบบ regex นี้:

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

Regex Trie

ข้อได้เปรียบอย่างมากคือการที่จะทดสอบว่าzooไม้ขีดไฟเครื่องยนต์ regex เพียงต้องการที่จะเปรียบเทียบตัวอักษรตัวแรก (มันไม่ตรง) แทนการพยายามที่ 5 คำ เป็นการใช้คำเกินขั้นตอนล่วงหน้าสำหรับ 5 คำ แต่แสดงผลลัพธ์ที่มีแนวโน้มสำหรับคำหลายพันคำ

โปรดทราบว่า(?:)กลุ่มที่ไม่จับภาพถูกใช้เนื่องจาก:

  • foobar|bazจะตรงfoobarหรือbaz, แต่ไม่foobaz
  • foo(bar|baz)จะบันทึกข้อมูลที่ไม่จำเป็นไปยังกลุ่มจับ

รหัส

นี่คือส่วนสำคัญที่แก้ไขเล็กน้อยซึ่งเราสามารถใช้เป็น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

สำหรับข้อมูล regex เริ่มต้นดังนี้:

(ที่: ((: \ 's | A (: \'?? s | chen | liyah (: \ 's) | R (:? dvark ((:? \' s | s )) | เมื่อ)) | B? (: \ 's | A (? C (: เรา ((: \?' s | e)) | [IK]) | ฟุต | คนเดียว (? (: \ 's | s)?) | ndon (? :( ?: เอ็ด | ไอเอ็นจี | ment (: \' s) |? s)) | s (: E (:( ?:? ment: | [ds])) | h (:( ?: อี [ds] | ไอเอ็นจี)) | ไอเอ็นจี) | T ((\ 's?):?? E (:( ?: ment ( ?: \ 's) | [ds])) | ไอเอ็นจี | toir (:? (: \?' s | s)))) | ข (: เป็น (??? id) | E (? : ss ((:? \ 's | e))? | Y ((: \?' |) | OT (s s)): (:? \ 's | ตัน (: \ 's) | s)) | reviat? (: อี [ds] | ฉัน (:? ng | เมื่อ (: (: \?' s | s)))) | Y (:? \' ? s) | \ é ((:? \ 's | s)?)) | d (: ICAT (อี [ds] | ฉัน (:? ng | เมื่อ (:? (: \ 's | s)))) | อ้อม (: TH ((: \?' s | s)) | Inal) | U (:? กะรัต (:( ?: เอ็ด | ฉัน (?: งะ | เมื่อ ((: \? 's | s))) | หรือ? ((:? \' s | s)????) | s)) | ลิตร (: \ 's)) ) | E ((:? \ 's | น | ลิตร ((:? \' s | อาด | ลูกชาย (:? \ 's))) | R (:? Deen (: \ 's) | nathy? (: \' s) | RA (:? NT | tion ((: \ 's | s))?)) | ตัน (:( ?: ตัน (?: E (: R ((: \? 's | s)) | d?) | ไอเอ็นจี | หรือ (:? (: \'s | s))) | S)) | Yance (:? \ 's) | d)) | hor (:( ?: R (??? E (: n (: CE (? : \ 's) | t) | d) | ไอเอ็นจี) | s)) | ฉัน (:? d (อี [ds] | ไอเอ็นจี | มกราคม (:? \' s?)) | gail | ลิตร (: ene | มัน (:? โอบอุ้ม | Y (: \ 's))?) | J | ur ((: ect (Ly?)?:? ation ((: \?)' s | s)) | E [ds] | ไอเอ็นจี)) | ลิตร (?:? (ที่: Tive ((:?? \ 's | s)) | ZE) | E (:(? : ST | R)) | ชอุ่ม | ution (:? (?:? \ 's? | s)) | y) | ม. \' s | n (: E (: GAT (อี [ds] ? | ฉัน (: ng | เมื่อ (: \ 's)?) | R (?: \?)' s)) | Ormal (:( ?: มัน (:? โอบอุ้ม | Y (:? \' s)) | Ly))) | o (:? อาด | เดอ ((:?? \ 's | s)) | li (: ดวลจุดโทษ (:( ?: อี [ds] | ไอเอ็นจี )) | tion? (:? (: \ 's | ist ((: \?' s | s))))) | มินา (:? BL [EY] | ตัน (?: E [ ds] | I? (: ng | เมื่อ (:? (: \ 's | s))))) | R (?:? igin (: อัล ((: \' s | s) ) | E? (:? (: \ 's | s))) | ตัน (:( ?: เอ็ด | ฉัน (:? ng | เมื่อ (: (:? \' s | ist (?: ) | s)) | ve) | s))) | U (| (\ 's s?):?? ND (:( ?: เอ็ด | ไอเอ็นจี | s)) | t) | ve ((:? \ 's | บอร์ด))) | R (:? (ที่: Cadabra (: \' s) | d (:? E [ds] | ไอเอ็นจี) | แฮม (? : \ 's) | เมตร ((:? \' s | s)?) | si (: บน ((:? \ 's | s)) |? ve (:( ?:?\ 's | Ly | Ness (: \' | s)))) | ตะวันออก | IDG (s):? E (:( ?: ment ((:? \ 's | s)) ? | [ds])) | ไอเอ็นจี | ment (:? (: \ 's | s))) | o (:? โฆษณา | GAT (อี [ds] | ฉัน (:? ng | เมื่อวันที่ ((: \? 's | s))))) | UPT (:( ?: อี (?:? ST | R) | Ly | Ness (: \' s)))) | s (:? Alom | C (: ESS ((: \ 's | E [ds] | ไอเอ็นจี)) | issa (:? (: \'? s | [e])) | ond)) | th (:( ?: เอ็ด | | ไอเอ็นจี s?)? (: CE ((:? \ 's | s)???) | ตัน (:( ?: อิเล็กทรอนิกส์ (อี ( : (: \ 's | ISM (: \' s) | s?)) | d) | ไอเอ็นจี | Ly | s))) | inth (:? (:? \ 's | E ( ?: \ 's))) | o? (:? ลิตร (: ยูทาห์ (: E ((: \?' s | Ly | ST)?) | ฉัน (:? ใน (?: \ 's) | เอสเอ็ม (: \' s?))) | v (อี [ds] | ไอเอ็นจี)) | R (:? B (:( ?: อี (: n (? : cy (: \ '| T s)? ((:? \' s | s))?) | d) | ไอเอ็นจี | s)) | PTI ...s | [e])) | ond (:( ?: เอ็ด | ไอเอ็นจี | s))) | th (?:? CE ((:? \ 's | S)) | ตัน (?: (อี (: E ((:?? \ 's? | ISM (: \'? s) | s)) | d) | ไอเอ็นจี | Ly | s))) | inth (?: (: \ 's | E (: \' s)?)) | o (:? ลิตร (: ยูทาห์ (: E ((: \ 's | Ly | ST))???? | ฉัน (: บน (: \ 's) | เอสเอ็ม (:? \'? s))?) | v (อี [ds] | ไอเอ็นจี)) | R (:? B (:( : E (: n (: cy (: \ 's) | ตัน (: (:? \'?? s | s))) | d)? | ไอเอ็นจี | s)) | PTI .. .s | [e])) | ond (:( ?: เอ็ด | ไอเอ็นจี | s))) | th (?:? CE ((:? \ 's | S)) | ตัน (?: (อี (: E ((:?? \ 's? | ISM (: \'? s) | s)) | d) | ไอเอ็นจี | Ly | s))) | inth (?: (: \ 's | E (: \' s)?)) | o (:? ลิตร (: ยูทาห์ (: E ((: \ 's | Ly | ST))???? | ฉัน (: บน (: \ 's) | เอสเอ็ม (:? \'? s))?) | v (อี [ds] | ไอเอ็นจี)) | R (:? B (:( : E (: n (: cy (: \ 's) | ตัน (: (:? \'?? s | s))) | d)? | ไอเอ็นจี | s)) | PTI .. .

มันอ่านไม่ออกจริงๆ แต่สำหรับรายการคำต้องห้าม 100,000 คำ Trie regex นี้เร็วกว่าสหภาพ regex ธรรมดาถึง 1,000 เท่า!

นี่คือแผนภาพของ trie ที่สมบูรณ์ซึ่งส่งออกด้วยtrie-python-graphvizและ graphviz twopi:

ใส่คำอธิบายภาพที่นี่


ดูเหมือนว่าเพื่อจุดประสงค์ดั้งเดิมไม่จำเป็นต้องมีกลุ่มที่ไม่จับภาพ อย่างน้อยควรกล่าวถึงความหมายของกลุ่มที่ไม่ได้จับ
Xavier Combelle

3
@XavierCombelle: คุณพูดถูกที่ฉันควรพูดถึงกลุ่มจับภาพ: คำตอบได้รับการอัปเดตแล้ว ฉันเห็นมันในทางกลับกัน: parens จำเป็นสำหรับการสลับ regex ด้วย|แต่การจับกลุ่มไม่จำเป็นสำหรับจุดประสงค์ของเราเลย พวกเขาแค่ทำให้กระบวนการช้าลงและใช้หน่วยความจำมากขึ้นโดยไม่เกิดประโยชน์
Eric Duminil

3
@EricDuminil โพสต์นี้สมบูรณ์แบบขอบคุณมาก :)
Mohamed AL ANI

1
@ MohamedALANI: เปรียบเทียบกับโซลูชันใด?
Eric Duminil

1
@ PV8: ควรจับคู่คำที่สมบูรณ์เท่านั้นใช่ขอบคุณ\b( ขอบเขตคำ ) ถ้ารายการเป็นที่['apple', 'banana']มันจะเข้ามาแทนที่คำที่จะตรงappleหรือbananaแต่ไม่nana, หรือbana pineapple
Eric Duminil

15

สิ่งหนึ่งที่คุณอาจต้องการลองคือการประมวลผลประโยคล่วงหน้าเพื่อเข้ารหัสขอบเขตของคำ โดยทั่วไปจะเปลี่ยนแต่ละประโยคให้เป็นรายการคำโดยแบ่งขอบเขตของคำ

สิ่งนี้ควรจะเร็วกว่าเนื่องจากในการประมวลผลประโยคคุณเพียงแค่ต้องผ่านแต่ละคำและตรวจสอบว่าตรงกันหรือไม่

ขณะนี้การค้นหา regex จะต้องผ่านสตริงทั้งหมดอีกครั้งในแต่ละครั้งโดยมองหาขอบเขตของคำจากนั้นจึง "ละทิ้ง" ผลลัพธ์ของงานนี้ก่อนที่จะผ่านไป


8

นี่เป็นวิธีแก้ปัญหาที่ง่ายและรวดเร็วพร้อมชุดทดสอบ

กลยุทธ์การชนะ:

re.sub ("\ w +", repl, ประโยค) ค้นหาคำ

"repl" สามารถเรียกได้ ฉันใช้ฟังก์ชั่นที่ทำการค้นหาแบบเขียนตามคำบอกและมีคำที่ต้องการค้นหาและแทนที่

นี่เป็นวิธีแก้ปัญหาที่ง่ายและเร็วที่สุด (ดูฟังก์ชันแทนที่ 4 ในโค้ดตัวอย่างด้านล่าง)

ดีที่สุดเป็นอันดับสอง

แนวคิดคือการแยกประโยคออกเป็นคำโดยใช้ re.split ในขณะที่สงวนตัวคั่นเพื่อสร้างประโยคใหม่ในภายหลัง จากนั้นการแทนที่จะทำได้ด้วยการค้นหาแบบง่ายๆ

(ดูฟังก์ชันแทนที่ 3 ในโค้ดตัวอย่างด้านล่าง)

การกำหนดเวลาสำหรับฟังก์ชันตัวอย่าง:

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และรหัสของฉันมีการแสดงที่คล้ายกัน
Eric Duminil

ไม่แน่ใจว่า def repl(m):กำลังทำอะไรและคุณกำหนดอย่างไรmในฟังก์ชัน replace4
StatguyUser

นอกจากนี้ฉันได้รับข้อผิดพลาดerror: unbalanced parenthesisสำหรับบรรทัดpatterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
StatguyUser

ในขณะที่ฟังก์ชัน replace3 และ replace4 ระบุถึงปัญหาเดิม (เพื่อแทนที่คำ) การแทนที่ 1 และการแทนที่ 2 นั้นมีวัตถุประสงค์ทั่วไปมากกว่าเนื่องจากการทำงานเหล่านี้แม้ว่าเข็มจะเป็นวลี (ลำดับของคำ) ไม่ใช่แค่คำเดียว
Zoltan Fedor

7

บางที 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

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)

โซลูชันเหล่านี้แบ่งขอบเขตของคำและค้นหาแต่ละคำในชุด ควรจะเร็วกว่า re รองของคำทางเลือก (โซลูชันของ Liteyes) เนื่องจากโซลูชันเหล่านี้คือO(n)โดยที่ n คือขนาดของอินพุตเนื่องจากการamortized O(1)ค้นหาชุดในขณะที่การใช้ regex ทางเลือกจะทำให้เอนจิน regex ต้องตรวจสอบคำที่ตรงกัน ในทุกตัวอักษรแทนที่จะเป็นเพียงขอบเขตคำ วิธีแก้ปัญหาของฉันใช้ความระมัดระวังเป็นพิเศษในการรักษาช่องว่างที่ใช้ในข้อความต้นฉบับ (กล่าวคือไม่บีบอัดช่องว่างและรักษาแท็บขึ้นบรรทัดใหม่และอักขระช่องว่างอื่น ๆ ) แต่ถ้าคุณตัดสินใจว่าคุณไม่สนใจมัน ควรจะค่อนข้างตรงไปตรงมาในการลบออกจากเอาต์พุต

ฉันทดสอบบน corpus.txt ซึ่งเป็นการเชื่อมต่อของ eBook หลาย ๆ เล่มที่ดาวน์โหลดจากโครงการ Gutenberg และ ban_words.txt คือ 20000 คำที่สุ่มเลือกจากรายการคำของ Ubuntu (/ usr / share / dict / american-english) ใช้เวลาประมาณ 30 วินาทีในการประมวลผล 862462 ประโยค (และครึ่งหนึ่งใน PyPy) ฉันได้กำหนดประโยคเป็นอะไรก็ได้โดยคั่นด้วย "."

$ # 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 ได้รับในคำถาม ฉันเพิ่มคะแนนสิ่งนี้ แต่ฉันคิดว่ามันอาจจะคุ้มค่าที่จะเสียสละรายละเอียดบางอย่างและการใช้งาน 'ที่ดีที่สุด' ในโค้ดนี้เพื่อให้มีรายละเอียดน้อยลง
pvg

ถ้าฉันเข้าใจถูกต้องก็เป็นหลักการเดียวกันกับคำตอบของฉัน แต่มีรายละเอียดมากกว่า? การแยกและการเข้าร่วม\W+นั้นโดยพื้นฐานแล้วจะเหมือนกับsubบน\w+ใช่ไหม?
Eric Duminil

ฉันสงสัยว่าโซลูชันของฉันด้านล่าง (function replace4) เร็วกว่า pypy หรือไม่) ฉันต้องการทดสอบไฟล์ของคุณ!
bobflux

3

แนวทางปฏิบัติ

โซลูชันที่อธิบายด้านล่างนี้ใช้หน่วยความจำจำนวนมากในการจัดเก็บข้อความทั้งหมดในสตริงเดียวกันและเพื่อลดระดับความซับซ้อน หาก RAM เป็นปัญหาให้คิดสองครั้งก่อนใช้งาน

ด้วย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

  • สมัครสมาชิกคำด้วย regex ที่คอมไพล์แล้วแยกตามอักขระตัวคั่นพิเศษกลับไปที่ประโยคที่คั่น:
  • 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 จะบล็อกแต่ละการดำเนินการเนื่องจากงานของคุณถูกผูกไว้กับ CPU อย่างเคร่งครัดดังนั้น GIL ​​จึงไม่มีโอกาสได้รับการปลดปล่อย แต่แต่ละเธรดจะส่งเห็บพร้อมกันซึ่งทำให้เกิดความพยายามมากขึ้นและยังนำการดำเนินการไปสู่อนันต์


    ในกรณีที่ (ถูก) เก็บไว้ในไฟล์ข้อความประโยคจะถูกคั่นด้วยขึ้นบรรทัดใหม่แล้ว ดังนั้นไฟล์ทั้งหมดสามารถอ่านเป็นสตริงขนาดใหญ่ (หรือบัฟเฟอร์) หนึ่งคำลบออกแล้วเขียนกลับอีกครั้ง (หรือสามารถทำได้ในไฟล์โดยตรงโดยใช้การแมปหน่วยความจำ) โอเพื่อลบคำส่วนที่เหลือของสตริงจะต้องถูกย้ายกลับเพื่อเติมช่องว่างดังนั้นจะเป็นปัญหากับสตริงที่มีขนาดใหญ่มาก อีกทางเลือกหนึ่งคือเขียนส่วนระหว่างคำกลับไปยังสตริงหรือไฟล์อื่น (ซึ่งจะรวมขึ้นบรรทัดใหม่) หรือเพียงแค่ย้ายส่วนเหล่านั้นในไฟล์ mmapped (1) ..
    Danny_ds

    .. แนวทางสุดท้ายนั้น (การย้าย / เขียนส่วนระหว่างคำ) รวมกับการค้นหาชุดของ Eric Duminilอาจทำได้เร็วมากบางทีอาจจะไม่ต้องใช้ regex เลยด้วยซ้ำ (2)
    Danny_ds

    .. หรือบางที regex ได้รับการปรับให้เหมาะสมแล้วให้ย้ายเฉพาะส่วนเหล่านั้นเมื่อแทนที่คำหลายคำฉันไม่รู้
    Danny_ds

    0

    เชื่อมประโยคทั้งหมดของคุณเข้าด้วยกันเป็นเอกสารเดียว ใช้อัลกอริทึม Aho-Corasick ( นี่คือหนึ่ง ) เพื่อค้นหาคำที่ "ไม่ดี" ทั้งหมดของคุณ สำรวจไฟล์แทนที่คำที่ไม่ดีแต่ละคำอัปเดตการชดเชยของคำที่พบที่ตามมาเป็นต้น

    โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
    Licensed under cc by-sa 3.0 with attribution required.