原文:理解正则表达式
在我初学正则表达式的时候,走了一些弯路,强行记忆了很多符号和用法。
等到我有更深入的理解的时候我发现,从翻译和概念的角度上切入,学习起来会顺畅得多。
本文以JavaScript
里的正则表达式为例,讲解其中的关键要素。希望能帮助到初学者。
注:也只限于阐述关键要素,不会事无巨细地展开。
何为正则表达式?
在中文语境里,「正则」两个字有点让人发怵,仿佛高深数学或物理中的「正则化」和「归一化」,抽象而难懂。
其实放到英文里,它是regular expression
,而regular
有「规律、规范、整齐、合格、正规」等意味,「正则」只是其中一种翻译。
不把它翻译成一个词组,而翻译成一句话,大致是:表达规范和规则的句子。
这里的规范和规则,指的是一个字符串的形式规则。
至于JavaScript
里的 RegExp
构造函数,是Regular Expression
的前三个字母缩写。
正则表达式的格式
在JavaScript
里,正则表达式有两个构造方式,一个是通过RegExp
这个构造函数创建实例,另一个是正则表达式字面量写法。
var regexp1 = new RegExp('hello regular expression')
var regexp2 = /hello regular expression/
// test 方法,测试给定的字符串是否符合正则表达式所描述的字符串格式
regexp1.test('hello regular expression') // -> true
regexp1.test('hello word') // -> false
// exec 方法,是 execute 这个单词的缩写,「执行」。返回给定的字符串中符合「正则表达式所描述的字符串格式」的部分
regexp2.exec('hello regular expression') // 返回 'hello regular expression'
regexp2.exec('hello regular expression, more words') // 只返回 'hello regular expression',其它部分不匹配
正则表达式里的元字符
元字符听起来也很抽象,其实换个例子就容易理解:学习如何学习,叫元学习;关于知识的知识,叫元知识。
元字符,则是描述字符的字符,比如,数字,字母,空格,换行等。
元编程,就是能生成代码的代码,在 Javascript
构造符合语法的字符串,放到eval(code)
里运行一下,你就在元编程了。
然后看看元字符、元编程的英文:meta-character
与meta-programming
,对 meta
长个记性,怯魅。
列举几个元字符。元字符大多以反斜杠开头 \
,因为前面展示的「正则表达式字面量」写法里,用的是两个斜杠包裹,所以得用反斜杠或其他标识符。
\d
,匹配单个数字;d 是 digit 这个单词的缩写,它的中文意思就是「数字」\w
,匹配单个单词字符,w 是 word 的缩写,就是字母 a-z,数字 0-9,不包括逗号、句号、加减乘除号、括号等。\s
,匹配单个空白字符,s 是 space 的缩写,就是空白的意思。\n
,匹配换行符,n 是 newline 的缩写,中文就是换行。\r
,匹配回车符,r 就是 return 的缩写,回车在这里就是它的中文意思。\t
,匹配制表符,就是 tab 键打出来的一串用以缩进的空白字符,tab 是 tabel 的缩写,table 就有表格和制表的意思。\b
,匹配单词边界,b 是 boundary 的缩写,中文就是边界的意思。
如你所见,所谓的元字符,就是反斜杠加单词缩写,来表征某个字符类型。这就是它们的设计原则。
正则表达式里的量词
元字符大多只能表示单个字符的类型。
我们还需要量词,以表示「有,有0到多个,有至少一个,有n个以上,有n到m个,以某个字符开头,以某个字符结尾等」。
这时你可以停下来,稍作思考,让你来设计,你会设计成什么样?
Javascript
的设计如下:
n+
,至少1个 n 类型的字符n*
,0到多个 n 类型的字符n?
,0 或 1 个 n 类型的字符n{X}
,X 个 n 类型的字符n{X,Y}
,X 到 Y 个 n 类型的字符n{X,}
,至少 X 个 n 类型的字符n$
,以 n 类型的字符结尾^n
,以 n 类型的字符开头
如你所见,大致是一些类似数学里表达区间的意思。
正则表达式里的表达式
你可以戏谑地说它是「元表达式」。
其实,它们也是描述范围的,只是不是所有范围都是关于某个字符类型n 的数量和出现位置,有些范围跟多个字符组成的集合有关。
比如,在这几个字符类型之内,在这几个字符类型之外的,便利地表示 26 个字母,便利地表示 10 个数字字符。
Javascript
的设计如下:
[abc]
,匹配单个字符,它是abc的集合的元素[^abc]
,匹配单个字符,它不是abc的集合的元素[0-9]
,匹配单个字符,它是从0到9这个集合的元素[a-z]
,匹配单个字符,它是26 字母这个集合的元素(red|blue|green)
,匹配多个连续字符,它是 red blue green 这三个词的集合的元素
小试牛刀
匹配一个电话号码,形式如 020-88813243。
简单版本,(开头)三个数字+一个横杠+八个数字(结尾),就是/^\d{3}-\d{8}$/
。
需求变化,只匹配 020 开头的电话号码,就是/^020-\d{8}$/
。
需求变化,支持分机,分机为 5 个数字,加后缀,就是/^020-\d{8}-\d{5}$/
。
需求变化,电话号码可以是7个,用区间量词,就是/^020-\d{7,8}-\d{5}$/
。
需求变化,有可能没有分机,用区间量词,中括号包裹住分机为一组,后面加个问号,表示0或多个,就是/^020-\d{7,8}(-\d{5})?$/
。
需求变化,区隔符可能是横杠,也可能是星号或空格,用集合表达式,就是/^020[-*\s]\d{7,8}([-*\s]\d{5})?$/
结语
在我们理解了正则表达式的概念和设计思路之后,剩下的,就是查文档和寻找模式的工作了。
实在有难题,网上也可以搜索到现成的坚实的正则表达式可用。这里面的门道还是很多的,在此我们入个门,打个基础即可。
补充:正则表达式简要学习
元字符
元字符是功能性的匹配符号, 如:
\b
单词的开头或结尾,也就是单词的分界处
*
匹配任意数量的字符
.
匹配除了换行之外的所有字符
\d
匹配0到9单个数字
\s
匹配任意的空白符,包括空格,制表符(Tab),换行符,中文全角空格等
\w
匹配 字母 或 数字 或 下划线 或 汉字 等
^
匹配字符串的开始
$
匹配字符串的结束
字符转意
查找元字符本身的话,比如你查找.
,或者*
,就出现了问题:你没办法指定它们,因为它们会被解释成别的意思。这时你就得使用\
来取消这些字符的特殊意义。因此,你应该使用\.
和\*
。当然,要查找\
本身,你也得用\\
字符类
[ ]
集合查找 ,比如 [abcde]
表示匹配里面包含的字符 , 常见的[0-9]
和\d
等价, 即匹配一位数字, [a-z0-9A-Z_]
也完全等同于\w
(如果只考虑英文的话)
\(?0\d{2}[) -]?\d{8}
首先是一个转义字符\(
,它能出现0次或1次?
,然后是一个0,后面跟着2个数字\d{2}
,然后是)
或-
或空格
中的一个,它出现0次或1次?
,最后是8个数字\d{8}
分枝条件
上面那个表达式也能匹配010)12345678或(022-87654321这样的“不正确”的格式。
正则表达式里的分枝条件指的是有几种规则,如果满足其中任意一种规则都应该当成匹配,具体方法是用|
把不同的规则分隔开。
0\d{2}-\d{8}|0\d{3}-\d{7}
表示0开头接两位数字,-
后面连着8位数的电话号码,比如020-12345678 或者 0开头接三位数字,-
后面连着7位数的电话号码,比如0751-1234567
使用分枝条件时,要注意各个条件的顺序。原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。
分组
如果想要重复多个字符又,你可以用小括号来指定子表达式(也叫做分组)
(\d{1,3}\.){3}\d{1,3}
是一个简单的IP地址匹配表达式。要理解这个表达式,请按下列顺序分析它:\d{1,3}
匹配1到3位的数字,(\d{1,3}\.){3}
匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,最后再加上一个一到三位的数字(\d{1,3})
。
正则表达式中并不提供关于数学的任何功能,所以只能使用冗长的分组,选择,字符类来描述一个正确的IP地址:((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
反义
有时需要查找不属于某个能简单定义的字符类的字符。比如想查找除了数字以外,其它任意字符都行的情况,这时需要用到反义:
符号 | 表示 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[ ^x ] | 匹配除了x以外的任意字符 |
[ ^aeiou ] | 匹配除了aeiou这几个字母以外的任意字符 |
例子:
\S+
匹配不包含空白符的字符串。
<a[^>]+>
匹配用尖括号括起来的以a开头的字符串
贪婪与懒惰
当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.*b
,它将会匹配最长的以a
开始,以b
结束的字符串。如果用它来搜索aabab
的话,它会匹配整个字符串aabab
。这被称为贪婪匹配。
有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?
。这样.*?
就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。
a.*?b
匹配最短的,以a
开始,以b
结束的字符串。如果把它应用于aabab
的话,它会匹配aab
(第一到第三个字符)和ab
(第四到第五个字符)
为什么第一个匹配是aab
(第一到第三个字符)而不是ab
(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权
符号 | 表示 |
---|---|
*? | 重复任意次,但尽可能少重复 |
+? | 重复1次或更多次,但尽可能少重复 |
?? | 重复0次或1次,但尽可能少重复 |
{n,m}? | 重复n到m次,但尽可能少重复 |
{n,}? | 重复n次以上,但尽可能少重复 |