【问题标题】:Python string occurence count regex performancePython字符串出现计数正则表达式性能
【发布时间】:2021-04-01 20:33:33
【问题描述】:

我被要求找出给定字符串中出现的子字符串(不区分大小写,带/不带标点符号)的总数。 一些例子:

count_occurrences("Text with", "This is an example text with more than +100 lines") # Should return 1
count_occurrences("'example text'", "This is an 'example text' with more than +100 lines") # Should return 1
count_occurrences("more than", "This is an example 'text' with (more than) +100 lines") # Should return 1
count_occurrences("clock", "its 3o'clock in the morning") # Should return 0

我选择了正则表达式而不是 .count(),因为我需要完全匹配,结果是:

def count_occurrences(word, text):
    pattern = f"(?<![a-z])((?<!')|(?<='')){word}(?![a-z])((?!')|(?=''))"
    return len(re.findall(pattern, text, re.IGNORECASE))

我得到了所有匹配的计数,但我的代码占用了0.10secs,而预期时间是0.025secs。我错过了什么吗?有没有更好(性能优化)的方法来做到这一点?

【问题讨论】:

  • 你需要什么额外的匹配?只有不区分大小写?
  • 我已经有了所有的匹配,问题是知道是否有更好的方法来做到这一点。因为这可以使执行时间达到预期的 0.25 秒
  • Regex 通常比人们需要的要多得多。如果您只为不区分大小写的匹配选择正则表达式,text.lower().count(word.lower()) 会快得多。你需要另一个正则表达式吗?或者,您可能会发现杂乱但更具体优化的代码。
  • 看我上面的例子,它的混合(大小写,标点符号,括号)等。如果我选​​择.count,假设txt = "texts texts texts',如果我搜索text,计数将返回3,我不想要那个(它只需要返回一个完全匹配的单词)
  • 是的,当然,就我的目标表现而言..

标签: python regex string performance


【解决方案1】:

好的,我一直在努力让它在没有正则表达式的情况下工作,因为我们都知道正则表达式很慢。这是我想出的:

def count_occurrences(word, text):
    spaces = [' ', '\n', '(', '«', '\u201d', '\u201c', ':', "''", "__"]
    endings = spaces + ['?', '.', '!', ',', ')', '"', '»']
    s = text.lower().split(word.lower())
    l = len(s)
    return sum((
            (i == 0 and (s[0] == '' or any(s[i].endswith(t) for t in spaces)) and (s[1] == '' or any(s[i+1].startswith(t) for t in endings))) 
            or (i == l - 2 and any(s[i].endswith(t) for t in spaces) and (s[i+1] == '' or any(s[i+1].startswith(t) for t in endings)))
            or (i != 0 and i != l - 2 and any(s[i].endswith(t) for t in spaces) and any(s[i+1].startswith(t) for t in endings))
        ) for i in range(l - 1))

整个文件runs in ideone:

Ran 1 test in 0.025s

OK

问题要问的是什么。

逻辑很简单。让我们将textword 分开,两者都小写。现在让我们看看每对邻居。例如,如果索引 0 以一个有效的分隔符结束,而索引 1 以一个有效的分隔符开始,那么让我们将其算作一次出现。让我们做到这一点,直到分裂的最后一对。

由于性能在这里很重要,我们必须注意spacesendings 的顺序。我们基本上是在列表中寻找满足条件的第一个。因此,重要的是首先找到更常见的变量。例如,如果我声明:

spaces = ['(', '«', '\u201d', '\u201c', ':', "''", "__", '\n', ' ']

我的解决方案不是我的解决方案,而是运行 0.036 秒。

例如,如果我声明一个数组:

spaces = [' ', '\n', '(', '«', '\u201d', '\u201c', ':', "''", "__", '?', '.', '!', ',', ')', '"', '»']

它具有所有分隔符并仅使用它,我得到 0.053 秒。这比我的解决方案多 60%。

可能有更好的解决方案,以另一种顺序声明分隔符。

【讨论】:

  • 这看起来有点努力,而且表现不错。让我们看看,如果它满足OP!再加上我。但是,我通常不同意,正则表达式很慢 :) 这取决于。
  • 哇,好快!我认为正则表达式更快,你证明我错了:),我会接受这个作为答案,因为它满足我的性能要求。谢谢!!
【解决方案2】:

如果您搜索的单词是定义的并且是有限的,通过re.compile 进行的正则表达式预编译可以帮助加快速度。 比如:

search_words = [
  'foo',
  'bar',
  'baz',
]

words_to_re = {w: re.compile(f"(?<![a-z])((?<!')|(?<='')){w}(?![a-z])((?!')|(?=''))") for w in search_words}

def count_occurrences(word, text):
    regex = words_to_re[word]
    return len(regex.findall(text))

【讨论】:

  • re.compile(f"(?&lt;![a-z])((?&lt;!')|(?&lt;='')){word}(?![a-z])((?!')|(?=''))", re.IGNORECASE) 试过,但性能很差。花了 +1 秒
  • 你必须预编译一次搜索模式,然后在你的函数中重用它们。
  • 不,我的文字不是预定义的,每次都是通过函数参数count_occurences("different word", "different text to compare +100 line")来的
  • 前段时间预编译比较重要。最后一次看到,re 模块对最近的正则表达式使用 LRU 缓存,因此编译只发生一次。
【解决方案3】:

您可以使用 string.lower() 函数将所有单词手动转换为小写。 检查这个也许这会对你有所帮助:

def count_occurrences2(word, text):
    text = text.lower()
    word = word.lower()
    pattern = f"(?<![a-z])((?<!')|(?<='')){word}(?![a-z])((?!')|(?=''))"
    return len(re.findall(pattern, text))

我已经使用 timeit 库检查了执行时间:

import timeit

def checkTime(word, text, function):
  now = timeit.default_timer()
  function("more than", lines)
  return timeit.default_timer()-now

text = "This is an example 'text' with (more than) +100 lines "*1000
word = "more than"
time_0 = checkTime("more than",text, count_occurrences)
time_1 = checkTime("more than",text, count_occurrences2)
print(time_0)
print(time_1)
print(time_1 < time_0) //true

编辑:

这是另一种方式:

def count_occurences_in_text(word, text):
    pattern = r"(?<![a-z])((?<!')|(?<=''))"+str(word.lower())+"(?![a-z])((?!')|(?=''))"
    line_now = text.lower()
    count = 0
    search = re.search(pattern, line_now)
    while search:
        count +=1
        line_now = line_now[search.span()[1]:]
        search = re.search(pattern,line_now)
    return count

编辑 2:

此函数将传递代码中的所有断言(考虑执行时间):

def count_occurences_in_text(word, text):
    text = text.lower()
    word = word.lower()
    word_len = len(word)
    text_len = len(text)
    if not (word[0] >= 'a' and word[0] <= 'z') :
        word = word[1:word_len]
        if not (word[len(word) - 1] >= 'a' and word[len(word) - 1] <= 'z') :
            word = word[1:len(word)-1]
    count = 0
    index = 0
    have = [' ', ",","!","?",".","\n",":"]
    haveP = [' ',':']
    if word_len > text_len:
        return 0;
    while index < text_len-word_len+1:
        if text[index:index+word_len] == word:
            if index != 0:
                prev_word = text[index-1]
                # if (prev_word >= 'a' and prev_word <= 'z') or prev_word == "'":
                if prev_word not in haveP:
                    if index > 1 and text[index-1] =="'" and text[index-2]=="'":
                        count+=1
                        index += word_len+1
                        continue
                    if index > 1 and text[index-1] =="_" and text[index-2]=="_":
                        count+=1
                        index += word_len+1
                        continue
                    else:
                        index += 1
                        continue
            if index + word_len <= text_len-1:
                last_word = text[index+word_len]
                # if (last_word >= 'a' and last_word <= 'z') or last_word == "'":
                if last_word not in have:
                    if index+word_len <= text_len-2 and last_word =="'" and text[index+word_len+1]=="'":
                        count +=1
                        index += word_len+1
                        continue
                    if index+word_len <= text_len-2 and last_word =="_" and text[index+word_len+1]=="_":
                        count +=1
                        index += word_len+1
                        continue
                    else:
                        index += 1
                        continue
            count += 1
            index += word_len+1
        index += 1
    return count

【讨论】:

  • 它有帮助但不完全,你看到我的问题中的示例文件(链接)了吗?
  • 是的,这里:follow this link,我尝试了另一种方式,
  • 您的第一种方法比第二种方法执行得更好,但我的执行时间0.1728 仍然没有低于0 虽然
  • 我尝试了另一种方法并成功通过了所有断言,但执行时间不符合您的期望。不过,您可以查看此follow this link
【解决方案4】:

使用正则表达式拆分

def count_occurrences(search_word,text):
    alist=re.split(r'\s+',text)
    matches=[word for word in alist if word==search_word]
    return len(matches)

count_occurrences("clock", "its 3o'clock in the morning")

输出

0

【讨论】:

    【解决方案5】:

    第一个错误是在同一个句子中使用了“Python”和“Performance”这两个词。 Python 主要面向“廉价——快速开发代码”的方向,“良好——按预期运行”是可行的。快出来了。这里的任何建议都严格依赖于实现。

    1. 您可以清理正则表达式。我建议使用组快捷方式\b(单词边界)。在所有情况下,我都强烈建议您在 regex101 或等效中以交互方式使用您的正则表达式。

    2. 您可以在 Python 中编写自己的搜索函数。在 Python 中运行会更慢,而跳过匹配项和其他一般性的存储会更快。

    3. 您可以将您的速度与简单的字符串.count() 进行比较。您将需要使用lower() 并确定单词“this”是否匹配“sthisany”。

    4. 您可以将您的测试函数修改为实际上有一百行,例如,text = (text + '\n')*100

    5. 您可以使用 PyPi,它通常会通过牺牲一些启动时间和一些元编程灵活性来加快您的执行速度。

    6. 您可以编写一小段 C 代码 sn-p 并学习从 Python 中调用它。玩得开心。

    我建议您保留笔记和比较,并将它们与您的家庭作业一起提交,而不仅仅是最终产品。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-10-17
      • 2013-02-15
      • 1970-01-01
      • 1970-01-01
      • 2012-05-28
      • 1970-01-01
      • 1970-01-01
      • 2017-02-09
      相关资源
      最近更新 更多