一个个音符杂乱无章的组合在一起,弹奏出的或许就是噪音,同样的音符经过作曲家的手,就可以谱出非常动听的乐曲,一个演奏者同样可以照着乐谱奏出动听的乐曲,但他/她或许不知道该如何去改变音符的组合,使得乐曲更动听。
作为正则的使用者也一样,不懂正则引擎原理的情况下,同样可以写出满足需求的正则,但是不知道原理,却很难写出高效且没有隐患的正则。所以对于经常使用正则,或是有兴趣深入学习正则的人,还是有必要了解一下正则引擎的匹配原理的。
定义
正则表达式(regular expression)是一个描述字符模式的对象,使用正则表达式可以进行强大的模式匹配和文本检索与替换功能。
JavaScript 的正则表达式语法是 Perl5 的正则表达式语法的大型子集,所以对于有 Perl 编程经验的程序员来说,学习 JavaScript 中的正则表达式是小菜一碟。
正则表达式是描述字符模式的对象,用于对字符串模式匹配及检索替换,是对字符串执行模式匹配的强大工具。
JavaScript中的正则表达式用RegExp对象表示,可以使用RegExp()构造函数来创建RegExp对象,不过RegExp对象更多是通过字面量的语法来创建。
创建
构造函数
1 | <!-- // 不推荐写法 --> |
注意:当使用构造函数创造正则对象时,需要常规的字符转义规则(在前面加反斜杠 \)。
字面量
1 | <!-- // 推荐写法 --> |
1 | var reg = new RegExp("\\w+") |
版本区别
在ECMAScript5中这种情况有所改变,相同正则表达式字面量的每次计算都会创建新的实例对象
,目前很多现代浏览器也对此做了纠正。
1 | function getRE(){ |
表达式
模式修饰词
表达式 | 描述 |
---|---|
i | 执行对大小写不敏感的匹配 |
g | 执行全局匹配模式(查找所有匹配而非在找到第一个匹配后停止) |
m | 执行多行匹配模式 |
1 | var str='HwwwwLwello orllld lLll!' |
元字符
与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。正则表达式中的元字符包括:
1 | ( [ { \ ^ $ | ) ? * + . ] } |
这些元字符在正则表达式中都有一或多种特殊用途,因此如果想要匹配字符串中包含的这些字符,就必须对它们进行转义。
下面给出几个例子。
1 | var pattern1 = /[bc]at/i |
直接量字符
字符 | 描述 |
---|---|
字母数字 | 自身 |
\0 | 查找 NUL 字符(\u0000) |
\t | 查找制表符(\u0009) |
\v | 查找垂直制表符(\u000A) |
\n | 查找换行符(\u000B) |
\f | 查找换页符(\u000C) |
\r | 查找回车符(\u000D) |
\xdd | 查找以十六进制数 dd 规定的字符(\x0A => \n) |
\uxxxx | 查找以十六进制数 xxxx 规定的 Unicode 字符(\u0009 => \t) |
\cX | 控制字符^X (\cJ => \n) |
1 | var str='null \t \n \f \r ' |
字符类
字符 | 描述 |
---|---|
. | 查找单个字符,除了换行和行结束符 |
\w | (word)查找单词字符:[a-zA-Z_0-9](单词字符包括:a-z、A-Z、0-9,以及下划线) |
\W | 查找非单词字符:[^a-zA-Z_0-9](单词字符包括:a-z、A-Z、0-9,以及下划线) |
\s | (white space)查找空白字符 |
\S | 查找非空白字符 |
\d | (digit)查找数字:[0-9] |
\D | 查找非数字字符:[^0-9] |
[0-9] | 查找任何从 0 至 9 的数字 |
[a-z] | 查找任何从小写 a 到小写 z 的字符 |
[A-Z] | 查找任何从大写 A 到大写 Z 的字符 |
[A-z] | 查找任何从大写 A 到小写 z 的字符 |
[…] | 查找方括号之间的任何字符(没有顺序同级) |
[^…] | 查找不在方括号之间的任何字符 |
[adgk] | 查找给定集合内的任何字符 |
[^adgk] | 查找给定集合外的任何字符 |
如果要匹配任意字符怎么办?可以使用 [\d\D]、[\w\W]、[\s\S] 和 [^] 中任何的一个。
1 | var str='3 o !_..' |
重复字符
字符 | 描述 |
---|---|
X{n,m} | 匹配包含 n 至 m 个 X 的序列的字符串。 |
X{n,} | 匹配包含至少 n 个 X 的序列的字符串。 |
X{n} | 匹配包含 n 个 X 的序列的字符串。 |
X? | (有吗?)匹配任何包含零个或一个 X 的字符串 {0,1} |
X+ | (加号是追加的意思)匹配任何包含至少一个 X 的字符串 {1,} |
X* | (任意次)匹配任何包含零个或多个 X 的字符串 {0,} |
1 | var str='Hwwwwlllll orlllld lll!' |
非贪婪重复
尽可能少的匹配:??、+?、*?、{1,4}?
1 | var str='0111' |
锚字符
字符 | 描述 |
---|---|
^ | 匹配字符串开头(用正则表达式处理多行时匹配行的开始) |
$ | 匹配字符串结尾(处理多行时匹配行尾) |
\b | 匹配单词边界 |
\B | 匹配非单词边界 |
(?=p) | 零宽正向先行断言,要求接下来的字符都与p匹配,但不能包括匹配p的那些字符 (?=p) => p |
(?!p) | 零宽正向先行断言,要求接下来的字符不与p匹配 (?!p) => [^p] |
1 | var str='orllld' |
1 | var str='JavaScriptS' |
选择、分组、引用字符
字符 | 描述 |
---|---|
(…) | 组合,将几个项组合为一个单元,这个单元可通过*、+、?、等符号加以修饰,而且可以记住和这个组合相匹配的字符串以供使用的字符。 |
(?:…) | 只组合,把项组合到一个单元,但不记住与该组相匹配的字符 |
\n | 反向引用。比如 \2,表示引用的是第二个括号里的捕获的数据。 |
red|blue|green 查找任何指定red、blue、green的选项。
用()表示的就是要提取的分组(Group)^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码
1 | <!-- 识别合法的时间 --> |
1 | var str='JavaScriptS' |
非捕获性分组
1 | reg = /abc{2}/ |
方法
实例方法
exec
检索字符串中指定的值。返回找到的值,并确定其位置。
1 | var date = 'Ubuntu 8' |
1 | var text = "mom and dad baby" |
这个例子中的模式包含两个捕获组。最内部的捕获组匹配 "and baby"
,而包含它的捕获组匹配 "and dad"
或者 "and dad and baby"
。当把字符串传入 exec()
方法中之后,发现了一个匹配项。因为整个字符串本身与模式匹配,所以返回的数组 matchs
的 index
属性值为 0
。数组中的第一项是匹配的整个字符串,第二项包含与第一个捕获组匹配的内容,第三项包含与第二个捕获组匹配的内容。
对于 exec()
方法而言,即使在模式中设置了全局标志 g
,它每次也只会返回一个匹配项。在不设置全局标志的情况下,在同一个字符串上多次调用 exec()
将始终返回第一个匹配项的信息。
而在设置全局标志的情况下,每次调用 exec()
则都会在字符串中继续查找新匹配项,如下面的例子所示。
1 | var text = "cat, bat, sat, fat" |
这个例子中的第一个模式 pattern1
不是全局模式,因此每次调用 exec()
返回的都是第一个匹配项 "cat"
。而第二个模式 pattern2
是全局模式,因此每次调用 exec()
都会返回字符串中的下一个匹配项,直至搜索到字符串末尾为止。此外,还应该注意模式的 lastIndex
属性的变化情况。在全局匹配模式下,lastIndex
的值在每次调用 exec()
后都会增加,而在非全局模式下则始终保持不变。
IE 的 JavaScript 实现在
lastIndex
属性上存在偏差,即使在非全局模式下,lastIndex
属性每次也会变化。
正则表达式的第二个方法是 test()
,它接受一个字符串参数。在模式与该参数匹配的情况下返回 true
;否则,返回 false
。在只想知道目标字符串与某个模式是否匹配,但不需要知道其文本内容的情况下,使用这个方法非常方便。因此,test()
方法经常被用在 if
语句中,如下面的例子所示。
1 | var text = "000-00-0000" |
在这个例子中,我们使用正则表达式来测试了一个数字序列。如果输入的文本与模式匹配,则显示一条消息。这种用法经常出现在验证用户输入的情况下,因为我们只想知道输入是不是有效,至于它为什么无效就无关紧要了。
RegExp
实例继承的 toLocaleString()
和 toString()
方法都会返回正则表达式的字面量,与创建正则表达式的方式无关。例如:
1 | var pattern = new RegExp("\\[bc\\]at", "gi") |
即使上例中的模式是通过调用 RegExp
构造函数创建的,但 toLocaleString()
和 toString()
方法仍然会像它是以字面量形式创建的一样显示其字符串表示。
test
检索字符串中指定的值。返回 true 或 false。可以修改lastIndex从指定位置开始匹配。
1 | var date = 'Ubuntu 8' |
字符串方法
search
可在字符串内检索指定的值,或找到一个正则表达式的匹配,得到第一个位置,没有则返回-1
1 | var str='Hello world!' |
split
方法用于把一个字符串分割成字符串数组。
1 | stringObject.split(separator,howmany) |
separator: 字符串或正则表达式
separator: 该参数可指定返回的数组的最大长度
参数是字符串转换数组后间隔的参照物,但是有一些复杂的转换就比较麻烦了,这时候我们可以使用正则表达式对字符串进行筛选后再组成
注释:
- 如果把空字符串 (“”) 用作 separator,那么 stringObject 中的每个字符之间都会被分割。
- String.split() 执行的操作与 Array.join 执行的操作是相反的。
1 | str ="some some \tsome\t\f" |
match
方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
该方法类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置。
1 | stringObject.match(searchvalue) |
1 | 检索字母l: |
replace
可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配,并将其替换。值对应match所解读的位置
字符 | 描述 |
---|---|
$n | 匹配第n个匹配正则表达式中的圆括号子表达式文本 |
$& | 匹配正则表达式的子串 |
$` | 匹配子串左边的文本 |
$’ | 匹配子串右边的文本 |
$$ | 匹配美元符号 |
1 | var date = ' Ubuntu 8 ' |
1 | str ='some some \tsome\t\f' |
属性
每个实例都具有下列属性,通过这些属性可以取得有关模式的各种信息。
实例属性
global
:布尔值,表示是否设置了g
标志。ignoreCase
:布尔值,表示是否设置了i
标志。lastIndex
:整数,表示开始搜索下一个匹配项的字符位置,从0算起。multiline
:布尔值,表示是否设置了m
标志。source
:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回。
1 | var pattern1 = /\[bc\]at/i |
source
正则表达式文本
1 | var reg = /[a-z]/i |
global
只读布尔值,是否有修饰符g
1 | var reg = /[a-z]/i |
ignoreCase
只读布尔值,是否有修饰符i
1 | var reg = /[a-z]/i |
multiline
只读布尔值,是否有修饰符m
1 | var reg = /[a-z]/i |
lastIndex
下一次检索开始的位置,用于exec() 和 test()
1 | var text = "cat, bat, sat, fat" |
构造函数属性
RegExp
构造函数包含一些属性(这些属性在其他语言中被看成是静态属性)。这些属性适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而变化。关于这些属性的另一个独特之处,就是可以通过两种方式访问它们。换句话说,这些属性分别有一个长属性名和一个短属性名(Opera是例外,它不支持短属性名)。下表列出了RegExp构造函数的属性。
长属性名 | 短属性名 | 说明 |
---|---|---|
input | $_ | 最近一次要匹配的字符串。Opera未实现此属性。 |
lastMatch | $& | 最近一次的匹配项。Opera未实现此属性。 |
lastParen | $+ | 最近一次匹配的捕获组。Opera未实现此属性。 |
leftContext | $` | input字符串中lastMatch之前的文本。 |
multiline | $* | 布尔值,表示是否所有表达式都使用多行模式。IE和Opera未实现此属性。 |
rightContext | $’ | Input字符串中lastMatch之后的文本。 |
使用这些属性可以从 exec()
或 test()
执行的操作中提取出更具体的信息。请看下面的例子。
1 | var text = "this has been a short summer" |
如前所述,例子使用的长属性名都可以用相应的短属性名来代替。只不过,由于这些短属性名大都不是有效的 JavaScript 标识符,因此必须通过方括号语法来访问它们,如下所示。
1 | var text = "this has been a short summer" |
除了上面介绍的几个属性之外,还有多达9个用于存储捕获组的构造函数属性。访问这些属性的语法是 RegExp.$1
、RegExp.$2
…RegExp.$9
,分别用于存储第一、第二…第九个匹配的捕获组。在调用 exec()
或 test()
方法时,这些属性会被自动填充。然后,我们就可以像下面这样来使用它们。
1 | var text = "this has been a short summer" |
这里创建了一个包含两个捕获组的模式,并用该模式测试了一个字符串。即使 test()
方法只返回一个布尔值,但 RegExp
构造函数的属性 $1
和 $2
也会被匹配相应捕获组的字符串自动填充。
模式的局限性
尽管 JavaScript 中的正则表达式功能还是比较完备的,但仍然缺少某些语言(特别是 Perl)所支持的高级正则表达式特性。下面列出了 JavaScript 正则表达式所不支持的特性。
- 匹配字符串开始和结尾的\A和\Z锚
- 向后查找(lookbehind)
- 并集和交集类
- 原子组(atomic grouping)
- Unicode支持(单个字符除外,如\uFFFF)
- 命名的捕获组
- s(single,单行)和x(free-spacing,无间隔)匹配模式
- 条件匹配
- 正则表达式注释
即使存在这些限制,JavaScript 正则表达式仍然是非常强大的,能够帮我们完成绝大多数模式匹配任务。
运行原理
NFA引擎匹配原理
环视(Lookaround)
环视只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的。环视匹配的最终结果就是一个位置。
表达式 | 描述 |
---|---|
(?<=Expression) | 逆序肯定环视,表示所在位置左侧能够匹配Expression |
(?<!Expression) | 逆序否定环视,表示所在位置左侧不能匹配Expression |
1 | var str = 'aa<p>one</ps>bb<div>two</div>cc' |
回溯法原理
正则表达式匹配字符串的这种方式,有个学名,叫回溯法也称试探法
1 | 没有回溯 |
1 | 分支结构 |
万能的‘正则’
比如匹配这样的字符串:1010010001…。 虽然很有规律,但是只靠正则就是无能为力。
要认识到正则的局限,不要去研究根本无法完成的任务。同时,也不能走入另一个极端:无所不用正则。能用字符串 API 解决的简单问题,就不该正则出马。
日期选取
1 | var string = "2017-07-01"; |
字符串判断
1 | var string = "?id=xx&act=search"; |
获取子串
1 | var string = "JavaScript"; |
提取数据
提取出年、月、日,可以这么做:
1 | var regex = /(\d{4})-(\d{2})-(\d{2})/; |
替换
想把 yyyy-mm-dd 格式,替换成 mm/dd/yyyy 怎么做
1 | var regex = /(\d{4})-(\d{2})-(\d{2})/; |
案例分析
正则表达式
1 | <!-- // 挑战一:数字 --> |
1 | <!-- // 挑战二:3位的数字 --> |
1 | <!-- // 挑战三:至少3位的数字 --> |
1 | <!-- // 挑战四:3-5位的数字 --> |
1 | <!-- // 挑战五:由26个英文字母组成的字符串 --> |
1 | <!-- // 挑战六:由数字和26个英文字母组成的字符串 --> |
1 | <!-- // 挑战七:日期格式:年-月-日 --> |
1 | <!-- // 挑战八:时间格式:小时:分钟, 24小时制 --> |
1 | <!-- // 挑战九:中国大陆身份证号,15位或18位 --> |
判断
匹配整数
注:就是像-3,-2,-1,0,1,2,3,10等这样的数。
-: 0-1
[1-9]: 0-
[0-9]: 1
1 | var reg = /^-?[1-9]*\d$/ |
匹配负浮点数
注:必须负数,第一位1-9,点后面位随机数字,第一位为0,点后面要有个不为零的数字。
1 | var reg = /^-([1-9]\d*\.\d*|0\.\d*[1-9]\d*)$/ |
匹配浮点数
注:为了表示更大范围的数据,数学上通常采用科学计数法,把数据表示成一个小数乘以一个以10为底的指数。
1 | var reg = /^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$/ |
1 | var reg = /\<(.*?)\>/ |
匹配非负浮点数
注:正浮点数 + 0
(0.0是浮点数吗?浮点数是什么)
1 | var reg = /^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$/ |
匹配非正浮点数
注:负浮点数 + 0
1 | var reg = /^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$/ |
匹配HTML元素
1 | var reg = /\<(.*?)\>/g |
验证密码问题
密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符。
1 | var reg = /^[0-9A-Za-z]{6,12}$/ |
判断PDF后缀
1 | var reg = /^.+\.pdf$/i |
匹配中文字符
1 | var reg = /^[\u4e00-\u9fa5]+$/ |
两位小数
1 | <!-- var reg = /^((?:-?0)|(?:-?[1-9]\d*))(?:\.\d{1,2})?$/ --> |
至少3位的数字
1 | var reg = /^\d{3,}$/ |
中国邮政编码
注:中国邮政编码为6位数字,前两位数字表示省(直辖市,自治区);前三位数字表示邮区;前四位数字表示县(市);最后两位数字表示投递局(所)。
1 | var reg = /^[1-9]\d{5}$/ |
验证帐号是否合法
注:字母、数字、下划线组成,字母开头,4-16位。
1 | var reg = /^[a-zA-z]\w{3,15}$/ |
匹配
常用正则表达式
更复杂的用法,使用子匹配
1 | <!-- // exec返回的数组第1到n元素中包含的是匹配中出现的任意一个子匹配 --> |
匹配空行
注:匹配空白字符
1 | <!-- var 空格 = /[ ]+/g --> |
匹配首尾空格
注:匹配首空格和尾空格,空格有一个以上,肯能同时存在
1 | var reg = /(^\s+)|(\s+$)/g |
m~n位的数字
1 | var date = '8888' |
匹配非负整数
注:正确格式为:0 1 9 100
1 | var date = '011' |
验证一年的12个月
注:正确格式为:”01”~”09”和”1”~”12”
1 | var date = '01' |
IPV4 地址
注:提取ip地址时有用
1 | var date = '192.168.0.1' |
注:
1 | Email : /^\w+([-+.]\w+)*@\w+([-.]\\w+)*\.\w+([-.]\w+)*$/ |
验证身份证号
第二代身份证号码编排规则
注:15位或18位数字(第二代身份证最后一位可能为X)
1 | var reg = /^(\d{15}|\d{17}[\dxX])$/ |
Phone手机号码
注:只有13、15和18开头的11位手机号码
1 | var date = '18961856168' |
网址
注:https、http
1 | var date = 'https://zhidao.baidu.com/' |
顶级域名
1 | var date = 'http://zhidao.baidu.com/jdslfjdsf' |
驼峰化
1 | function camelize (str) { |
中划线化
1 | function dasherize (str) { |
匹配成对标签
1 | var regex = /<([^>]+)>[\d\D]*<\/\1>/; |
千位分隔符(js 实现)
方法一
匹配内容进行替换
1 | function thousandBitSeparator(num) { |
方法二
通过匹配位置来判断
1 | <!-- 弄出最后一个逗号 --> |
支持2-10位的汉字或数字的正则表达式(还包含汉字和数字混合哦)
数字 0-9
汉子 \u4e00-\u9fa5:这两个unicode值正好是Unicode表中的汉字的头和尾。
1 | var regex = /^([0-9\u4e00-\u9fa5]{2,10})$/; |