regex 匹配字符串中的多个子字符串,有什么比在循环中搜索每个子字符串更好的方法吗?

vkc1a9a2  于 2023-04-22  发布在  其他
关注(0)|答案(4)|浏览(153)

我正在使用Python,我想匹配一个给定的字符串与多个子字符串。我试图用两种不同的方法解决这个问题。我的第一个解决方案是将子字符串与字符串匹配,如下所示:

str = "This is a test string from which I want to match multiple substrings"
value = ["test", "match", "multiple", "ring"]
temp = []
temp.extend([x.upper() for x in value if x.lower() in str.lower()])
print(temp)

这导致temp = ["TEST", "MATCH", "MULTIPLE", "RING"]
但是,这不是我想要的结果,子字符串应该有精确的匹配,所以“环”不应该与“字符串”匹配。
这就是为什么我试图用正则表达式来解决这个问题,就像这样:

str = "This is a test string from which I want to match multiple substrings"
value = ["test", "match", "multiple", "ring"]
temp = []
temp.extend([
    x.upper() for x in value
    if regex.search(
        r"\b" + regex.escape(x) + r"\b", str, regex.IGNORECASE
    ) is not None
])
print(temp)

这导致["TEST", "MATCH", "MULTIPLE"],正确的解决方案。
尽管如此,这个解决方案的计算时间太长了。我必须对大约100万个字符串进行检查,使用正则表达式的解决方案需要几天才能完成,而使用第一个解决方案需要1.5小时。
有没有办法让第一个解决方案工作,或者让第二个解决方案运行得更快?
value也可以包含数字,或者像“test1 test2”这样的短语。

9w11ddsr

9w11ddsr1#

在没有看到实际数据的情况下,很难提出最佳解决方案,但您可以尝试以下方法:

  • 生成一个匹配所有值的模式。这样你只需要搜索字符串一次(而不是每个值搜索一次)。
  • 跳过转义值,除非它们包含特殊字符(如'^''*')。
  • 使用列表解析创建结果列表,而不是重复调用list.extend()
# 'str' is a built-in function, so use another name instead
string = 'A Test test string from which I want to match multiple substrings'
values = ['test', 'test2', 'Multiple', 'ring', 'match']
pattern = r'\b({})\b'.format('|'.join(map(re.escape, values)))

# unique matches, uppercased
matches = set(map(str.upper, re.findall(pattern, string, regex.IGNORECASE)))

# arrange the results as they appear in `values`
matched_values = [v for value in values if (v := value.upper()) in matches]
print(matched_values)  # ['TEST', 'MULTIPLE', 'MATCH']
kq4fsx7k

kq4fsx7k2#

我想到了两种可能的优化:

  • 使用re.compile预编译模式,这样每次调用match时它就不会重新编译。
  • 创建一个匹配所有值的正则表达式,而不是匹配四个独立的正则表达式。
import re

str = "This is a test string from which I want to match test1 test2 multiple substrings"
values = ["test", "match", "multiple", "ring", "test1 test2"]

pattern = re.compile("|".join(r"\b" + re.escape(x) + r"\b" for x in values))
temp = []

temp.extend([x.upper() for x in pattern.findall(str, re.IGNORECASE)])
print(temp)

结果:

['TEST', 'MATCH', 'TEST1 TEST2', 'MULTIPLE']

这种方法的潜在缺点:

  • 输出可能会以不同的顺序进行。您原来的方法将结果按它们在values中出现的顺序放置。这种方法将结果按它们在str中出现的顺序放置。
  • 如果相同的值在str中出现多次,则它将在temp中出现多次。与您最初的方法相反,该值在temp中最多出现一次。
  • search在找到匹配项后立即终止。findall总是搜索整个字符串。如果您希望大多数字符串匹配value中的每个单词,并且希望大多数匹配项出现在字符串的早期,那么findall可能比search慢。另一方面,如果您希望搜索经常出现None,那么findall将可能稍微更快。
ycggw6v2

ycggw6v23#

您可以按空格分割字符串,然后将value中的元素与==匹配。
你说values中的一些字符串可以在它们之前或之后有空格。你可以用这行来解决这个问题:

values = [i.strip() for i in values]

这将删除字符串前后的所有空白字符(在您的情况下,每个元素都是空白字符)。
此外,你提到如果你按空格分割字符串,一些单词会有分割后留下的标点符号,例如'Hi, how are you?'会导致['Hi,', 'how', 'are', 'you?']。你可以通过使用字符串startswith()方法来过滤所有以values中的元素开头的单词来解决这个问题,如下所示:

str = ['Hi,', 'how', 'are', 'you?']`
values = ['how', 'you', 'time', 'space']

new_str = []
for word in str:
  for j in values:
    if word.startswith(j):
      new_str.append(word)

# result -> ['how', 'you?']

然后你可以用正则表达式从结果列表中删除标点符号,但是现在你需要迭代的列表要小得多。在你删除所有标点符号字符之后,你就可以像我建议的那样匹配整个字符串了。

ylamdve6

ylamdve64#

在我的笔记本电脑上使用以下代码执行100万次'statement'变量大约需要3秒:

from timeit import timeit
import re

# I inserted punctuation to cover more situations
string = """
This, is a ; test string from which I want to match multiple substrings
"""
substrings = ['test', 'match', 'multiple', 'ring']

statement = """
string_normalized = (re.sub(r'[^\w\s]', '', string)
                       .strip()
                       .lower()
                       .split(' '))
result = list(filter(lambda x: x in string_normalized, substrings))
"""

if __name__ == '__main__':
    time_it_takes = timeit(stmt=statement, globals=globals(), number=1_000_000)
    print(time_it_takes)

相关问题