【问题标题】:Python Regex, how to substitute multiple occurrences with a single pattern?Python 正则表达式,如何用单个模式替换多次出现?
【发布时间】:2021-12-13 14:57:46
【问题描述】:

我正在尝试制作一个模糊的自动完成建议框,以突出显示带有 HTML 标记的搜索字符

例如,如果用户输入“ldi”并且其中一个建议是“Leonardo DiCaprio”,那么期望的结果是“Leonardo D卡普里奥”。每个字符的第一次出现按出现顺序突出显示。

我现在正在做的是:

def prototype_finding_chars_in_string():
    test_string_list = ["Leonardo DiCaprio", "Brad Pitt","Claire Danes","Tobey Maguire"]
    comp_string = "ldi" #chars to highlight
    regex = ".*?" + ".*?".join([f"({x})" for x in comp_string]) + ".*?" #results in .*?(l).*?(d).*?(i).*
    regex_compiled = re.compile(regex, re.IGNORECASE)
    for x in test_string_list:
        re_search_result = re.search(regex_compiled, x) # correctly filters the test list to include only entries that features the search chars in order
        if re_search_result:
            print(f"char combination {comp_string} are in {x} result group: {re_search_result.groups()}")

结果

char combination ldi are in Leonardo DiCaprio result group: ('L', 'D', 'i')

现在我想用<b>[whatever in the result]</b> 替换结果组中的每个匹配项,但我不知道该怎么做。

我目前正在做的是循环遍历结果并使用内置的str.replace 方法来替换出现的情况:

def replace_with_bold(result_groups, original_string):
    output_string: str = original_string
    for result in result_groups:
        output_string = output_string.replace(result,f"<b>{result}</b>",1)
    
    return output_string

这会导致:

Highlighted string: <b>L</b>eonar<b>d</b>o D<b>i</b>Caprio

但我认为,当我已经拥有匹配组时,像这样在结果上循环是浪费的。此外,它甚至不正确,因为它从每个循环的开头检查字符串。所以对于输入 'ooo',结果如下:

char combination ooo are in Leonardo DiCaprio result group: ('o', 'o', 'o')
Highlighted string: Le<b><b><b>o</b></b></b>nardo DiCaprio

什么时候应该是Le&lt;b&gt;o&lt;/b&gt;nard&lt;b&gt;o&lt;/b&gt; DiCapri&lt;b&gt;o&lt;/b&gt;

有没有办法简化这个?也许这里的正则表达式有点矫枉过正?

【问题讨论】:

  • 将您的模式从 .*?(l).*?(d).*?(i).* 更改为 (l)(.*?)(d)(.*?)(i) 并使用捕获组内容构建您的替换。 (注意,只有奇数捕获需要包含在 b 标签之间)
  • 您能详细说明一下吗?你的意思是像f"&lt;b&gt;{group_1}&lt;/b&gt;{group_2}&lt;b&gt;{group_2}&lt;/b&gt;{group_3}"这样的东西吗?这听起来是个好主意。但是不会省略第一个 .*? 产生仅以 'l' 开头的结果吗?
  • 省略第一个.*?应该没有问题,re.subre.search不会像re.match那样锚定到字符串的开头。
  • @CasimiretHippolyte 但是第一场比赛之前的文本会发生什么?如果我理解正确,您是说我应该一个接一个地组合结果组。但是第一次匹配之前的文本不在结果中。还是我错过了什么?
  • 想法是根据组的数量动态构建替换字符串(或您喜欢的格式化字符串)。

标签: python regex


【解决方案1】:

一种使用 re.split 的方式:

test_string_list = ["Leonardo DiCaprio", "Brad Pitt", "Claire Danes", "Tobey Maguire"]

def filter_and_highlight(strings, letters):
    
    pat = re.compile( '(' + (')(.*?)('.join(letters)) + ')', re.I)
    
    results = []
    
    for s in strings:
        parts = pat.split(s, 1)
        
        if len(parts) == 1: continue
        
        res = ''
        for i, p in enumerate(parts):
            if i & 1:
                p = '<b>' + p + '</b>'
                
            res += p
            
        results.append(res)
        
    return results

filter_and_highlight(test_string_list, 'lir')

re.split 的一个特殊之处在于,默认情况下,捕获作为部分包含在结果中。此外,即使第一个捕获在字符串的开头匹配,也会在它之前返回一个空部分,这意味着搜索的字母始终位于子字符串列表中的奇数索引处。

【讨论】:

  • 感谢您的帮助!它确实有效。你能解释一下i &amp; 1吗?它是否因为所有奇数的二进制表示以1 结尾而评估为 True?
  • @Curtwagner1984:确实是空竹,是位运算符 AND。
【解决方案2】:

这应该可行:

for result in result_groups:
    output_string = re.sub(fr'(.*?(?!<b>))({result})((?!</b>).*)',
         r'\1<b>\2</b>\3',
         output_string,
         flags=re.IGNORECASE)

在每次迭代中,结果的第一次出现(? 使 .* 变得懒惰,这一起产生了第一次出现的魔力)如果之前没有被标签包围((?!&lt;b&gt;) 和 @987654326)将被 &lt;b&gt;result&lt;/b&gt; 替换@ 做那部分)和\1 \2 \3 是第一组,第二组和第三组,另外我们将使用IGNORECASE 标志使其不区分大小写。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-05-25
    • 2012-02-12
    • 1970-01-01
    • 2012-02-16
    • 2011-09-01
    • 2011-04-26
    相关资源
    最近更新 更多