系统地学习正则表达式(二):进阶篇
在看这篇文章前,希望大家先看一下《系统地学习正则表达式(一):基础篇》。今天这篇我们将讲解子表达式
和后向引用
的使用。后面会持续更新。
理解子表达式
通过(
和)
括起来的就是子表达式。
下面是一个例子:用来匹配IP 地址的正则表达式。 IP 地址是由" . "分隔的四组数字,如12.159.46.200。因为每个部分的数字都可以为一个、两个或者三个数字字符,这个匹配模式可以表示为\d{1,3}
:
正则表达式:
\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}
待匹配文本:12.159.46.200
匹配后结果:12.159.46.200
分析:每个\d{1,3}
的实例都匹配了 IP 地址的一个数字。四个数字则是被\.
表示的" . "分隔开的。\d{1,3}\.
模式重复了三遍,因此可以使用重复操作来处理。下面是相同例子的另外一个版本:
正则表达式:
(\d{1,3}\.){3}\d{1,3}
待匹配文本:12.159.46.200
匹配后结果:12.159.46.200
分析:此模式可以和前面的效果是一样的。表达式\d{1,3}\.
使用(
和)
括起来从而组成子表达式。(\d{1,3}\.){3}
重复了子表达式三次(也就是 IP 地址的前三个数字),最后的\d{1,3}
匹配最后的数字。
使用子表达式来分组是很重要的,尽管这个例子中根本不包括重复。来看一个例子:
正则表达式:
19|20\d{2}
待匹配文本:1967-08-17
匹配后结果:19
67-08-17
分析:19|20\d{2}
用来定位一个前两个数字只能为 19或者20
的四位数的年份。但是很显然这个正则并没有实现预想的效果。|
操作符从左到右读取,将19|20\d{2}
分析为要么19
,要么20\d{2}
,也就是匹配数字 19
或者20开头的四个数字字符
。
解决方法是将19|20
作为一个子表达式,(19|20)\d{2}
就能够匹配所有19 和 20 开头的四个数字了。
嵌套子表达式
子表达式可以嵌套。实际上,子表达式可以一层一层嵌套在子表达式内。为了演示嵌套子表达式的用法,我们再来看看查找 IP 地址的例子。
其实上面例子中(\d{1,3}\.){3}\d{1,3}
匹配IP 地址是有问题的,因为非法的 IP 地址也将被匹配。IP 地址中的每个数字都是小于 255 的。而上面的模式可以匹配 300甚至是999 ,而这些实际上都是非法的 IP 地址。
下面定义了所有合法 IP 地址所需要满足的一种条件:
所有的一位数和两位数
三位数的第一位为 1
如果三位数的第一位为 2 ,且第二位从 0 到 4
如果三位数的前二位为 25 ,且第三位从 0 到 5
当定义了需要匹配的情况后,就比较容易实现可以工作的模式。下面是个例子:
(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.){3}((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))
分析:这个模式可以工作的原因是一系列的嵌套子表达式。首先从(((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))\.)
子表达式开始。包含了四个嵌套子表达式。(\d{1,2})
可以匹配一位数和两位数(0到99)。(1\d{2})
匹配了任何第一位为 1 的三位数(100到199) 。(2[0-4]\d)
匹配数字从 200 到 249 。(25[0-5])
匹配数字从 250 到 255 。每个子表达式都是通过“ | ”包括在另一个子表达式中。在数字范围之后是\.
表示的" . ",然后这个系列括起来作为子表达式并重复三遍(使用{3}
)。最后,((\d{1,2})|(1\d{2})|(2[0-4]\d)|(25[0-5]))
被用来匹配最后一个 IP 地址的数字(没有了\.
后缀)。由于将四个数字都是限制在了 0 到 255 之间,所以此模式可以匹配所有的合法 IP 地址。
理解后向引用
HTML 开发者经常使用标题标签(<H1>到<H6>,包括相应的结束标签</H1>到</H6>)。假设你需要定位所有的标题标签:
正则表达式:
<[hH][1-6]>.*?</[hH][1-6]>
待匹配文本:<H1>title 1</H1> — <H6>title 6</H6>
匹配后结果:<H1>title 1</H1>
—<H6>title 6</H6>
分析:<[hH][1-6]>
可以匹配所有的开始标签,而</[hH][1-6]>
可以匹配所有的结束标签。
注意:我们这里使用了.*?
而不是.*
。正如在上一篇文章解释的一样,*
的量词是贪婪的,所以模式<[hH][1-6]>.*</[hH][1-6]>
将匹配从<H1> 直到 </H6> 。所以可以使用非贪婪量词.*?
来解决这个问题。
接着看这个例子:
正则表达式:
<[hH][1-6]>.*?</[hH][1-6]>
待匹配文本:<H1>title</H6>
匹配后结果:<H1>title</H6>
分析:采用 <H1> 开始而采用 </H6> 的标题标签是非法的,但是现在的模式可以匹配。问题在于匹配的第二个部分(匹配结束的标签)没有办法知道匹配第一部分(匹配开始的标签)是什么。这时候就需要后向引用了。
使用后向引用匹配
后向应用
就是引用前面的子表达式。你可以将后向应用理解成变量。例如\1
匹配模式中第一个子表达式。同理,\2
将匹配第二个子表达式,\3
将匹配第三个。我们结合例子来理解。一个子表达式可以通过后向引用根据需要引用多次。
在上个例子中,<[hH][1-6]>.*?</[hH][1-6]>
会匹配非法的标题。我们可以使用后向引用来解决:
正则表达式:
<[hH]([1-6])>.*?</[hH]\1>
待匹配文本:<H1>title</H1>—<H1>title</H6>
匹配后结果:<H1>title</H1>
—<H1>title</H6>
分析:就像以前一样,<[hH]([1-6])>
将匹配任何的标题标签。但是和以前不一样的是,这里的[1-6]
使用了小括号括起来成为了子表达式。这样,匹配结束标签的模式可以通过</[hH]\1>
中的\1
来引用此子表达式。(1-6)
是一个可以匹配数字 1 到 6 的子表达式,\1
因此可以匹配相同的数字。在这种情况下,"<H1>title</H6>"将不能匹配。
注意:后向引用只能够引用子表达式(需要使用小括号括起来),后向引用语法在不同的正则表达式实现中可能是不一样的。引用的匹配一般是从 1 开始。在大多数的正则表达式实现中,0 可以用来引用整个表达式。
执行替换操作
到现在为止我们所看到的正则表达式都是进行搜索,在一段文本中定位单词。正则表达式还可以用来执行替换操作。举个例子,将CA 替换成California和将MI替换成Michigan 并不是正则表达式需要完成的工作。尽管使用正则表达式也是合法的,但是没有必要这么做。事实上,在这里如果使用简单的字符串操作函数的话过程将会变得更加容易。
让我们来看一个例子,将313-555-1234
格式的电话号码重新格式化为(313) 555-1234
格式:
查找表达式:
(\d{3})(-)(\d{3})(-)(\d{4})
替换表达式:($1) $3-$5
文本
313-555-1234
248-555-9999
结果
(313) 555-1234
(248) 555-9999
分析:(\d{3})(-)(\d{3})(-)(\d{4})
匹配了一个电话号码,并分成了五个子表达式。(\d{3})
匹配刚开始的三个数字并作为第一个子表达式,(-)
匹配" - "并作为第二个子表达式,依此类推。这五个部分可以根据需要单独引用,($1) $3-$5
只是使用了其中的三个子表达式。因此“313-555-1234”改变为了“(313) 555-1234”。
在Xcode中查找和替换文本:
查找
替换
其实正则表达式还是很简单的,大家可以多找些例子熟悉一下。用的多了就熟了。
正则表达式在iOS中的使用。
NSPredicate
NSPredicate通常用来验证一个字符串是不是符合某种格式,例如验证一个字符串是不是由数字和字母组成的:
//待匹配字符串
NSString *string = @"song123";
//正则表达式
NSString *regex = @"^[a-z0-9A-Z]*$";
//创建predicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];
//用predicate匹配string。result为匹配结果,YES or NO。
BOOL result = [predicate evaluateWithObject:string];
关于NSPredicate还有一些其他的可以使用方法们,具体可以查看官方API。
NSString
NSString可以用正则来查找自己当中符合要求的子字符串,例如查找字符串中的数字:
//待匹配字符串
NSString *string = @"song123";
//使用正则\d+去string中进行匹配,得到的时匹配到的range,在这里为{4,3}。
NSRange range = [string rangeOfString:@"\\d+" options:NSRegularExpressionSearch];
//如果匹配到就打印匹配到的子字符串,在这里为123。
if (range.location != NSNotFound)
{
NSLog(@"%@",[string substringWithRange:range]);
}
分析:\d+
用来查找数字,rangeOfString:options:
会返回一个NSRange,用来接收匹配的范围。options
必须要用NSRegularExpressionSearch
,代表用正则去匹配。range.location==NSNotFound
的话代表匹配不到结果。当写正则字符串时,\\
需要写成\\\\
,所以,\d+
需要写成\\\\d+
。
NSRegularExpression
对于匹配字符串中的数字,我们也可以用到NSRegularExpression这个类实现。它可以用户来查找字符串中符合要求的第一个匹配结果
或者所有匹配结果
。
查找字符串中第一个匹配结果
//待匹配字符串
NSString *string = @"123abc45fgt7tyu";
//正则表达式
NSString *pattern = @"\\d+";
//创建NSRegularExpression对象并指定正则表达式
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
options:NSRegularExpressionCaseInsensitive
error:nil];
//查找第一个匹配结果,如果查找不到的话match会是nil
NSTextCheckingResult *match = [regex firstMatchInString:string
options:NSMatchingReportCompletion
range:NSMakeRange(0, [string length])];
if (match)
{
//如果查找到就打印出来,结果为123
NSLog(@"%@",[string substringWithRange:match.range]);
}
分析:firstMatchInString:options:range
会查找字符串中第一个匹配,所以123abc45fgt7tyu
的匹配结果为123
。
查找字符串中所有匹配结果
//待匹配字符串
NSString *string = @"123abc45fgt7tyu";
//正则表达式
NSString *pattern = @"\\d+";
//创建NSRegularExpression对象并指定正则表达式
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
options:NSRegularExpressionCaseInsensitive
error:nil];
//查找所有匹配结果
NSArray* matches = [regex matchesInString:string
options:NSMatchingReportProgress
range:NSMakeRange(0, [string length])];
//如果有匹配就打印出来,结果为123 45 7
if (matches.count > 0)
{
for (NSTextCheckingResult *match in matches)
{
NSLog(@"%@",[string substringWithRange:match.range]);
}
}
分析:matchesInString:options:range:
会查找字符串中的所有匹配,结果是一个包含NSTextCheckingResult
的数组。所以123abc45fgt7tyu
的匹配结果为123
、45
和7
。
创建NSRegularExpression
对象时的options
参数为NSRegularExpressionOptions
类型,可选值以及代表的意义如下:
typedef NS_OPTIONS(NSUInteger, NSRegularExpressionOptions) {
NSRegularExpressionCaseInsensitive = 1 << 0, //不区分大小写
NSRegularExpressionAllowCommentsAndWhitespace = 1 << 1, //忽略空白和注释
NSRegularExpressionIgnoreMetacharacters = 1 << 2, //将所有的patter当作普通字符串
NSRegularExpressionDotMatchesLineSeparators = 1 << 3, // 允许.匹配所有字符
NSRegularExpressionAnchorsMatchLines = 1 << 4, // 允许^,$匹配每一行的开头和结尾
NSRegularExpressionUseUnixLineSeparators = 1 << 5, // 只把\n识别为换行符
NSRegularExpressionUseUnicodeWordBoundaries = 1 << 6 // 使用 Unicode TR#29 规定的边界,否则,使用传统的正则表达式的词边界
};
关于NSRegularExpression还有一些其他的可以使用方法们,具体可以查看这里。
一些常用的正则:
邮箱:^[a-zA-Z0-9]{4,}@[a-z0-9A-Z]{2,}\\.[a-zA-Z]{2,}$
手机号码:^((13[0-9])|(15[^4\\D])|(18[0,2,5-9]))\\d{8}$
大陆固定电话号码:^\\d{4}-|\\d{3}-)?(\\d{8}|\\d{7}$
身份证号:\\d{14}[[0-9],0-9xX]
Email地址:^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\.\\w+([-.]\\w+)*$
纯数字:^[0-9]*$
由数字和英文字母组成:^[A-Za-z0-9]+$
QQ号:^[1-9][0-9]\{4,\}$
中国邮政编码:^[1-9]\\d{5}(?!\\d)$
URL:^http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?$
纯汉字:^[\u4e00-\u9fa5]{0,}$