玩正则表达式
我第一次认识正则表达式大概是在高中玩 everything 的时候,在那之前只知道使用*
和?
这类的通配符来查找文件,似乎也够用了。但在翻看 everything 的 FAQ 的时候,它说它支持更高级的用于检索文本的语法,叫正则表达式。虽然现在看来 everything 只是支持了部分正则表达式的语法,但当时我就被这些有趣的符号震惊了。
正则表达式,又称正规表示法、常规表示法(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。
许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在Perl中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由Unix中的工具软件(例如sed和grep)普及开的。正则表达式通常缩写成“regex”,单数有regexp、regex,复数有regexps、regexes、regexen。
wikipedia
大一入学后,我立马就跑去图书馆狩猎。在书架上翻到了一本很薄的书 正则表达式必知必会,只有不到150页。花了半天就看完了,受益匪浅。没想到学完立马就用上了。那一次在帮辅导员整理资料,需要统计整个专业的同学的出生年月信息,手头上只有一列身份证信息,于是我用几分钟写了个正则表达式,瞬间完成了转换工作。
身份证大概是一串数字,15位或18位,前面6位是籍贯信息,然后跟着4位出生年份,2位出生月份,2位出生日期,以及剩下的识别码。用正则表达式提取其中的信息,只需要构造简单可行的表达式,然后用括号给我们需要的部分分组即可:
^\d{6}(\d{4})(\d{2})(\d{2}).+$
于是在上面表达式中的 \1 分组表示年份,\2 和 \3 分组分别表示月份和日期。
在上下文环境已知的情况下,要构造正则表达式相对容易得多。因为这种情况下有很多前提条件,例如上面例子的前提就是整个文档里只有合法身份证信息,且每条一行。少了干扰,所以我们不需要再去判断给定的文本是不是身份证,就可以直接提取年月日。
然而在复杂环境下,要对信息进行验证再提取,这要复杂得多。例如在 google 的搜索结果中提取有用的邮件地址。首先你要定义什么样的信息算邮件地址,这可以从互联网行业标准中得到答案。然后构造出相应的正则表达式,接着从搜索结果中大量彩集邮件地址。很多垃圾邮件的制造者就是这么做的。他们甚至不需要自己构造正则表达式,实际上网络上已经有很多公开的规范化的正则表达式可以参考使用。
后来在编译原理课上,我才知道原来正则表达式是以有限状态机的原理工作的。所以可以用化简有限状态机的范式来优化正则表达式。虽然我还不需要用到这么高深的技术,但这真是太酷了。在出来工作的一年里,正则表达式好几次帮了我大忙。让我得以应付数十个上百个文件中的代码变更,文本统计等等。
最近在网上有人发起了一个测验来考验你的正则表达式水平,非常有意思:http://regex.alf.nu 我做了几题就被击败了,不过从前辈的答案中得到了不少灵感。
第7题,匹配素数长度的文本:(?!(..+)+$)
巧妙地用排除法,将长度为2以上的整数的任意倍数个字符串的情况排除。
第10题,匹配三的倍数。有大神依照“各个位置上的数字的和能被3整除的数即是3的倍数”的推理用有限状态机推导出了能够匹配所有三的倍数的正则表达式,^([0369]|[258][0369]*[147]|[147]([0369]|[147][0369]*[258])*[258]|[258][0369]*[258]([0369]|[147][0369]*[258])*[258]|[147]([0369]|[147][0369]*[258])*[147][0369]*[147]|[258][0369]*[258]([0369]|[147][0369]*[258])*[147][0369]*[147])*$
,论文在此。
暂且不谈以上这些杀脑细胞的趣题。“程序员一生中80%的时间在跟字符串打交道。”所以为了给自己节省大量的时间,花个下午去学学正则表达式绝对是值得的。