[倚天屠龙记] vim 查找与替换(正则表达式) {{{
正则表达式是文本处理领域中的一个强大的工具,它可以让文本处理的能力呈指数级的提升,如果一款文本编辑器不支持正则表达式,那么它就算不上是一个现代化的编辑器,这绝非虚言。
正则表达式是模式匹配的高峰,它用一些特殊符号代表一些特殊意义,例如用星号代表它前面的元素可以出现任意次数(包括零次),用[0-9]表示十个数字字符中的任何一个,如此种种。
vim 一旦与正则表达式结合,其搜索替换能力随即发生质的飞跃,可以一次性将文件中的全部日期时间从一种格式替换为另一种格式,可以一次性将《红楼梦》中的回目编号由第一回第二回这样的中文序号替换为第1回第2回这样的阿拉伯序号,而且这只能算是小菜一碟和冰山一角。
vim的正则表达式与通用的正则表达式在语法上是有一些差异的,这点需要读者注意。
vim正则表达式用来表明一个元素可以重复出现任何次数的符号是星号*,例如a*能匹配a,aa,aaa等,它还能匹配空字符串,只是字符a出现了零次,零次当然符合任何次数的限制。如果要匹配字符串hello的任何次数出现需要将它括起来,正确的表示法是\ (hello\ )*,左右括号需要加上转义字符反斜杠,以表示这里不是要匹配左右括号字符,而是正则表达式中要作为一个整体的元素,星号就作用于这个元素上,它就能匹配空串、hello、hellohello等等。
如果要限制不能匹配空串,至少要出现一次,对应的符号是\+,\ (ab\ )\+能匹配ab、abab、ababab等,但不能匹配空串。
如果一个元素可有可无,即出现次数为零或者为一,则用符号\=或者\?,但是后者不能用在以?打头的命令中,这意味着反向搜索时不能用,所以推荐使用\=。\ (ab\ )\=只能匹配空串和ab。英文中可能希望同时匹配一个单词的单数和复数形式,复数形式通常是在单词结尾加上一个字符s,例如要同时匹配fold和folds,正则表达式可以写成 folds\=。
还可以把重复次数指定的更具体,要匹配字符a的三到五次出现可以使用 a\{3,5},它可以匹配 aaa, aaaa, aaaaa,注意右大括号前是没有转义符的。也可以只指定上限或者只指定下限,\ (abc\ )\{3,}表示字符串abc出现至少3次,而\ (abc\ )\{,5}则表示出现最多5次。当然前面介绍的星号加号和等号都可以用这里的方式代替,*等效于\{0,},\+等效于\{1,},而\=等效于\{0,1}。此外,也可以指定固定次数,例如\ (abc\ )\{3}表示字符串abc重复三次。
到现在为止,正则表达式的各个部分(单个字符或者用括号括起来的一个整体)之间是“且”的关系,比如a\+b\+代表若干个a字符后跟若干个b字符,有“且”必有“或”,这就是\|,当然本来是用|表示或的,前面的反斜杠仍然是转义符,以表示后面的|并不是用来匹配一个竖线的。a\|b 既能匹配一个字符a,也能匹配一个字符b,vim的脚本语言中有选择结构if和循环结构while与for,它们都以end结尾,if结尾于endif,while结尾于endwhile,for结尾于endfor,如果要同时匹配这三种结尾符号就可以用end\ (if\|while\|for\ )。
如果你希望在某个位置匹配一个字母,任何字母都行,只要不是26个字符以外的字符,你当然可以写出\ (a\|b\|c\|d\|e\|f\|g\|h\|i\|j\|k\|l\|m...\|x\|y\|z\ ),这绝对会让人抓狂,好在对于ASCII码值连续的字符,正则表达式提供了范围表示法,这只要用[a-z]表示就可以了,自然也可以用[x-z]匹配x,y,z三个字符之中的任何一个,而数字当然可以使用[0-9]。中括号的用法非常灵活,其中可以有多个范围,还可以包含一些离散的值,比如[a-z0-9]能够任何一个字母或数字,而[abc5-8]则匹配abc这三个字符之一或者5-8这四个数字中的任何一个,而[0123456789]则等效于[0-9]。既然中括号用短横线表示范围的两端,那我要是要匹配一个短横线怎么办呢,当然有办法,只要你把短横线放在中括号内的第一个或者最后一个位置,就不会被理解为范围标示符,而是短横线了,例如[-0123]就能匹配短横线和0123这四个数字中的任何一个。如果你想匹配除指定字符之外的字符,则可以使用符号^,它表示取反,例如[^a]能匹配除a以外的任何字符,[^0-9]匹配除非数字的字符,作为一个例子,假如你想匹配由一对双引号引起来的一段文本,你可以使用"[^"]*",亦即在一对双引号内,由除引号以外的任何字符重复多次所组成的字符串,像"hello","this is vim."这样的文本串将能被匹配上,但是若双引号内又有双引号,就不能匹配了。
一个点号.可以匹配任意字符,例如.\{3,5}表示三至五个任意字符,而.*则可以匹配我们这个星球上的一切字符串,尽管这没什么用。
为了方便使用,vim预置了一些经常使用的正则表达式,它们都可以经由基础的正则表达式写出来。简单列出:
预置简写 意义 基础的正则表达式
========================================================
\d 数字 [0-9]
\D 非数字 [^0-9]
\x 十六进制字符 [0-9a-fA-F]
\X 非十六进制字符 [^0-9a-fA-F]
\s 空白字符 [ ] (Tab键和空格键)
\S 非空白字符 [^ ] (非Tab键和空格键)
\l 小写字母 [a-z]
\L 非小写字母 [^a-z]
\u 大写字母 [A-Z]
\U 非大写字母 [^A-Z]
注意这些预置的简写是不能放在中括号内部的,比如像[\d\l]这样的表达式不是一个合法的正则表达式,因为它们本身就是某个中括号正则的简写,而中括号是不能嵌套的。
如果要搜索的文本是跨行的,这只要在搜索串中加上换行符\n就成,例如the\nworld将能匹配上一行以the结尾而下一行以world打头的文本块。
这里是一个使用正则表达式的一个例子。我国古典名著《红楼梦》有七十多万字,我从网上下载到了它的txt文本文件,这么长的文件如果要进行阅读,没有目录结构是无法想象的,好在它的每一回的标题都拥有同样的格式,下面是截取的某几回的回目:
上卷 第一回 甄士隐梦幻识通灵 贾雨村风尘怀闺秀
上卷 第二回 贾夫人仙逝扬州城 冷子兴演说荣国府
上卷 第三回 贾雨村夤缘复旧职 林黛玉抛父进京都
.......
下卷 第一一八回 记微嫌舅兄欺弱女 惊谜语妻妾谏痴人
下卷 第一一九回 中乡魁宝玉却尘缘 沐皇恩贾家延世泽
下卷 第一二零回 甄士隐详说太虚情 贾雨村归结红楼梦
现在通过正则表达式搜索回目标题,每一回标题前面有四个空格,但考虑到文本文件可能因为某些原因出现了混乱的情况,我们忽略这个数目,只指明有任意数目的空白字符,然后接下来是上卷(前八十回,曹雪芹写的那一部分)和下卷(高鹗续写的那一部分),然后是一个空格,后面是中文回目序号和回目标题,因此可以写出正则表达式如下:^\s*[上下]卷 第[零一二三四五六七八九十百]\{1,5}回 .*$,下图即是搜索结果:
借助这个搜索列表,你可以跳到任何一回进行阅读,当然,使用折叠也是一个不错的主意。
}}}