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

时间:2018-08-17 05:26:37

标签: python regex python-3.x

我试图用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中的工作方式还是很陌生的。

4 个答案:

答案 0 :(得分:2)

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

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

('', '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*

P.S。拆分并重新加入可能是您最好的选择。无论如何,这都是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
    )

答案 1 :(得分:0)

它不会直接回答您的问题,但这不是:

def replace_words(s, rep_dict, sep):
    return sep.join(rep_dict.get(word, word) for word in s.split(sep))

比regex解决方案(无论如何都使用join)更优雅吗?

答案 2 :(得分:0)

如果我对您的理解正确,那么您就不需要regex

rep_odds = {'first': '1st', 'third': '3rd', 'fifth': '5th'}
rep_evens = {'second': '2nd', 'fourth': '4th'}
orders = ['first', 'second', 'third', 'fourth', 'fifth']

print('_'.join([rep_odds.get(x, rep_evens.get(x, 0)) for x in orders]))
# 1st_2nd_3rd_4th_5th

您可以将其推广为与任何分隔符或orders的任何顺序一起使用,例如:

def fun(sep, orders):
    print(sep.join([rep_odds.get(x, rep_evens.get(x, 0)) for x in orders]))

fun('*', orders)
# 1st*2nd*3rd*4th*5th

答案 3 :(得分:0)

这是另一种方式:

z=dict(rep_odds.items() + rep_evens.items())
for i,item in enumerate(orders):
    orders[i]=z.get(item)

输出:

print(orders)
['1st', '2nd', '3rd', '4th', '5th']