跳到主要内容

2 篇博文 含有标签「正则表达式」

查看所有标签

· 阅读需 20 分钟

正则表达式一直是困扰很多程序员的一门技术,当然也包括曾经的我。大多数时候我们在开发过程中要用到某些正则表达式的时候,都会打开谷歌或百度直接搜索然后拷贝粘贴。当下一次再遇到相同问题的时候,同样的场景又再来一遍。作为一门用途很广的技术,我相信深入理解正则表达式并能融会贯通是值得的。所以,希望这篇文章能帮助大家理清思路,搞懂正则表达式各种符号之间的内在联系,形成知识体系,当下次再遇到正则表达式的时候可以不借助搜索引擎,自己解决。

正则表达式到底是什么

正则表达式 (Regular Expression) 其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。它起源于上个 20 世纪 50 年代科学家在数学领域做的一些研究工作,后来才被引入到计算机领域中。从它的命名我们可以知道,它是一种用来描述规则的表达式。而它的底层原理也十分简单,就是使用状态机的思想进行模式匹配。大家可以利用 regexper.com 这个工具很好地可视化自己写的正则表达式:

/\d\w+/这个正则生成的状态机图:

对于具体的算法实现,大家如果感兴趣可以阅读《算法导论》。

从字符出发

我们学习一个系统化的知识,一定要从其基础构成来了解。正则表达式的基本组成元素可以分为:字符和元字符。字符很好理解,就是基础的计算机字符编码,通常正则表达式里面使用的就是数字、英文字母。而元字符,也被称为特殊字符,是一些用来表示特殊语义的字符。如 ^ 表示非,| 表示或等。利用这些元字符,才能构造出强大的表达式模式 (pattern)。接下来,我们就来从这些基本单位出发,来学习一下如何构建正则表达式。

单个字符

最简单的正则表达式可以由简单的数字和字母组成,没有特殊的语义,纯粹就是一一对应的关系。如想在'apple'这个单词里找到‘a'这个字符,就直接用/a/这个正则就可以了。

但是如果想要匹配特殊字符的话,就得请出我们第一个元字符 \, 它是转义字符字符,顾名思义,就是让其后续的字符失去其本来的含义。举个例子:

我想匹配*这个符号,由于*这个符号本身是个特殊字符,所以我要利用转义元字符\来让它失去其本来的含义:

如果本来这个字符不是特殊字符,使用转义符号就会让它拥有特殊的含义。我们常常需要匹配一些特殊字符,比如空格,制表符,回车,换行等, 而这些就需要我们使用转义字符来匹配。为了便于记忆,我整理了下面这个表格,并附上记忆方式:

特殊字符正则表达式记忆方式
换行符\nnew line
换页符\fform feed
回车符\rreturn
空白符\sspace
制表符\ttab
垂直制表符\vvertical tab
回退符[\b]backspace, 之所以使用 [] 符号是避免和 \ b 重复

多个字符

单个字符的映射关系是一对一的,即正则表达式的被用来筛选匹配的字符只有一个。而这显然是不够的,只要引入集合区间和通配符的方式就可以实现一对多的匹配了。

在正则表达式里,集合的定义方式是使用中括号[]。如/[123]/这个正则就能同时匹配 1,2,3 三个字符。那如果我想匹配所有的数字怎么办呢?从 0 写到 9 显然太过低效,所以元字符-就可以用来表示区间范围,利用/[0-9]/就能匹配所有的数字, /[a-z]/则可以匹配所有的英文小写字母。

即便有了集合和区间的定义方式,如果要同时匹配多个字符也还是要一一列举,这是低效的。所以在正则表达式里衍生了一批用来同时匹配多个字符的简便正则表达式:

匹配区间正则表达式记忆方式
除了换行符之外的任何字符.句号, 除了句子结束符
单个数字, [0-9]\ddigit
除了 [0-9]\Dnot digit
包括下划线在内的单个字符,[A-Za-z0-9_]\wword
非单字字符\Wnot word
匹配空白字符, 包括空格、制表符、换页符和换行符\sspace
匹配非空白字符\Snot space

循环与重复

一对一和一对多的字符匹配都讲完了。接下来,就该介绍如何同时匹配多个字符。要实现多个字符的匹配我们只要多次循环,重复使用我们的之前的正则规则就可以了。那么根据循环次数的多与少,我们可以分为 0 次,1 次,多次,特定次。

0 | 1

元字符?代表了匹配一个字符或 0 个字符。设想一下,如果你要匹配colorcolour这两个单词,就需要同时保证u这个字符是否出现都能被匹配到。所以你的正则表达式应该是这样的:/colou?r/

>= 0

元字符*用来表示匹配 0 个字符或无数个字符。通常用来过滤某些可有可无的字符串。

>= 1

元字符+适用于要匹配同个字符出现 1 次或多次的情况。

特定次数

在某些情况下,我们需要匹配特定的重复次数,元字符{}用来给重复匹配设置精确的区间范围。如'a'我想匹配 3 次, 那么我就使用/a{3}/这个正则,或者说'a'我想匹配至少两次就是用/a{2,}/这个正则。

以下是完整的语法:

- {x}: x次

- {min, max}: 介于min次到max次之间

- {min, }: 至少min次

- {0, max}: 至多max次

由于这些元字符比较抽象,且容易混淆,所以我用了联想记忆的方式编了口诀能保证在用到的时候就能回忆起来。

匹配规则元字符联想方式
0 次或 1 次?, 此事
0 次或无数次*宇宙洪荒, 辰宿列张:宇宙伊始,从无到有,最后星宿布满星空
1 次或无数次+一加, +1
特定次数{x}, {min, max}可以想象成一个数轴,从一个点,到一个射线再到线段。min 和 max 分别表示了左闭右闭区间的左界和右界

位置边界

上面我们把字符的匹配都介绍完了,接着我们还需要位置边界的匹配。在长文本字符串查找过程中,我们常常需要限制查询的位置。比如我只想在单词的开头结尾查找。

单词边界

单词是构成句子和文章的基本单位,一个常见的使用场景是把文章或句子中的特定单词找出来。如:

The cat scattered his food all over the room.

我想找到cat这个单词,但是如果只是使用/cat/这个正则,就会同时匹配到catscattered这两处文本。这时候我们就需要使用边界正则表达式\b,其中 b 是 boundary 的首字母。在正则引擎里它其实匹配的是能构成单词的字符 (\w) 和不能构成单词的字符 (\W) 中间的那个位置。

上面的例子改写成/\bcat\b/这样就能匹配到cat这个单词了。

字符串边界

匹配完单词,我们再来看一下一整个字符串的边界怎么匹配。元字符^用来匹配字符串的开头。而元字符$用来匹配字符串的末尾。注意的是在长文本里,如果要排除换行符的干扰,我们要使用多行模式。试着匹配I am scq000这个句子:

I am scq000. I am scq000. I am scq000.

我们可以使用/^I am scq000\.$/m这样的正则表达式,其实 m 是 multiple line 的首字母。正则里面的模式除了 m 外比较常用的还有 i 和 g。前者的意思是忽略大小写,后者的意思是找到所有符合的匹配。

最后,总结一下:

边界和标志正则表达式记忆方式
单词边界\bboundary
非单词边界\Bnot boundary
字符串开头^头尖尖那么大个
字符串结尾$终结者,美国科幻电影,美元符 $
多行模式m 标志multiple of lines
忽略大小写i 标志ignore case, case-insensitive
全局模式g 标志global

子表达式

字符匹配我们介绍的差不多了,更加高级的用法就得用到子表达式了。通过嵌套递归和自身引用可以让正则发挥更强大的功能。

从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式。

分组

其中分组体现在:所有以()元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。

回溯引用

所谓回溯引用(backreference)指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1,\2,...., 其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。

假设现在要在下面这个文本里匹配两个连续相同的单词,你要怎么做呢?

Hello what what is the first thing, and I am am scq000.

利用回溯引用,我们可以很容易地写出\b(\w+)\s\1这样的正则。

回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2... 来引用要被替换的字符串。下面以 js 代码作演示:

var str = 'abc abc 123'; str.replace(/(ab)c/g,'$1g'); // 得到结果 'abg abg 123'

如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。

var str = 'scq000'. str.replace(/(scq00)(?:0)/, '$1,$2') // 返回scq00,$2 // 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2

有时,我们需要限制回溯引用的适用范围。那么通过前向查找和后向查找就可以达到这个目的。

前向查找

前向查找 (lookahead) 是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程中都会用来限制前面的表达式的匹配。例如happy happily这两个单词,我想获得以happ开头的副词,那么就可以使用happ(?=ily)来匹配。如果我想过滤所有以happ开头的副词,那么也可以采用负前向查找的正则happ(?!ily),就会匹配到happy单词的happ前缀。

后向查找

介绍完前向查找,接着我们再来介绍一下它的反向操作:后向查找 (lookbehind)。后向查找(lookbehind) 是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: applepeople都包含ple这个后缀,那么如果我只想找到appleple,该怎么做呢?我们可以通过限制app这个前缀,就能唯一确定ple这个单词了。

其中(?<=regex)的语法就是我们这里要介绍的后向查找。regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向查找。另外一种限制匹配是利用(?<!regex) 语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找appleple也可以这么写成/(?<!peo)ple

需要注意的,不是每种正则实现都支持后向查找。在 javascript 中是不支持的,所以如果有用到后向查找的情况,有一个思路是将字符串进行翻转,然后再使用前向查找,作完处理后再翻转回来。看一个简单的例子:

// 比如我想替换apple的ple为ply var str = 'apple people'; str.split('').reverse().join('').replace(/elp(?=pa)/, 'ylp').split('').reverse().join('');

最后回顾一下这部分内容:

回溯查找正则记忆方式
引用\0,\1,\2 和 $0, $1, $2转义 + 数字
非捕获组(?:)引用表达式 (()), 本身不被消费 (?), 引用 (:)
前向查找(?=)引用子表达式 (()),本身不被消费 (?), 正向的查找 (=)
前向负查找(?!)引用子表达式 (()),本身不被消费 (?), 负向的查找 (!)
后向查找(?<=)引用子表达式 (()),本身不被消费 (?), 后向的 (<,开口往后),正的查找 (=)
后向负查找(?<!)引用子表达式 (()),本身不被消费 (?), 后向的 (<,开口往后),负的查找 (!)

逻辑处理

计算机科学就是一门包含逻辑的科学。让我们回忆一下编程语言当中用到的三种逻辑关系,与或非。

在正则里面,默认的正则规则都是的关系所以这里不讨论。

关系,分为两种情况:一种是字符匹配,另一种是子表达式匹配。在字符匹配的时候,需要使用^这个元字符。在这里要着重记忆一下:只有在[]内部使用的^才表示非的关系。子表达式匹配的非关系就要用到前面介绍的前向负查找子表达式(?!regex)或后向负查找子表达式(?<!regex)

或关系,通常给子表达式进行归类使用。比如,我同时匹配 a,b 两种情况就可以使用(a|b)这样的子表达式。

逻辑关系正则元字符
regex 和!

总结

对于正则来说,符号之抽象往往让很多程序员却步。针对不好记忆的特点,我通过分类和联想的方式努力让其变得有意义。我们先从一对一的单字符,再到多对多的子字符串介绍,然后通过分组、回溯引用和逻辑处理的方式来构建高级的正则表达式。

· 阅读需 7 分钟

语法 #匹配中文字符的正则表达式: [u4e00-u9fa5] 评注:匹配中文还真是个头疼的事,有了这个表达式就好办了

#匹配双字节字符(包括汉字在内):x00-xff 评注:可以用来计算字符串的长度(一个双字节字符长度计 2,ASCII 字符计 1)

#匹配空白行的正则表达式:ns*r 评注:可以用来删除空白行

#匹配 HTML 标记的正则表达式:< (S?)>>.?|< .? /> 评注:网上流传的版本太糟糕,上面这个也仅仅能匹配部分,对于复杂的嵌套标记依旧无能为力

#匹配首尾空白字符的正则表达式:^s|s\$ 评注:可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式

#匹配 Email 地址的正则表达式:w+([-+.]w+)@w+([-.]w+).w+([-.]w+)* 评注:表单验证时很实用

#匹配网址 URL 的正则表达式:[a-zA-z]+://s* 评注:网上流传的版本功能很有限,上面这个基本可以满足需求

#匹配帐号是否合法(字母开头,允许 5-16 字节,允许字母数字下划线):^[a-zA-Z][a-za-z0-9_]{4,15}\$ 评注:表单验证时很实用

#匹配国内电话号码:d{3}-d{8}|d{4}-d{7} 评注:匹配形式如 0511-4405222 或 021-87888822

#匹配腾讯 QQ 号:[1-9][0-9]{4,} 评注:腾讯 QQ 号从 10000 开始

#匹配中国邮政编码:[1-9]d{5}(?!d) 评注:中国邮政编码为 6 位数字

#匹配身份证:d{15}|d{18} 评注:中国的身份证为 15 位或 18 位

#匹配 ip 地址:d+.d+.d+.d+ 评注:提取 ip 地址时有用

#匹配特定数字: ^[1-9]d\$    //匹配正整数 ^-[1-9]d$   //匹配负整数 ^-?[1-9]d$   //匹配整数 ^[1-9]d|0\$  //匹配非负整数(正整数 + 0) ^-[1-9]d|0$   //匹配非正整数(负整数 + 0) ^[1-9]d.d|0.d[1-9]d$   //匹配正浮点数 ^-([1-9]d.d|0.d[1-9]d)$  //匹配负浮点数 ^-?([1-9]d.d|0.d[1-9]d|0?.0+|0)$  //匹配浮点数 ^[1-9]d.d|0.d[1-9]d|0?.0+|0$   //匹配非负浮点数(正浮点数 + 0) ^(-([1-9]d.d|0.d[1-9]d*))|0?.0+|0$  //匹配非正浮点数(负浮点数 + 0) 评注:处理大量数据时有用,具体应用时注意修正

#匹配特定字符串: ^[A-Za-z]+$  //匹配由26个英文字母组成的字符串 ^[A-Z]+$  //匹配由 26 个英文字母的大写组成的字符串 ^[a-z]+$  //匹配由26个英文字母的小写组成的字符串 ^[A-Za-z0-9]+$  //匹配由数字和 26 个英文字母组成的字符串 ^w+\$  //匹配由数字、26 个英文字母或者下划线组成的字符串

#在使用 RegularExpressionValidator 验证控件时的验证功能及其验证表达式介绍如下:

只能输入数字:“^[0-9]*$”
只能输入n位的数字:“^d{n}$”
只能输入至少n位数字:“^d{n,}$”
只能输入m-n位的数字:“^d{m,n}$”
只能输入零和非零开头的数字:“^(0|[1-9][0-9]*)$”
只能输入有两位小数的正实数:“^[0-9]+(.[0-9]{2})?$”
只能输入有1-3位小数的正实数:“^[0-9]+(.[0-9]{1,3})?$”
只能输入非零的正整数:“^+?[1-9][0-9]*$”
只能输入非零的负整数:“^-[1-9][0-9]*$”
只能输入长度为3的字符:“^.{3}$”
只能输入由26个英文字母组成的字符串:“^[A-Za-z]+$”
只能输入由26个大写英文字母组成的字符串:“^[A-Z]+$”
只能输入由26个小写英文字母组成的字符串:“^[a-z]+$”
只能输入由数字和26个英文字母组成的字符串:“^[A-Za-z0-9]+$”
只能输入由数字、26个英文字母或者下划线组成的字符串:“^w+$”
验证用户密码:“^[a-zA-Z]w{5,17}$”正确格式为:以字母开头,长度在6-18之间,


只能包含字符、数字和下划线。
验证是否含有^%&’,;=?$”等字符:“[^%&',;=?$x22]+”
只能输入汉字:“^[u4e00-u9fa5],{0,}$”
验证Email地址:“^w+[-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*$”
验证InternetURL:“^http://([w-]+.)+[w-]+(/[w-./?%&=]*)?$”
验证电话号码:“^((d{3,4})|d{3,4}-)?d{7,8}$”


正确格式为:“XXXX-XXXXXXX”,“XXXX-XXXXXXXX”,“XXX-XXXXXXX”,


“XXX-XXXXXXXX”,“XXXXXXX”,“XXXXXXXX”。
验证身份证号(15位或18位数字):“^d{15}|d{}18$”
验证一年的12个月:“^(0?[1-9]|1[0-2])$”正确格式为:“01”-“09”和“1”“12”
验证一个月的31天:“^((0?[1-9])|((1|2)[0-9])|30|31)$”


正确格式为:“01”“09”和“1”“31”。


匹配中文字符的正则表达式: [u4e00-u9fa5]
匹配双字节字符(包括汉字在内):[^x00-xff]
匹配空行的正则表达式:n[s| ]*r
匹配HTML标记的正则表达式:/<(.*)>.*|< (.*) />/
匹配首尾空格的正则表达式:(^s*)|(s*$)
匹配Email地址的正则表达式:w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*
匹配网址URL的正则表达式:http://([w-]+.)+[w-]+(/[w-./?%&=]*)?

#(1)应用:计算字符串的长度(一个双字节字符长度计 2,ASCII 字符计 1) String.prototype.len=function(){returnthis.replace(x00-xff/g,”aa”).length;}

#(2)应用:JavaScript 中没有像 vbscript 那样的 trim 函数,我们就可以利用这个表达式来实现 String.prototype.trim = function() { return this.replace(/(^s)|(s$)/g, “”); } #(3)应用:利用正则表达式分解和转换IP地址 function IP2V(ip) //IP地址转换成对应数值 { re=/(d+).(d+).(d+).(d+)/g //匹配IP地址的正则表达式 if(re.test(ip)) { returnRegExp.$1Math.pow(255,3))+RegExp.\$2Math.pow(255,2))+RegExp.$3255+RegExp.$41 } else { throw new Error(”Not a valid IP address!”) } } #(4)应用:从 URL 地址中提取文件名的 javascript 程序 s=”http://www.9499.NET/page1.htm”; s=s.replace(/(.*/){0,}(.+).*/ig,”\$2″) ; //Page1.htm
#(5)应用: #利用正则表达式限制网页表单里的文本框输入内容 用正则表达式限制只能输入中文: onkeyup=”value=”/blog/value.replace(/["^u4E00-u9FA5]/g,”)” onbeforepaste=”clipboardData.setData(’text’,clipboardData.getData(’text’).replace(/[^u4E00-u9FA5]/g,”))” #用正则表达式限制只能输入全角字符: onkeyup=”value=”/blog/value.replace(/["^uFF00-uFFFF]/g,”) ”onbeforepaste=”clipboardData.setData(’text’,clipboardData.getData(’text’).replace(/[^uFF00-uFFFF]/g,”))” #用正则表达式限制只能输入数字: onkeyup=”value=”/blog/value.replace(/["^d]/ g,”)“onbeforepaste=“clipboardData.setData (’text’,clipboardData.getData(’text’).replace(/[^d]/ g,”))” #用正则表达式限制只能输入数字和英文: onkeyup=”value=”/blog/value.replace(/[W]/g,””)“onbeforepaste=”clipboardData.setData(’text’,clipboardData.getDa