游客发表
写正则表达式最让人抓狂的正则,不是表达语法本身,而是式语每次调试时那种“明明看着没问题,跑起来就是正则不对”的瞬间。上周帮同事排查一个日志解析脚本,表达他用 \d{ 4}-\d{ 2}-\d{ 2} 去匹配日期,式语结果把 2024-13-45 这种非法日期也放过去了。正则问题不在正则引擎,表达在于他把“格式匹配”和“逻辑校验”混为一谈。式语正则表达式只管字符长什么样,正则不管内容有没有意义。表达
字符类是式语最基础的东西。[abc] 匹配 a、正则b、表达c 中任意一个;[^abc] 反过来,式语匹配除了这三个之外的任何字符。范围写法 [a-z] 很常见,但要注意 [a-Z] 这种写法在多数引擎里会报错或者行为诡异,因为 ASCII 码表里大写字母在小写字母前面,范围顺序反了。我习惯用 [A-Za-z] 明确写开,省得踩坑。
量词决定重复次数。* 是零到多次,+ 是一到多次,? 是零或一次,{ n} 精确 n 次,{ n,} 至少 n 次,{ n,m} 在 n 到 m 之间。贪婪模式是默认行为,.* 会一路吃到行尾再回溯。加个问号变成 .*? 就切换成懒惰模式,匹配到第一个满足条件的位置就停。处理 HTML 标签时这个区别特别明显:<.*> 会把整行所有标签一口气吞掉,<.*?> 才是一个一个来。
锚点和边界容易被忽视。^ 匹配行首,$ 匹配行尾,\b 是单词边界,\B 是非单词边界。有个经典案例:用 \bcat\b 去匹配 “concatenate” 里的 “cat” 是匹配不上的,因为前后都是字母,没有边界。但 \bcat\b 能匹配 “a cat sat” 里的 “cat”。这个细节在做关键词提取时很关键。
分组和引用是进阶内容。圆括号 () 把一部分模式包起来,既可以整体加量词,也能捕获内容供后面引用。(ab)+ 匹配 “ababab”。反向引用用 \1、\2 这种写法,比如 (['"]).*?\1 可以匹配成对的引号。非捕获分组 (?:...) 只分组不编号,性能稍好,也不需要反向引用时用它更干净。命名分组 (?<name>...) 在 Python 和 .NET 里支持,JavaScript 从 ES2018 也开始支持,写复杂正则时可读性提升明显。
零宽断言是我觉得最值得花时间掌握的部分。(?=...) 是正向先行断言,(?!...) 是负向先行断言,(?<=...) 是正向后行断言,(?<!...) 是负向后行断言。它们只匹配位置,不消耗字符。比如要匹配后面跟着 “元” 的数字,用 \d+(?=元),结果里不会包含 “元” 字。后行断言在 JavaScript 里曾经长期缺失,直到 ES2018 才补上,之前写密码强度校验时只能绕路。
不同语言的正则实现差异不小。JavaScript 的 \d 只认 ASCII 数字,Python 的 re 模块默认也是,但加上 re.UNICODE 或直接用 regex 库就能匹配全角数字。PCRE 系(PHP、Perl)的递归模式 (?R) 能匹配嵌套括号,JavaScript 至今没有原生支持。写跨语言的正则时,我一般先在 regex101.com 上选好对应的 flavor,省得来回改。
最后说个实用习惯:复杂正则拆成多行,用 x 标志(Python 的 re.VERBOSE,JavaScript 没有原生支持但可以拼字符串)加注释。一个二十行的正则如果挤成一行,三天后自己都看不懂。正则不是越短越好,可维护性比炫技重要得多。
友情链接