MelonBlog

正则表达式中的零宽断言(Zero-Length Assertions)

前言

今天碰到一个问题,需要将 camel case 的字符串转成 snake case,因为功能比较简单就懒得用一个三方库了。

写的过程中发现实现这种功能真的有很多方法,其中使用零宽断言是最优雅的方式。

def snake_case(s):
    return re.sub(r'(?<!^)(?=[A-Z])', '_', s).lower()

零宽断言(Zero-Length Assertions)

上面的代码,通过一个 re.sub 函数就可以实现 snake_case 真的非常优雅,现在我们来看看零宽断言到底是什么。

正则表达式可以支持向前查找(Lookahead)和向后查找(Lookbehind),向前查找和向后查找是不消耗字符的,也就是说零宽断言只是匹配一个”位置”。

这也是为什么称为零宽。

上面的 snake_case 函数里,re.sub 是将匹配到的字符串替换成指定的字符串,但是你会发现这里写的 ’_’ 并没有替换任何字符串,因为它匹配的是一个位置。

不同的查找方式

(?=):正向(向后)查找指定字符串的位置

(?!):正向(向后)查找非指定字符串的位置

# 查找大写字母的位置,并且再该位置塞入一个下划线
print(re.sub(r'(?=[A-Z])', '_', 'abCdeC'))
# 返回:ab_Cde_C
# 查找字符串 b,只查 后面跟着一个大写字母的 b,并将 b 替换成下划线
print(re.sub(r'b(?=[A-Z])', '_', 'abCdbeC'))
# 返回:a_CdbeC
# 查找所有不是大写字母的位置
print(re.sub(r'(?![A-Z])', '_', 'abCdeC'))
# 返回:_a_bC_d_eC_
# 查找字符串 b,只查后面不跟着大写字母的 b,并将 查到的 b 替换成下划线
print(re.sub(r'b(?=[A-Z])', '_', 'abCdbeC'))
# 返回:abCd_eC

(?≤):反向(向前)查找指定字符串的位置

(?!≤):反向(向前)查找非指定字符串的位置

# 查找以大写字母开头的 d,并将 d 替换成下划线
print(re.sub(r'(?<=[A-Z])d', '_', 'abCdbeC'))
# 返回:abC_beC
# 查找不以大写字母开头的 d,并将 d 替换成下划线
print(re.sub(r'(?<![A-Z])d', '_', 'abCdbedC'))
# 返回:abCdbe_C

这里要注意的是,向前和向后不是以你的代码位置为准,例如(?=[A-Z])b 你可能会觉得这里是匹配大写字母开头的 b,实际上这一行匹配不到任何内容,因为?= 只能向后匹配