Skip to main content
 首页 » 编程设计

python之正则表达式替换 Python 中用下划线分隔的单词

2024年09月07日28wuhuacong

我正在尝试用 Python 替换特定单词(由下划线的特定单词边界分隔)。我知道我可以做一个字符串拆分并循环遍历列表项以替换关键短语,然后再将其重新加入字符串.. 但它感觉不是最优雅或最好的方法。

所以,假设我为了我想做的事情而创建了一个基于正则表达式的小函数,如下所示:

def replace_words(string, rep_dict, separator): 
    regex = r'({0}|\b)({1})({2}|\b)'.format(re.escape(separator), '|'.join(rep_dict.keys()), re.escape(separator)) 
    return re.sub(regex, lambda x: '{0}{1}{2}'.format(x.group(1), rep_dict[x.group(2)], x.group(3)), string) 

假设我使用任何其他分隔符,例如星号 (*),那么它会按预期工作:

 rep_odds = {'first': '1st', 'third': '3rd', 'fifth': '5th'} 
 rep_evens = {'second': '2nd', 'fourth': '4th'} 
 orders = ['first', 'second', 'third', 'fourth', 'fifth'] 
 before = '*'.join(orders) 
 after = replace_words(before, rep_odds, separator='*') 
 # returns: 1st*second*3rd*fourth*5th 
 after = replace_words(before, {**rep_odds, **rep_evens}, separator='*') 
 # returns: 1st*2nd*3rd*4th*5th 

但是如果我将其更改为使用分隔符作为下划线 (_),我会得到这种意外的(错误的)行为:

before = '_'.join(orders) 
after = replace_words(before, rep_odds, separator='_') 
# returns: 1st_second_3rd_fourth_5th <-- Good 
after = replace_words(before, {**rep_odds, **rep_evens}, separator='_') 
# returns: 1st_second_3rd_fourth_5th <-- What went wrong ?! 

如果有人能帮助我理解这种行为,我对学习正则表达式及其在 Python 中的工作方式还是个新手……谢谢。

请您参考如下方法:

整洁的问题。这是问题所在:

re.sub 不允许一个字符位于多个匹配组中;一旦一个字符属于一个匹配项,它就会被消耗掉,除非您指定该匹配项是非消耗性的。使用星号进行匹配时,一个关键事实是单词边界位于星号和单词字符之间。以下是在 lambda 中使用星号({0}、{1} 和 {2} 时的匹配组):

('', 'first', '*') 
('', 'second', '*') 
('', 'third', '*') 
('', 'fourth', '*') 
('', 'fifth', '') 

当正则表达式匹配器到达第一个匹配项的末尾时,它的光标位于第一个星号和单词 second 之间,这是一个单词边界。因此 second* 也是一个匹配项,然后是 third*,等等。

但是,当您使用下划线时,这里是相应的匹配项:

('', 'first', '_') 
('_', 'third', '_') 
('_', 'fifth', '') 

当正则表达式匹配器到达第一个匹配项的末尾时,其光标位于第一个下划线和单词 second 之间,不是单词边界。由于它已经超过了第一个下划线并且不在单词边界处,因此无法匹配 (_|\b)second。因此,在 second 之后的下一个下划线之前没有匹配项,您可以看到该匹配项包括与 third 相邻的两个下划线。

简而言之,第一个示例是“幸运的”,因为在传递分隔符后,您会进入单词边界,而第二个示例并非如此。

要解决此问题,您可以使用前瞻断言,它不会消耗匹配的字符。

def replace_words(string, rep_dict, separator): 
    regex = r'({0}|\b)({1})((?={2}|\b).*?)'.format( 
        re.escape(separator), '|'.join(rep_dict.keys()), re.escape(separator) 
    ) 
 
    return re.sub( 
        regex, lambda x: '{0}{1}{2}'. 
        format(x.group(1), rep_dict[x.group(2)], x.group(3)), string 
    ) 

现在匹配如下:

('', 'first', '') 
('*', 'second', '') 
('*', 'third', '') 
('*', 'fourth', '') 
('*', 'fifth', '') 

忽略下面的删除线,它会匹配单词前缀,例如*firstperson* 会变成 *1stperson*。

附言 split 和重新加入可能是你最好的选择。无论如何,这很可能是 re.sub 在幕后所做的事情,因为字符串是不可变的。

要解决此问题,您可以仅匹配关键字 或 字符串开头的分隔符(或者,匹配关键字 或 字符串结尾的分隔符).

def replace_words(string, rep_dict, separator): 
    regex = r'(^|{0})({1})'.format( 
        re.escape(separator), '|'.join(rep_dict.keys()) 
    ) 
 
    return re.sub( 
        regex, lambda x: print(x.groups()) or '{0}{1}'. 
        format(x.group(1), rep_dict[x.group(2)]), string 
    )