很多天前和 zuola 聊天, 偶然提到正则表达式, zuola 说, 会正则表达式的都是牛人. 我说, 其实不难, 买本书看看就会了. 这几天, zuola 又在我博客上留言说会正则表达式才是真的程序员, 因此我想, 还是写篇比较浅显的教程, 让 zuola 同学快速成为牛人吧.
首先说正则表达式是什么. 正则表达式是一种描述性的语言, 用来概括一类字符串 (或者说一个字符串集合). 我们当然可以用自然语言来描述一类字符串, 比如我们说, 以 “010 开头的电话号码”, “夹在HTML 的 和 中间的内容”, “含有 hello 的字符串”, “负数”, “IP地址” “邮箱地址”, 等等. 其实在实际应用中, 我们也常常有这个需求, 比如说提取一篇邮件中所有的 email 地址 (查找), 或者把提取某类电话号码, 升个位, 加个区号什么的 (替换). 人当然可以做这个事情, 但是这个事情重复且单调, 又并不需要太多的智力, 因此, 计算机是最好的工具. 但是问题是, 我们怎么能够告诉计算机, 我们对哪类字符串感兴趣呢? 计算机科学家就帮我们设计了一种让人能够简单的写出来, 表达我们人类想表达的含义, 而计算机又恰好能够很容易的理解和处理的一种表达式, 这就是正则表达式了. 从人和计算机的角度说, 正则表达式是一种人和计算机都能轻松处理的约定, 用来描述一类具有某个性质的字符串.
正则表达式它既有倾向于人的思考方式的一面, 也有倾向于计算机工作原理 (有限自动机) 的一面. 因此, 传统意义上, 如果想真正理解正则表达式, 就要从理解计算机原理入手. 所幸的是, 我们普通用户, 在日常使用中, 并不需要了解计算机的原理, 因为这么多年技术的发展给了正则表达式很多新特性, 让正则表达式越来越脱离计算机的局限, 变得更加适合复杂的任务, 但这样的代价是正则表达式的细节越来越繁杂了, 对于初学者来说更加难学了. 因此我们在这里, 先讲本质, 后谈细节.
最基本的正则表达式, 只有三句话:
一个字符串是一个正则表达式, 比如 aaa, 就是一个正则表达式, 它描述了一个字符串集合, 这个字符串集合里面只有 aaa 这一个元素
两个正则表达式可以直接串起来, 比如 aaabbb 其实, 是由六个正则表达式 a a a b b b 接起来组成的. 我们先笼统的说, 接起来就等于把描述的内容接起来, 等一下再详细解释接起来的含义.
两个字符串, 比如 aaa 和 bbb, 用 | 连起来, 变成了 aaa | bbb, 也构成一个正则表达式, 它描述的字符串集合是原来分别的并集, 比如 aaa | bbb 描述了一个集合, 这个集合里面有 {aaa, bbb} 两个字符串. |
好了, 就这两三话, 就可以解释正则表达式最基本的思维方式了: 用一个表达式, 去描述一类字符串(或者说, 一个集合).
光有这两个, 还不够强大, 因为上面的正则表达式, 我写几个, 就描述了几个字符串, 也就是说, 描述来, 描述去, 都是有限的集合, 不能描述无限的集合. 而我们想要描述的整数啊, 域名啊, 邮箱地址啊, 都是一切就有可能的, 因此, 我们有必要引入一个新的记号, 能够描述无限的集合,
一个正则式 X 可以加上一个 *, 用来描述任意多个原来 X 描述的字符串拼起来的字符串.
这句话比较费解, 我们用例子来说明一下, 比如 a* 这个正则表达式, 我们知道 a 描述了一类字符, 这类字符里面只有一个 a, 所以, a* 描述了一个或者多个 a.
我们再看 a | b* , 按照定义, 这个正则表达式描述了 a 和 b, bb, bbb 等. 如果我们引入一个括号, 写成 (a | b)* , 那么 a | b 就变成一个整体, 描述了 a 或者 b, 这时候, (a | b)* 就是一切只由 a, b 组成的字符串. 这里的括号, 是为了避免歧义, 表示 * 是作用在 a | b 整体上的. 这时候, (a | b) 描述了 a 和 b, 整体加了一个 *, 意味者我们可以任意选 a 或者 b 一个接一个拼起来, 所以, aba, aab 都是在 (a | b)* 的那一类里面的. 注意, * 可以匹配 0 个, 就是说, 这里面包含了什么都没有. 比如说 ab*c 也描述了 ac, 因为中间可以有 0 个 b. 如果您想至少要一个b, 可以写成 abb*c. |
为了帮助您理解接起来, 我们再看一个复杂的例子, o(n | ff). 我们知道, n | ff 描述了 n 或者 ff. 当我们直接把 o 接在前面的时候, 描述的是 on 或者 off. 就是说, 接起来的时候, 要把 o 和后面每种情况都组合一次. 我们再看 (a | o)(n | ff). 前面描述的是 a 或者 o, 后面描述的是 n 或者 ff, 接起来, 描述了 an, aff, on, off. |
我们都知道, 正则表达式描述的是一类字符串, 所以, X 和 Y 在接起来变成 XY 以后, 自然的变成了描述 每一种 X 里面的字符串和 Y里面字符串接起来的情况. 同样, * 好像把 X 和自己接起来多次一样 (可以是任意次), 每次只要接起来的是X里面的字符串, 就一定被 X* 所表述.
(熟悉集合的朋友立即知道 正则表达式是用一个表达式代表了一个集合, X | Y 等价于两个集合的并集, 而 XY 拼起来等价于他们所有的元素 x, y 拼起来的集合). |
好了, 恭喜您, 您已经学会正则表达式了. 真的, 你已经全部学会了正则表达式的知识. 不过不着急, 我们先回顾一下正则表达式的要点:
-
正则表达式由普通的字符, 以及几个特殊的字符, 即 括号 (), 或者 和 星号 * 组成. 用来描述一类字符. -
表示或者. 如果有两个正则表达式 X 和 Y, 那么 X Y 就描述了原来 X 描述的和 Y 描述的. -
正则表达式可以接起来, 变成一个更长的, 描述了一个各个部分被那些被接起来的正则表达式描述的字符串.
- () 是为了避免歧义.
我们上面说的这四个, 就是 100% 如假包换的正则表达式了. 以后的, 都是为了更加方便的使用正则表达式, 而又引入的一些扩展. 恰恰是这些扩展, 让初学者陷入了细节的泥潭. 我们在下一节, 一个一个的来对付诸如 +, [, -, ], ^, $, {m}, 等这些非基本的高级的功能. 需要强调的是, 这些高级的功能, 其实都只是为了人书写方便, 而且是完全可以用我们这里说的最基本的几个规则代替的. 这些高级功能, 我们下节再讲.
练习:
写出匹配以下性质字符串的正则表达式:
-
字符串 2009
-
周曙光同学有两个名字, 分别叫做 zola 和 zuola, 人们常常混淆. 请帮周曙光同学设计一个正则表达式, 可以帮他匹配自己的名字.
-
二进制数字 (最少有一位, 但只含有 0 或者 1的)
-
非零的十进制数字 (有至少一位数字, 但是不能以0开头)
练习软件:
有一些比较好的软件帮你学习正则表达式, 我推荐初学者用 egrep. 可以在 windows 下用, 具体用法是在命令行 打入 egrep “正则表达式” 文件名
egrep 会把文件里面和正则表达式匹配的行 (该行含有一个字符串, 被正则表达式描述了) 打出来. egrep -o “正则表达式” 文件名 的话就会只打出那个完全匹配的字符串, 而不是行. 另外, 在 Linux 下可以用 grep –color “表达式” 文件名, 这样, 匹配上的那个字符串, 会被高亮显示出来.
练习文件:
0108200920088964
zuola -d
zooooola
world hello -012012 2009
0909 zola zhou
0101001
zuola
(把这个文件存成文本文件, 用 windows 的朋友可以放在您的 “我的文档” 里面, 因为 cmd 就是从那里开始运行. 然后您下载一下 egrep 做实验)
答案:
-
2009
-
z( u)ola [或者您可以写成 zuola zola] -
(0 1)(0 1)* -
(1 2 3 4 5 6 7 8 9)(0 1 2 3 4 5 6 7 8 9)*
你会看到第四题的答案很笨拙, 居然写了这么长. 后面的大部分细节, 就是为了诸如此类的写得更加简洁一点.
Update:
- 按照 AW 的留言和他的博客上的读者留言, 这个在线网站可以在线测试正则表达式:
-
如果要论正则表达式方面的参考书的话, 我推荐 < 精通正则表达式>, 中文版余晟同学翻译的, 质量上乘. 这本书可能是正则表达式方面唯一的一本圣经了, 上次我也是直接推荐给 zuola. 本来我是想打算写完了所有的初级教程再推荐的, 所以在本文初稿中没有提到这本参考书.
-
才和 zuola 聊天, 他说要讲点具体的 blogger 用到的例子. 其实我之所以没在这篇文章里面讲, 就是因为这样的例子, 都是和应用程序结合的, 需要 sed, htaccess, awk 或者 linux 管道的具体知识, 我就是想解开这些知识的耦合. 一下子看着天书一样的 sed 替换表达式, 是很难一下子学会的. 他的建议是非常有价值的, 可能在本系列最后, 我会补充一篇 blogger 常用的正则表达式用例.