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