一、正则表达式的用途(搜索和替换)
- 1.1.正则表达式(
regular expression
,简称regex
)是一种工具,和其他的工具是一样的,它是人们为了解决某一类问题而发明的,要想理解正则表达式及其功用,最好的办法是了解它们可以解决什么样的问题。 - 1.2. 在编写处理字符串的程序时,经常会有查找符合某些复杂规则的字符串或者替换一些字符串。正则表达式就是用于描述这些规则的工具。换句话说,正则表达式就是记录文本规则的代码
- 1.3.正则表达式是对字符串操作的一种逻辑公式,用事先定义好的一些特定字符、及这些特定字符的组合,组成一个"规则字符串",这个"规则字符串"用来表达对字符串的一种过滤逻辑。
- 1.4.-几乎所有的程序设计语言都支持正则表达式,例如:
OC
,java
,c#
,python
,js
等 - 1.5.在很多文本编辑器里,可以使用正则表达式进行检索,正则表达式是文本处理方面功能最强大的工具之一,正则表达式语言来构造正则表达式(最终构造出来的字符串就称为正则表达式),正则表达式用来完成搜索和替换的操作,当然,Xcode同样支持正则表达式!
二、匹配单个字符(下面的都将调用textRegex()方法)
-
2.1.匹配纯文本(
"ben"
)
import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 匹配单个字符 view.addSubview(label) let str = "Hello, my name is Ben. Please visit my ben website at https://www.forta.com/." label.attributedText = textRegex(pattern: ".a.",str: str, font: 22) } // 1.匹配纯文本 func textRegex(pattern: String,str: String,font: CGFloat) -> NSMutableAttributedString{ //富文本设置 let attributeString = NSMutableAttributedString(string:str) do { // 1.1.定义规则 //let pattern = "ben" // 1.2.创建正则表达式对象 let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive) // 1.3.开始匹配 let res = regex.matches(in: str, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, str.count)) for checkingRes in res { print("range\(checkingRes.range)") // substring 截取符合规定规则的字符串 print((str as NSString).substring(with: checkingRes.range)) //从文本checkingRes.range个字符字体HelveticaNeue-Bold attributeString.addAttribute(NSAttributedStringKey.font, value: UIFont(name: "HelveticaNeue-Bold", size: 22)!,range: checkingRes.range) //设置字体颜色 attributeString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.blue,range: checkingRes.range) //设置文字背景颜色 attributeString.addAttribute(NSAttributedStringKey.backgroundColor, value: UIColor.green,range: checkingRes.range) } return attributeString } catch { print(error) } return attributeString } lazy var label: UILabel = { let label1: UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height)) label1.font = UIFont.systemFont(ofSize: 22) label1.numberOfLines = 0 label1.backgroundColor = UIColor.white return label1 }() }
-
2.2.匹配任意字符(注意:下面的两句代码调了textRegex方法)
前面见到的正则表达式都是一些静态的纯文本,它们根本体现不出来正则表达式的威力,下面,我们一起来看看如何用正则表达式去匹配不可预知的字符。在正则表达式里,特殊字符(或字符集合)用来给出要搜索的东西。"."字符(英文句号)可以匹配任何一个单个的字符。
let str = "Hello, my name is Ben. Please visit my ben website at https://www.forta.com/." label.attributedText = textRegex(pattern: ".a.",str: str, font: 22)
-
2.3. 用正则表达式w.t进行的搜索将匹配到wht和wat(还能匹配到一些毫无意义的单词)
let str = "HellomynameisBen.whtareyouwatoulikeme" label.attributedText = textRegex(pattern: "w.t",str: str, font: 22)
-
2.4. “.”字符可以匹配任何单个的字符,字母,数字甚至只“.”字符本身
let str = "HellomynameisBen.whtareyouwatoulikeme" label.attributedText = textRegex(pattern: "n.",str: str, font: 22)
-
2.5.匹配特殊字符
“.”字符在正则表达式里面有着特殊的含义,如果模式里需要一个“.”,就要想办法告诉正则表达式你需要的是“.”字符本身而不是它在正则表达式里的特殊含义。为此,你必须在“.”前面加一个(反斜杠)字符来对它进行转义。\是一个元字符(metacharacter,表示“这个字符有特殊的含义”)。
-
在正则表达式里面,\字符永远出现在一个有着特殊含义的字符串序列的开头,这个序列可以由一个或者多个字符构成。下面看到的是
\.
序列,在后面的章节里面还会看到更多使用了\字符的例子。
let str = "Hellomynamei.sBen.whtarbyouwatoulikea.se" label.attributedText = textRegex(pattern: ".e.\\.s",str: str, font: 22)
注意:如果需要搜索\本身,就必须对\字符进行转义;相应的转义序列是两个连续的反斜杠字符
\\
。
三、匹配一组字符(下面会涉及字符集合)
3.1.说明字符集合则能匹配特定的字符和字符区间。
-
3.2.匹配多个字符中的某一个
let str = "Hellomynamei.sBen.whtareb.syouwatoulikea.se" label.attributedText = textRegex(pattern: "[mk]e.\\.s",str: str, font: 22)
解释:
"[mk]e.\\.s"
是e
的前面只能是 字母m
或者k
-
3.2.利用字符集合区间
A-Z: 匹配从A到Zdenka所有大写字母
a-z: 匹配a-z的所有小写的字母
A-F: 匹配从A到F所有大写字母
A-z: 匹配ASCII字符A到ASCII字符z的所有字母。这个模式一般不常用,因为它还包含着[^等在ASCII字符表里面排列在z和a之间的字符。字符区间的首、尾字符可以是ASCII字符表里的任意字符。但在实际工作中,最长哟个的字符区间还是数字字符区间和字母字符区间。提示:在定义一个字符区间一定要避免这个区间的尾字符小于它的首字符(例如:[3-1])。这种区间是没有意义的,二往往会让整个模式失效。
-
(连字符):是一个特殊的元字符,作为元字符它只能用在[和]之间。在字符集合意外的地方,“-”只是一个普通的字符,只能与“-”本身相匹配。因此,在正则表达式里面,“-”字符不需要被转义。
let str = "<BODY BGCOLOR=#336633 TEXT=#FFFFFF MARGINWIDTH=0 MARGINHEIGHT=0 TOPMARGIN=0 LEFTMARGIN=0>" label.attributedText = textRegex(pattern: "#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]",str: str, font: 22)
- 分析:这里使用的模式以普通字符
#
开头,随后是6个同样的[0-9A-Fa-f]
字符集合。这将匹配一个由字符#
开头,然后6个数字或字母A-F(大小写均可)的字符串
- 分析:这里使用的模式以普通字符
-
3.3.取非匹配
字符集合通常用来指定一组必须匹配其中之一的字符。但在某些场合,我们需要反过来做给出一组不需要得到的字符,换句话说,除了那个字符集合的字符,其他的字符都可以匹配。我们可以用元字符
^
来表明你想对一个字符集合进行取非匹配。这与逻辑运算很相似,只是这里的操作数是字符集合而已。
let str = "Real Love is n2 just instin1t, but intent." label.attributedText = textRegex(pattern: "[no][^a-zA-Z]",str: str, font: 22)
- 解释:
[no]
意思是第一个取n
或者o
都可以,[^a-zA-Z]
意思是除了小写a~z
和大写A~Z
的都不可以取,^
是非的意思
^
的效果将作用于给定字符集合里的所有字符或字符区间,而不是仅限于紧跟在^
字符后面的那一个字符或者字符区间。
- 解释:
3.4.总结
元字符[和]用来定义一个字符集合,其含义是必须匹配该集合里的字符之一。定义一个字符集合的具体的做法有两种:1、把所有的字符都列举出来;2、是利用元字符-
以字符区间的方式给出。字符集合可以用元字符^
来求非;这将把给定的字符集合强行排除在匹配操作以外,除了该字符集合里的字符,其他字符都可以进行匹配。
四、使用元字符(利用元字符去匹配特定的字符或字符类型)
-
4.1.对特定的字符进行转义
元字符是一些在正则表达式里有着特殊含义的字符。英文句号
.
是一个元字符,他可以用来匹配任意一个单个字符,类似地,在方括号[
也是一个元字符,它标志着一个字符集合的开始。
元字符是无法用来替代它们本身,因为在正则表达式里面有着特殊的含义,比如,你不能用一个[
来匹配[
本身,也不能使用.
来匹配.
本身,看下面的例子
let str = "var myArray = new Array() if (myArray[0] = 0)" label.attributedText = textRegex(pattern: "myArray[0]",str: str, font: 22)
说明:为什么没有匹配到
myArray[0]
呢?因为:[和]在正则表达式里是用来定义一个字符集合(而不是[和]本身)的元字符,所以,myArray[0]
将匹配myArray
后面跟着一个该集合成员的情况,而那个集合只有一个成员0
.因此,myArray[0]
只能匹配到myArray0
。 -
4.2. 如何才能匹配到myArray[0]呢???
答案是对元字符进行转义 前面加
\\
反斜杠,这样就可以匹配本身了,[
和]
就不再是元字符。
let str = "var myArray = new Array() if (myArray[0] = 0)" label.attributedText = textRegex(pattern: "myArray\\[0]",str: str, font: 22)
如果想匹配到0-9之间的数字都可以匹配的话,就需要对集合进行匹配了
let str = "var myArray[8] = new Array() if (myArray[0] = 0)" label.attributedText = textRegex(pattern: "myArray\\[[0-9]]",str: str, font: 22)
任何一个元字符都可以通过给它加上一个反斜杠字符 \
作为前缀的办法来转义,对元字符进行转义需要用到\
字符也是一个元字符,它的特殊含义是对其他的额字符进行转义,在需要匹配本身的时候,我们必须把它转移为\\
let str: String = "\\Users\\wangchong\\Desktop\\"
label.attributedText = textRegex(pattern: "\\\\",str: str, font: 22)
-
4.2.匹配空白字符
元字符大多分为2种,一种是用来匹配文本的(比如
.
),另一种是正则表达式的语法所要求的(比如[和])。随着学习的深入,我们会发现越来越多的这两种语法,下面是匹配空白字符的元字符。
let str: String = "Oviri is an \n\n 1894 ceramic sculpture by the French artist Paul Gauguin." label.attributedText = textRegex(pattern: "\n\n",str: str, font: 22)
\n\n
作为文本结束的标签- 元字符我们已经见了好几个了,但是差异性我们真的链接吗?比如
.
和[
是元字符(前提是没有对他们进行转义);f
、n
、r
、t
、也是元字符(前提是对它们进行了转义),否则它们将被视为普通的字符,从而也只能匹配它们本身。
- 元字符我们已经见了好几个了,但是差异性我们真的链接吗?比如
-
4.3.匹配特定的字符类别
.
匹配任意字符[和]
多个字符中的某一个^
取非匹配字符集合(匹配多个字符中的某一个)是最常见的匹配形式,而一些常用的字符集合可以用特殊的元字符来代替。这些元字符匹配的是某一类别的字符(术语为字符类)。看下面的例子
-
4.3.1. 匹配数字(与非数字)
let str: String = "Oviri is an 1894 ceramic scu1lpture by the French artist Paul Gauguin." label.attributedText = textRegex(pattern: "\\d",str: str, font: 22)
提示:正则表达式的语法是区分字母大小写的。
\d匹配数字
,\D
匹配非数字,它们正好相反。其他的元字符也一样-
4.3.2.匹配字母和数字(与非字母和数字)
-
例如
let str: String = "你%¥吗()==12_3" label.attributedText = textRegex(pattern: "\\W",str: str, font: 22)
-
4.3.3.匹配空字符串(与空白字符)
看下面的例子
let str: String = "11213 A1C2E3 48075 48237 M14F2 90046 H1H2H2" label.attributedText = textRegex(pattern: "\\s",str: str, font: 22)
提示:用来匹配退格字符的
[\b]
元字符就是一个特例;它不在类元字符\s
的覆盖范围内,当然也就没有被排除在类元字符\S
的覆盖范围外。
-
4.4.使用POSIX字符类
let str: String = "K$P1-21_3@" label.attributedText = textRegex(pattern: "[:alnum:]",str: str, font: 22)
五、重复匹配
-
5.1.匹配一个或者多个字符(
+
)-
+
号匹配一个或多个字符(至少一个;不匹配0
个字符的情况),比如c
匹配c
本身,c+
将匹配一个或多个连续出现的c
。类似地,[0-9]
匹配任意单个数字,[0-9]+
将匹配一个或多个连续的数字。
let str: String = "The world for.text@text.text is a fine place, and worth fighting The world is a finefor.hext@tdxt.text place, and worth fighting for." label.attributedText = textRegex(pattern: "\\w+@\\w+\\.\\w+",str: str, font: 22)
-
如何把上面的
for
也匹配上
let str: String = "The world for.text@text.text is a fine place, and worth fighting The world is a finefor hext@tdxt.text place, and worth fighting for." label.attributedText = textRegex(pattern: "[\\w.]+@\\w+\\.\\w+",str: str, font: 22)
说明:上面没有对字符集合
[\w.]
里的.
字符进行转义。尽管如此,它还是把原始文本里的.
字符匹配出来了。一般来说,当在字符集合里使用的时候,像.
和+
这样的元字符将被解释为普通的字符,不需要被转义,但转义了也没有坏处。[\w.]
的使用效果与[\w.\.]
是一样的 -
-
-
5.2.匹配零个或多个字符
let str: String = "The world .text@text.com is a fine place, and worth fighting The world is a finefor hext@tdxt.text place, and worth fighting for." label.attributedText = textRegex(pattern: "\\w+[\\w.]*@[\\w.]+\\.\\w+",str: str, font: 22)
*
与+
的区别是:+
匹配一个或者多个字符(或字符集合),最少也要匹配一次;*
匹配零个或任意多个字符(或字符集合),可以没有匹配。
*
是一个元字符。如果需要匹配*
本身,就必须对它进行转义。 -
5.3.匹配零个或者一个字符(
?
)
?
只能匹配一个字符(或字符集合)的零次或一次出现,最多不超过一次,如果需要在一段文本里匹配某个特定的字符(或字符集合)而该字符可能出现,也可能不出现,?
无疑是最佳的选择。
let str: String = "The URL is http://www.forta.com/, to connect securely use https://www.forta.com/ instand." label.attributedText = textRegex(pattern: "https?://[\\w.]+",str: str, font: 22)
同样
?
也是一个元字符,匹配本身的话也需要进行转义。 -
5.4.匹配的重复的次数
-
5.4.1、为重复匹配次数设定一个精确的值
let str: String = "<BODY BGCOLOR=#336633> TEXT=#FFFFFF MARGINWIDTH=0 MARGINHEIGHT=0 TOPMARGIN=0" label.attributedText = textRegex(pattern: "[0-9A-Fa-f]{6}",str: str, font: 22)
{6}
:意味着要连续匹配6次[0-9A-Fa-f]
区间的值 -
5.4.2、为重复匹配次数设定一个区间
{}
:语法不仅仅可以设置匹配的次数,还可以设置匹配的最小和最大次数
如: {1,3}代表连续匹配1~3次之间,当然之前的?其实就是等价于?
={0,1}
-
5.4.3、匹配“至少重复多少次”
{}
语法还可以设置至少重复的次数,也就是不用设置最大重复的次数,比如{2,}
let str: String = "1001: $496.80 1002: $1290.69 1003: $26.43 1004: $613.42 1007" label.attributedText = textRegex(pattern: "\\d+: \\$\\d{3,}\\.\\d{2}",str: str, font: 22)
注意:
{3,}
中的逗号千万别漏掉,否则就变成了精准匹配3位了。
-
-
5.5.防止过度匹配
?
只能匹配0个或者一个字符,{n}和{m,n}也有一个重复次数的上限,之前的例子都没有上限,下面举一个例子来说明
说明:贪婪型为什么会匹配and,那是因为贪婪型元字符在进行匹配的时候是多多益善而不是适可而止。它们会尽可能地从一段文本的开头一直匹配到这段文本的末尾,而不是从这段文本的开头匹配到第一个匹配为止。
- 解决办法是:在贪婪型元字符的后面加上
?
-
下面是常用的贪婪型元字符和他们的懒惰型版本
- 解决办法是:在贪婪型元字符的后面加上
5.6.总结
正则表达式的真正威力体现在重复次数匹配方面。上面介绍了+
(匹配字符或字符集合的一次或多次重复出现),*
(匹配字符或字符集合的0次或多次重复出现),?
(匹配字符或字符集合的0次或一次出现)等几个元字符的用法。要想获得更精准的控制,你可以使用{}
语法来精确地设定一个重复次数或是重复次数的最小值和最大值。元字符分“贪婪型”和“懒惰型”两种,使用时要防止过度匹配时候可以使用懒惰型元字符来创造适合自己的表达式。
六、位置匹配
-
6.1.边界
下面的例子只是想配
cat
这个单词,但是scattered
也被匹配到了,这不是我们想要的结果,后面再解决。
let str: String = "The cat scattered his food all over the room." label.attributedText = textRegex(pattern: "cat",str: str, font: 22)
-
6.2.单词边界
\b
可以用来限制单词的边界,也就是规定单词的开头和结尾。,如下所示let str: String = "The cat scattered his food all over the room." label.attributedText = textRegex(pattern: "\\bcat\\b",str: str, font: 22)
说明:
\b
:匹配的是这样一个位置,这个位置位于一个能够用来构成单词的字符(字母,数字,下划线,也就是与\w
相匹配的字符)和一个不能用来构成单词的字符(也就是与\w相匹配的字符)之间。
注意:要想精准的匹配某一个单词必须在其前后都要加上\b单词\b
-
如果不想匹配一个单词的边界,那么就可以使用
\B
了,看下面的例子
let str: String = "Please enter the nine-digit id as it appears on your color - coded pass-key." label.attributedText = textRegex(pattern: "\\B-\\B",str: str, font: 22)
\\B-\\B
: 将匹配一个前后都不是单词边界的连字符。nine-digit
和pass-key
不能与之匹配,但color - coded
中的连字符可以与之匹配。
匹配单词的还有\\<单词\\>
,但是swift4.0不支持,本人已经验证过了。 -
6.3、字符的边界
单词边界可以用来进行与单词有关位置的匹配(单词的开头,单词的结束,整个单词等)。字符串边界有着类似的用途,只不过是用来进行与字符串有关的位置匹配而已(字符串的开头,字符串的结束,整个字符串等)。用来定义字符串边界的元字符有两个:一个是用来定义字符串开头的
^
,另一个是用来定义字符串结尾的$
.
- 总结:正则表达式不仅仅可以用来匹配任意长度的文本块,还可以用来匹配出现在字符串中特定位置的文本。
\b
用来指定一个单词的边界(\B
刚好相反)。^
和$
用来指定字符串的边界(字符串的开头和字符串的结束)。如果与(?m)
配合使用,^
和$
还将匹配在一个换行处开头或结束的字符串(此时,换行符将被视为一个字符串分隔符)。
七、使用字表达式(元字符
和字符
是正则表达式
的基本构件
)
-
7.1、什么是子表达式?
let str: String = "Hello, my name is Ben Forta, and I am the author of books on SQL, ColdFusion, WAP, Windows 2000, and other subjects." label.attributedText = textRegex(pattern: " {2,}",str: str, font: 22)
:是HTML语言中的非换行空格字符。在这里使用模式 {2,}
的本意是希望它能把 连续两次或更多次的重复出现找出来,但它没能给出我们所预期的结果。为什么会这样?因为{2,}只作用于紧挨着它的前一个字符,那是一个分号。如此一来,这个模式只能匹配像 ;;;
这样的文本,但无法匹配  
。 -
7.2、子表达式
字表达式:是一个更大的表达式的一部分;把一个表达式划分为一系列表达式的目的是为了把那些字表达式当做一个独立元素来使用。字表达式必须用
()
括起来。()
是元字符。如果要匹配()
的话需要进行对它转义。看下面的例子
let str: String = "Hello, my name is Ben Forta, and I am the author of books on SQL, ColdFusion, WAP, Windows 2000, and other subjects." label.attributedText = textRegex(pattern: "( ){2,}",str: str, font: 22)
解释:
( )
是一个字表达式,它将被视为一个独立的元素,而紧跟着在它后面的{2,}
将做用于这个字表达式而不是仅仅作用于;
。
let str: String = "Pinging hog.forta.com [12.159.46.200] with 32 bytes of data:" label.attributedText = textRegex(pattern: "(\\d{1,3}\\.){3}\\d{1,3}",str: str, font: 22)
上面的例子中
(\\d{1,3}\\.)
是一个子表达式,上面的匹配规则还可以写为"(\\d{1,3}\\.?){4}"
和"(\\d{1,3}\\.){3}(\\d{1,3})"
。-
下面再写一个子表达式的匹配
let str: String = "Hope clouds 1987-09-02 observation." label.attributedText = textRegex(pattern: "(19|20)\\d{2}",str: str, font: 22)
解释:
|
是或的意思。 -
-
7.3、子表达式的嵌套(下面的例子没有正确匹配:待解决)
子表达式是允许嵌套的,实际上,子表达式允许多重嵌套,这种嵌套的层次在理论上没有限制,但在实际上应该合理的嵌套。下面的例子将全面的展示
let str: String = "Pinging hog.forta.com [12.159.46.200] with 32 bytes of data:" label.attributedText = textRegex(pattern: "(((\\d{1,2})|(1\\d{2})|(2[0-4]\\d)|(25[0-5]))\\.){3}(((25[0-5])|(2[0-4]\\d)|(1\\d{2})|\\d{1,2}))" ,str: str, font: 22)
注意:最后一个匹配的时候三位数要放到前面,否则的话匹配到两位数,
200
的最后一个0
就不能匹配上了 7.4、总结
子表达式的作用是把同一个表达式的各个相关部分组合在一起。子表达式必须用()
来定义。子表达式的常见用途包括: 对重复次数元字符的作用对象作出精确的设定和控制、对|
操作符条件作出准确额定义等,如有必要,子表达式还允许嵌套使用。
八、回溯引用,前后一致匹配
-
8.1、回溯引用的作用(看下面的例子)
let str: String = "<BODY><H1>Welcom to my Homepage</H1>\nContent is divided into two sections:<BR>\n <H2>ColodFusion</H2>\nInformation about Macromedia ColodFusion.\n <H2>Wrieless</H2>\n Information about Buletooth, 802.11, and more.\n <H2>This is not valid HTML</H3>\n</BODY>" label.attributedText = textRegex(pattern: "<[Hh][1-6]>.*?</[Hh][1-6]>" ,str: str, font: 22)
解释:上面匹配按照规则是正确的,但是按照语法最后一个匹配是不正确的,因为H2和H3是不对应的标签。为了解决这个问题,下面阐述:回溯引用
-
8.2、回溯引用匹配
-
8.2.1、寻找连着的两个相同单词
let str: String = "This is a block of of text,serveral words here are repeated,and and they should not not be." label.attributedText = textRegex(pattern: "[ ]+(\\w+)[ ]+\\1" ,str: str, font: 22)
解释:
[ ]+
匹配一个或多个空格,\w+
匹配一个或多个字母数字字符,[ ]+
匹配随后的空格,注意\w+
是括在括号里的,它是一个子表达式。这个子表达式不是用来进行重复匹配的,这里不涉及到重复匹配的问题。这个子表达式只是把整个模式的一部分单独划分出来以便在后面引用。这个模式的最后一部分是\1
;这是一个回溯引用。而它引用的正是前面划分出来的那个子表达式;当(\w+)
匹配到单子of
的时候,\1
也匹配单词of
;当(\w+)
匹配到单词and的时候,\1
也匹配到单词and
。
回溯引用指的是模式的后半部分引用在前半部分中定义的子表达式。
\1
代表模式里的第一个子表达式,\2
代表模式里的第2
个子表达式,以此类推,上面的例子将匹配到同一个单词的连续两次重复出现。 -
8.2.2、解决8.1的最后一个不正确标签的匹配
let str: String = "<BODY><H1>Welcom to my Homepage</H1>\nContent is divided into two sections:<BR>\n <H2>ColodFusion</H2>\nInformation about Macromedia ColodFusion.\n <H2>Wrieless</H2>\n Information about Buletooth, 802.11, and more.\n <H2>This is not valid HTML</H3>\n</BODY>" label.attributedText = textRegex(pattern: "<[Hh]([1-6])>.*?</[Hh]\\1>" ,str: str, font: 22)
解释:
([1-6])
是一个集合的子表达式,\1
匹配前面匹配到的([1-6])
,所以说H2
只能匹配到H2
注意:不同的正则表达式在实现回溯引用的语法方面往往有着巨大的差异。
提示:回溯引用只能用来引用模式里的子表达式((用(和))括起来的正则表达式片段)。回溯引用通常从1开始计数(\1、\2等)。在许多实现里,第0个匹配(\0)可以用来代表整个正则表达式。其实子表达式是通过它们的相对位置来引用的:\1对应着第1个子表达式,\5对应着第5个子表达式等等。这种语法是有问题的:如果子表达式的相对位置发生了变化,整个模式也许就不能再完成原来的工作,删除或者添加子表达式的后果可能更为严重。解决办法:是运用命令捕获。 -
8.3、回溯引用在替换操作中的应用(没理解透)
-
8.4.大小写转换
替换使用:$\\U$2\E$3
let str: String = "<BODY><H1>Welcom to my Homepage</H1>\nContent is divided into two sections:<BR>\n <H2>ColodFusion</H2>\nInformation about Macromedia ColodFusion." label.attributedText = textRegex(pattern: "(<[Hh]1>)(.*?)(</[Hh]1>)" ,str: str, font: 22)
分析:
"(<[Hh]1>)(.*?)(</[Hh]1>)"
是三个子表达式就是为了使用回溯引用,说一下替换部分:$1
包含着开始标签,U$2\E
把第二个子表达式(b标题文字)转换为大写,$3
包含着结束标签。 -
8.5、总结
子表达式用来定义字符或表达式的集合。除了可以用在重复匹配操作中意外,子表达式还可以在模式的内部被引用。这种引用被称为回溯引用。回溯引用的语法在不同的正则表达式实现里有很大的差异。回溯引用在文本匹配和文本替换操作里非常有用。
九、前后查找
-
9.1、向前查找
向前查找指定了一个必须匹配但不在结果中返回的模式。向前查找实际就是一个子表达式,而且从格式上看也确实如此。从语法上看,一个向前查找模式其实就是一个以?=
开头的子表达式,需要匹配的文本跟在=
后面
在向前查找里,被匹配的文本包含在最终返回的匹配结果里,被称为“不消费”。反之为消费。xiang
注意:向前查找和向后查找匹配本身其实是有返回结果的,只是这个结果的字节长度永远是0而已。因此,前后查找操作有时也被称为零宽度匹配操作。
其实,任何一个子表达式都可以转换为一个向前查找表达式,只要给它加上一个?=
前缀即可。在同一个搜索模式里可以使用多个向前查找表达式,它们可以出现在模式里的任意位置(而不仅仅是出现在整个模式的开头,就像你们在上面看到的那样。) -
9.2、向后查找
?=
被称为向前查找的操作符,许多正则表达式还支持向后查找,也就是查找出现在被匹配文本之前的字符(但不消费它也就是不包含其后的文本),向后查找操作符是?<=
。
?<=
与?=
使用方法大同小异,它必须在一个子表达式里,而且后跟要匹配的文本。
let str: String = "ABC01: $23.45\nHGG42: $5.31\nCFMX1: $899.00\nXTC99: $69.96\nTotal items founf : 4" label.attributedText = textRegex(pattern: "(?<=\\$)[0-9.]+" ,str: str, font: 22)
我们想要的是以
$
为基础匹配后面的消费金额,显然最后一种匹配最好。
提示:向前查找模式长度是可变的,它们可以包含.和+之类的元字符,所以非常灵活,而向后查找的模式只能是固定长度,这是一条几乎所有的正则表达式实现都遵循的限制。 -
9.3.把向前查找和向后查找结合起来
let str: String = "<HEAD>\n<TITLE>Ben Forta's Homepage</TITLE>\n<HEAD>" label.attributedText = textRegex(pattern: "(?<=<[tT][iI][tT][lL][eE]>).*(?=</[tT][iI][tT][lL][eE]>)" ,str: str, font: 18)
说明:眼睛尖锐的大家可能已经看到了,上面我读
<
进行了转义,再强调一下,向前查找?=
(不消费)和向后查找?<=
(不消费)都是针对的子表达式来操作的。 -
9.4、对前后查找取非
一般来说,凡是支持正向前查找的正则表达式也是支持负向前查找的,反之,凡是只是正向后查找的,也是支持负向后查找的。
上面大家可能看到了
\b
,单词的边界,去\b
会有问题的,有兴趣的可以自己验证下。 -
9.5、总结
有了前后查找,我们就可以对最终的匹配结果包含哪些内容做出更精确的控制。前后查找操作使我们可以利用子表达式来指定文本操作的发生位置,并收到只匹配不消费的效果。正向前查找要用
(?=)
来定义,负向前查找要用(?!)
来定义。有些正则表达式实现还支持正向后查找(响应的操作符是(?<=)
)和负向后查找(相应的操作符是(?!<)
)。
十、嵌入条件
-
10.1、为什么要嵌入条件?(看下面的例子)
(123)456-7890
和123-456-7890
都是可以接收的北美电话号码格式,而1234567890
、(123)-456-789
和(123-456-7890)
虽然都包含着数字正确的字符,但是格式不对,看下面的匹配
let str: String = "123-456-7890\n(123)456-7890\n(123)-456-7890\n(123-456-7890\n1234567890\n123 456 7890" label.attributedText = textRegex(pattern: "\\(?\\d{3}\\)?-?\\d{3}-\\d{4}" ,str: str, font: 24)
分析:
\\(?
匹配的是一个可选的左括号, 请注意,这里必须对(
进行转义;\d{3}
匹配前三位数字;\)?
匹配的是一个可选的右括号;-?
匹配的是一个可选的连字符
上面的匹配\\)?-
如果换做[\\)-]?
这样的话)
与-
只能出现一个就能排除第三行,但是无法排除第4行,正确的匹配应该是:只在电话号码里有一个左括号(的时候才去匹配)。更准确地说,应该是如果电话号码里有一个左括号(
,我们的模式必须去匹配)
;如果不是这样,它就必须去匹配-
,总之这种匹配需要条件。 -
10.2、正则表达式里的条件
正则表达式里的条件要用
?
来定义。事实上,你们已经见过几种非常特定的条件了。?
匹配前一个字符或者表达式,如果它存在的话(可有可无)。-
?=
和?<=
匹配前面或后面的文本,如果它存在的话。
嵌入条件语法也是用了?
,这并没有什么让人感到吃惊的地方,因为嵌入条件不外乎以下两种情况。- 根据前一个回溯引用来进行条件处理。
- 根据前后查找来进行条件处理。
-
10.2.1、回溯引用条件
回溯引用条件只在一个前面的表达式搜索去的成功的情况下才允许使用一个表达式。看下面的例子,我们需要把一段文本里<IMG>
标签全部都找出来;不仅仅如此,如果某个<IMG>
标签是一个链接(被括在<A>和</A>标签之间)的话,你还要把整个链接标签匹配出来。
用来定义这种条件的语法是(?(backrefence)truepregex)
,其中?
表明这是一个条件,括号里的backrefence
是一个回溯引用,truepregex
是一个只在backrefence
存在时才会被执行的表达式。let str: String = "<!-- Nav bar -->\n<TD>\n<A HREF=/home><IMG SRC=/imges/home.gif></A>\n<IMG SRC=/images/spacer.gif>\n<A HREF=/search><IMG SRC=/imges/home.gif></A>\n</A>\n<IMG SRC=/images/spacer.gif>\n<A HREF=/help><IMG SRC=/imges/home.gif>\n</TD>" label.attributedText = textRegex(pattern: "(<[Aa]\\s+[^>]+>\\s*)?<[Ii][Mm][Gg]\\s+[^>]+>(?(1)\\s*</[Aa]>)" ,str: str, font: 24)
分析:这个模式不解释是不容易看明白的。
(<[Aa]\\s+[^>]+>\\s*)?
将匹配一个<A>
或<a>
(以及<A>或<a>标签的任意属性),这个标签可有可无(因为这个子表达式的最后有一个?
)接下来,<[Ii][Mm][Gg]\\s+[^>]+>
匹配一个<IMG>
(大小写均可)及其任意属性。(?(1)\\s*</[Aa]>)
是一个回溯引用条件,?(1)
的含义是:如果第一个回溯引用条件(局具体到上面就是<A>标签)存在,则使用\s*</[Aa]>
继续进行匹配(换句话说,只有当前面的<A>标签匹配成功,才继续进行后面的匹配)。如果(1)存在,\s*</[Aa]>
将匹配结束标签</A>
之后出现的任意空白字符。
注意:(1)
检查第一个回溯引用是否存在,在条件里,回溯引用编号(本例中的1)不需要被转义。因此,?(1)
是正确的,?(\\1)
不正确(但是也能用)。
我们刚才使用的模式只在给定的条件得到满足时候才执行一个表达式。条件还可以有否表达式,否则表达式只在给定的回溯引用不存在(也就是条件没有得到满足)时才会执行。用来定义这种条件的语法是(?(backrefence)true-regex|false-regex)
,这个语法接受一个条件和两个将分别在这个条件得到满足和没有得到满足时执行的表达式。(下面就可以解决上面10.1电话号码的问题)let str: String = "123-456-7890\n(123)456-7890\n(123)-456-7890\n(123-456-7890\n1234567890\n123 456 7890" label.attributedText = textRegex(pattern: "(\\()?\\d{3}(?(1)\\)|-)\\d{3}-\\d{4}" ,str: str, font: 24)
分析:从结果上看,这个模式解决了问题,但它是如何解决问题呢?和前面一样,
(\\()?
也匹配一个可选的左括号,但我们这次把它用括号括起来得到一个子表达式。随后的\d{3}
匹配一位数字的区号。(?(1)\\)|-)
是一个回溯引用条件,它将根据条件是否得到满足而去匹配)
或-
:如果(1)
存在(也就是找到了一个左括号),\\)
必须被匹配;否则,-
必须被匹配。这样一来,只有配对出现的括号才会被匹配;如果没有使用括号或括号不配对,电话号码中的区域和其余数字之间的-
必须被匹配。 -
10.2.2、前后查找条件
前后查找条件只在一个向前查找或向后查找操作取得成功的情况下才允许一个表达式被使用。定义一个前后查找条件的语法与定义一个回溯引用的条件的语法大同小异,只需要把回溯引用(括号里的回溯引用标号)替换为一个完整的前后查找表达式就行了。
-
例子一美国邮政编码匹配
let str: String = "11111\n22222\n33333-\n44444-4444" label.attributedText = textRegex(pattern: "\\d{5}(-\\d{4})?" ,str: str, font: 24)
上面例子的更正:
let str: String = "11111\n22222\n33333-\n44444-4444" label.attributedText = textRegex(pattern: "\\d{5}(?(?=-)-\\d{4})" ,str: str, font: 24)
分析:
\d{5}
匹配前五位数字,接下来是一个(?(?=-)-\\d{4})
形式的向前查找条件。这个条件使用了?=-
来匹配(但不消费)一个连字符,如果条件得到满足(那个连字符存在),-\\d{4}
将匹配那个连字符和随后的4位数字。这样一来,33333-
将被排除在最终的匹配结果之外(它有一个连字符,所以满足给定的条件,但那个连字符后面没有必须出现在那里的4位数字)。平时工作中嵌入查找的模式相当少见,这是因为我们往往可以用最简单的办法来达到同样的目的。
-
-
10.3、总结
在正则表达式里面可以嵌入条件,只有相当条件得到(或者没有得到)满足时,相应的表达式才会被执行。这种条件可以是一个回溯引用(含义是检查该回溯引用是否存在),也可以是一个前后查找的操作。
十一、元字符表
-
11.1、基本的元字符
-
.
匹配任意单个字符 -
|
逻辑或操作符 -
[]
匹配字符集合中的一个字符 -
[^]
对集合求非 -
-
定义一个区间,如[a-z]
-
\
对下一个字符转义
-
-
11.2、数量元字符
-
*
匹配前一个字符(子表达式)零次或者多次 -
*?
是*
的懒惰型版本 -
+
匹配前一个字符(子表达式)的一次或多次重复 -
+?
是+
的懒惰型版本 -
?
前一个字符可以可无,也就是最多匹配一次 -
{n}
匹配前一个字符或者表达式n次 -
{m,n}
匹配前一个字符或表达式最少m次,最多n次 -
{m,}
匹配前一个字符至少m次 -
{m,}?
{m,}的懒惰型版本
-
-
11.3、位置元字符
-
^
匹配字符串的开头 -
\A
匹配字符串的开头 -
$
匹配字符串的结束 -
\z
匹配字符串的结束 -
\<
单词匹配的开始 -
\>
单词匹配的结束 -
\b
单词匹配的边界(开头和结束) -
\B
是\b
的反义
-
-
11.4、特殊字符元字符
-
[\b]
退格字符 -
\c
匹配一个控制字符 -
\d
匹配任意数字字符 -
\D
匹配任意非数字字符 -
\f
换页符 -
\n
换行符 -
\r
回车符 -
\s
匹配一个空白字符 -
\S
匹配一个非空白字符 -
\t
制表符(Tab字符) -
\v
垂直制表符 -
\w
匹配任意数字、字母、下划线以及中文汉字 -
\W
匹配任意非数字、非字母、非下划线以及非中文汉字 -
\x
匹配一个十六进制数字 -
\0
匹配一个八进制数字
-
-
11.5、回溯引用和前后查找
-
()
定义一个子表达式 -
\1
匹配第一个子表达式\2
代表匹配第二个子表达式 -
?=
向前查找 -
?<=
向后查找 -
?!
负向前查找 -
?<!
负向后查找 -
?()
条件 (if then) -
?()|
条件(if then else)
-
11.6、大小写转换
\E
结束\L或\U转换\l
把下一个字符转换为小写\L
把后面的字符转换为小写直到遇见\E为止\u
把下一个字符转换为大写\U
把后面的字符转换为大写直到遇见\E为止-
11.7、匹配模式
-
(?m)
分行匹配模式
-
十二、多规则匹配
-
主要阐述多规则匹配的用法
let pattern = pattern1 + "|" + pattern2 + "|" + pattern3
看下面let str = "@joanking:【周杰伦的歌曲】#大眼睛#小猫咪这么尖叫[偷笑]、@老北: 蝉叫、狼这么尖叫[吃惊]、@乐不思蜀:达芬奇#烧饼#妙的笑到最后[挖鼻屎]!~ http://www.baidu.com" do{ // 1.创建规则 let pattern1 = "\\[.*?\\]" let pattern2 = "@.*?:" let pattern3 = "#.*?#" // 多个规则之间使用 | 符号连接 let pattern = pattern1 + "|" + pattern2 + "|" + pattern3 // 2.创建正则表达式对象 let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive) // 3.开始匹配 let res = regex.matches(in: str, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, str.count)) // 4取出结果 for checkingRes in res { // print(checkingRes.range) print((str as NSString).substring(with: checkingRes.range)) } }catch { print(error) }
十三、正则练习的demo(有心的人可以看看,可以一起讨论一下回溯引用方面的知识,自己理解的不是很好)
- 测试用的JKRegexdemo
记得给个喜欢❤️