前言
最近工作中涉及到一些使用正则表达式的地方,虽然以前也遇到过这种情况,但是我的处理方式一般都是百度一下...看一下网上有成型的解决方式就拿来直接用了。从来没有认真的系统的了解正则表达式的语法结构和相关知识,这次在百度查询的时候偶然间看到 deerchao 大神写的关于正则表达式的一系列内容,让自己对这部分内容有了一个相对比较全面的了解,希望用一篇笔记整理一下自己的理解,并供以后查阅,当然如果能够帮助到看到这篇文章的你,我会感到不胜荣幸。鉴于个人水平有限,文中若有错误,请您谅解并不吝指正。
正则表达式是什么玩意?
其实当我们初步开始接触一门语言或者开发技术的时候,我们都无可避免的了解一些关于字符串处理的相关方法或者函数,比如 OC 中的 NSString 相关的方法:
NSString* str = [NSString stringWithFormat:@"%@", userId];
以及 Java 中的 string 相关的方法:
str.trim();
一般情况下,当然我是说我 =。= ,这个时候我们就会认为关于字符串处理的东西我们了解的够多了,系统提供的 API 已经足够应付我们遇到的所有问题了,然而在实际的开发工作中我们会经常遇到的场景是去查找符合某些复杂规则的字符串的需要,或者从一个字符串中提取出符合某些 规则 的子串,这个时候仅仅依靠系统的 API 是很难完成这个任务(最起码付出的时间成本很高),需要配合使用的 规则 便是我们今天要聊的 正则表达式 。
在编写处理字符串的程序或网页时,经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码。
正则表达式的语法
其实最简单的正则匹配我们都会,笨蛋其实就是‘相等’嘛,比如说:我们想要在一个长长的字符串中将 hello 提取出来,第一种方法不需要使用正则,我们可以遍历字符串,截取这个 range 下的子串,并一一与"hello"进行对比,相同的就取出来;还有另外的方法就是使用正则表达式进行匹配截取,当然这个时候的正则表达式就是最简单的正则,使用hello
就可以了。
当然,我们在开发中遇到的字符串匹配的场景并不会永远这样简单,比如说:还是刚刚的那个场景,只不过我需要提取的是 hello 这个单词,那么刚刚的使用正则的匹配就会将 ahellob 当中的 hello 也会匹配出来,而这并不是我所希望,这个时候我们就需要学习以下的一些内容。
元字符
元字符是帮助我们在书写正则表达式的时候,代表一个位置或者一个(或多个)字符而存在的,比如回到上面的例子,我需要精确的获得 hello 这个单词,并且保证我获得的 hello 不是在其他的单词当中,不难分析的是,我需要一个玩意来让我告诉正则匹配,h 的前面是单词的开始并且 o 的后面是单词的结束,这个时候我们需要用到\\b
,相应的正则表达式变为\\bhello\\b
,\\b
是代表 匹配单词的开始或结束 的元字符。
我们常见的元字符主要包括:
代码 | 元字符说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
重复限定符
有的时候需要表示有多个同一类型的字符出现的时候就需要使用限定符来进行限定,比如需要匹配两个数字,就需要使用\\d{2}
,以下是常见的限定符号:
代码 | 限定符说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复 n 次 |
{n,} | 重复 n 次或更多次 |
{n,m} | 重复 n 次到 m 次 |
反义
反义的理解比较简单,其实就是元字符表示的反义,举个例子就很好理解了:比如说想要查找查找除了数字以外,其它任意字符都行的情况。
代码 | 反义符说明 |
---|---|
\W | 匹配任意不是字母或数字或下划线或汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意不是数字的字符 |
\B | 匹配不是单词的开始或结束的位置 |
[^x] | 匹配x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
字符类
当我学习到上面的那些的时候,我已经觉得自己无所不能了。
好吧,我承认我就是一个牛皮匠=。=
对于数字、字母或数字、空白,这些已经有对应的元字符表示这些字符集合,书写正则进行匹配的时候还是比较简单的,但是如果说我们想要匹配的事 10以内的奇数 ,那么原来的提供的那些元字符就不那么容易能够完成任务了。
其实正则表达式的语法中给我们提供了可以自定义字符集的方法,在方括号里面将你需要的字符列出来就行了,比如上面的例子中只要使用[13579]
就可以了。
分支条件
分支条件我的理解简单来说就是编程语言中的 逻辑或 ,下面是一段引用的正式解释:
正则表达式里的分枝条件指的是有几种规则,如果满足其中任意一种规则都应该当成匹配,具体方法是用 | 把不同的规则分隔开。
比如美国邮编的规则是5位数字,或者用连字号间隔的9位数字,那么我们可以使用分支条件处理:\\d{5}-\\d{4}|\\d{5}
需要注意的是:使用分支条件的时候需要特别注意条件的先后顺序,原因是匹配分枝条件时,将会从左到右地测试每个条件,如果满足了某个分枝的话,就不会去再管其它的条件了。
例如上述例子,如果改成\\d{5}|\\d{5}-\\d{4}
的话,就只能够匹配5位的邮编(以及9位邮编的前5位)。
分组
前面提到的重复限定符已经让我们知道了如何对单个字符进行重复表达(在字符后面加上重复限定符,如\\d+
),但是考虑到现实应用中我们会遇到对多个字符进行重复表达,比如:常见的 IP 地址,IP 地址的大致样子为 3位数字加上一个点 重复三次,再加上一个三位数字,这里需要用到分组表达,用小括号来指定子表达式(也叫做分组),然后你就可以指定这个子表达式的重复次数了,正则表达式为:(\\d{1,3}\\.){3}\\d{1,3}
当然,上面的表达式最终可能会匹配出不符合 IP 地址规范的地址,如:999.999.999.999,只能使用冗长的分组,选择,字符类来描述一个正确的IP地址:
((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)
关于分组,还会引申出更多的一些细节之处:
后向引用
当我们在正则表达式中使用分组的时候,如果分组匹配到内容(即小括号内的子表达式匹配的内容),正则匹配引擎会记录这段内容,并分配组号,这个过程我们可以形象的称之为 捕获 。
书写复杂的正则的时候可能会需要对分组捕获的内容进行进一步的处理,后向引用就是对前面某个分组匹配的内容进行进一步的处理。
在这里最简单的例子就是重复的两个单词放在一起,比如:hello hello 或者 hi hi ,注意两个单词之间有若干空格,配这种字符串的正则表达式为:\\b(\\w+)\\b\\s+\\1\\b
,其中\\1
就代表分组1匹配到的内容。
关于分组组号的分配这里有一些补充:
1、分组0对应整个正则表达式;
2、实际上组号分配过程是要从左向右扫描两遍的:第一遍只给未命名组分配,第二遍只给命名组分配--因此所有命名组的组号都大于未命名的组号;
3、可以使用(?:exp)这样的语法来剥夺一个分组对组号分配的参与权;
4、可以给分组添加一个自定义的组号:(?<name>exp)
,name 就是这个分组的组号,其中的<name>
可以替换成'name'
分组中捕获相关的常用语法:
代码 | 反义符说明 |
---|---|
(exp) | 匹配exp,并捕获文本到自动命名的组里 |
(?<name>exp) | 匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp) |
(?:exp) | 匹配exp,不捕获匹配的文本,也不给此分组分配组号 |
零宽断言和负向零宽断言
这两个概念的名称表面上看是非常难以揣测意思的,并且非常拗口。
其实无论是零宽断言还是负向零宽断言都是代表的一个位置,就像前面介绍过的\\b
、^
和 $
,零宽 的含义就是零宽度的一个位置,并且 断言 这个位置满足一定的条件,零宽断言是用来查找在 某些内容 之前或者之后的 一些东西,并且不包括 这些内容 ;而负向零宽断言与零宽断言是相反的关系,它用来匹配 一些东西 的前面或之后不是 某些内容,并且不包括 这些内容。
相关的常用语法:
代码/语法 | 反义符说明 |
---|---|
(?=exp) | 匹配exp前面的位置 |
(?<=exp) | 匹配exp后面的位置 |
(?!exp) | 匹配后面内容不是exp的位置 |
(?<!exp) | 匹配前面内容不是exp的位置 |
下面我们通过几个例子逐个对上面的语法进行一下应用:
-
(?=exp)
即零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。比如\\b\\w+(?=ing\\b)
,匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找I'm singing while you're dancing.时,它会匹配sing和danc; -
(?<=exp)
即零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。比如(?<=\\bre)\\w+\\b
会匹配以re开头的单词的后半部分(除了re以外的部分),例如在查找reading a book时,它匹配ading; -
(?!exp)
即零宽度负预测先行断言,断言此位置的后面不能匹配表达式exp。例如:\\d{3}(?!\\d)
匹配三位数字,而且这三位数字的后面不能是数字;\\b((?!abc)\\w)+\\b
匹配不包含连续字符串abc的单词。 -
(?<!exp)
即零宽度负回顾后发断言,断言此位置的前面不能匹配表达式exp。(?<![a-z])\\d{7}
匹配前面不是小写字母的七位数字。
贪婪和懒惰原则
当正则表达式中包含接受重复的限定符时,匹配的过程中默认会在满足表达式整体得到匹配的前提下匹配尽可能多的字符,这种行为被称作 贪婪匹配。
比如对于字符串 aabab ,如果使用表达式a.*b
进行匹配,匹配的结果将会是以a开始,以b结束的最长的字符串,即 aabab 本身。
如果我们需要匹配尽可能少的字符,那么我们就需要懒惰匹配,在重复限定符的后面加上?
,就代表进行懒惰匹配,刚刚的例子中,如果我们使用a.*?
进行匹配的话,匹配结果会是aab(第一到第三个字符)和ab(第四到第五个字符)。
在这里补充一下:为什么第一个匹配是aab(第一到第三个字符)而不是ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先开始的匹配拥有最高的优先权——The match that begins earliest wins。
懒惰限定符的常见语法:
代码 | 说明 |
---|---|
*? | 重复任意次,但尽可能少重复 |
+? | 重复1次或更多次,但尽可能少重复 |
?? | 重复0次或1次,但尽可能少重复 |
{n,m}? | 重复n到m次,但尽可能少重复 |
{n,}? | 重复n次以上,但尽可能少重复 |
在 iOS 开发中使用正则表达式
正则的基本语法在所有的正则引擎中基本差不多,貌似看有的语言使用的正则引擎对零宽断言和负向零宽断言不支持,使用 Swift 测试了一下,应该 iOS 还是支持零宽断言和负向零宽断言的正则语法。
在 iOS 中常见的正则表达式的使用方式有三种:
- 在 NSPredicate 中使用正则表达式:
- (BOOL)validateNumber:(NSString *) textString
{
NSString* number=@"^[0-9]+$";
NSPredicate *numberPre = [NSPredicate predicateWithFormat:@"SELF MATCHES %@",number];
return [numberPre evaluateWithObject:textString];
}
- NSString 方法中使用:
NSString *searchText = @"rangeOfString";
NSRange range = [searchText rangeOfString:@"^[0-9]+$" options:NSRegularExpressionSearch];
if (range.location != NSNotFound) {
NSLog(@"range :%@", [searchText substringWithRange:range]);
}
- NSRegularExpression:
//1. 要匹配的字符串
NSString *str = @"I'm singing while you're dancing";
//Pattern: 正则表达式语句
NSString *pattern = @"\\\\b\\\\w+(?=ing\\\\b)";
//2. 创建正则表达式
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
//3. 匹配结果
NSArray *result = [regex matchesInString:str options:NSMatchingReportCompletion range:NSMakeRange(0, str.length)];
// 打印结果
if (result.count == 0) {
NSLog(@"匹配出错");
} else {
NSLog(@"匹配成功: %lu",(unsigned long)result.count);
NSLog(@"%@", result);
}
同样的代码也用 Swift 进行了测试,结果一致:
// 定义正则表达式语句
let pattern = "\\\\b\\\\w+(?=ing\\\\b)"
// 定义正则表达式对象
let regex = try! NSRegularExpression(pattern: pattern, options: .CaseInsensitive)
// 待匹配的字符串
let str = "I'm singing while you're dancing"
// 进行匹配
let arr: [NSTextCheckingResult] = regex.matchesInString(str , options: NSMatchingOptions.ReportCompletion, range: NSMakeRange(0, str.characters.count))
print(arr)
在我个人的日常使用中,还是第三种用的稍微多一些,NSPredicate 基本用的机会很少,也不是太了解,后续有机会了解的话会补充到本篇博客中。
结尾
- 本篇博客中只是罗列了正则表达式中较为基础的部分语法,其实有许多东西我也是边学习边总结,很多语法也不是特别清楚,后面有补充的会一点一点补充到这篇博客中;
- 正则所涵盖的语法点非常多也比较琐碎,正则表达式书写出来也比较的绕而且乱七八糟的符号也比较乱,所以需要不断的练习和使用才能灵活的解决开发中的实际问题,就我个人而言也是需要加强练习,改掉工作中依赖搜索的懒毛病(前提是开发进度允许=。=);
最后,祝大家玩的愉快!