我正在尝试用 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
)
