五分钟重拾正则表达式

什么是正则表达式

正则表达式(Regular Expression,常简写为Regex)是一种表示文本规则的代码。在编写处理字符串的程序时,经常会有查找、替换符合某些规则的字符串的需要,正则表达式就是用于描述这些规则的工具。

大多数人都在电脑上使用过用于文件查找的通配符,例如用“*.png”来查找所有的PNG格式的文件。正则表达式和通配符类似,也是用来进行文本匹配的工具。只是比起通配符,它能进行更精确的匹配,同时,也更为复杂。

正则表达式事实上是一种轻量级、简洁的编程语言,几乎所有的高级编程语言都支持正则表达式(语法不一定完全相同)。此外,大部分的代码编辑器,如 Sublime、VS Code 也都支持正则表达式的查找替换。因此,在学习正则表达式的时候,可以在 Sublime 之类的编辑器中进行尝试。

注:文件通配符与正则表达式无关。

基础语法

字符

正则表达式的语法中有普通字符和一些被称为“元字符”的特殊字符。

包括所有字母和数字字符在内的大部分字符,都是普通字符。普通字符只能匹配它们本身,如正则表达式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
只能匹配 ios 这个字符串(区分大小写)。
下表是元字符及其行为的一个完整列表(转自维基百科。不是唯一的,不同的解析引擎可能略有不同)。
| 字符 | 描述 |
| --- | --- |
| \ | 将下一个字符标记为一个特殊字符(File Format Escape)、或一个原义字符(Identity Escape)、或一个向后引用(backreferences)、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。序列“\\”匹配“\”而“\(”则匹配“(”。 |
| \^ | 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,\^也匹配“\n”或“\r”之后的位置。 |
| $ | 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。 |
| * | 匹配前面的子表达式零次或多次。例如,zo*能匹配“z”、“zo”以及“zoo”。*等价于{0,}。 |
| + | 匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。 |
| ? | 匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“do”或“does”中的“do”。?等价于{0,1}。 |
| {n} | n是一个非负整数。匹配确定的n次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。 |
| {n,} | n是一个非负整数。至少匹配n次。例如,“o{2,}”不能匹配“Bob”中的“o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。 |
| {n,m} | m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。例如,“o{1,3}”将匹配“fooooood”中的前三个o。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。 |
| ? | 非贪心量化(Non-greedy quantifiers):当该字符紧跟在任何一个其他重复修饰符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串“oooo”,“o+?”将匹配单个“o”,而“o+”将匹配所有“o”。 |
| . | 匹配除“\n”之外的任何单个字符。要匹配包括“\n”在内的任何字符,请使用像“(.|\n)”的模式。 |
| (pattern) | 匹配pattern并获取这一匹配的子字符串。该子字符串用于向后引用。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符,请使用“\(”或“\)”。 |
| (?:pattern) | 匹配pattern但不获取匹配的子字符串(shy groups),也就是说这是一个非获取匹配,不存储匹配的子字符串用于向后引用。这在使用或字符“(|)”来组合一个模式的各个部分是很有用。例如“industr(?:y|ies)”就是一个比“industry|industries”更简略的表达式。 |
| (?=pattern) | 正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“Windows(?=95|98|NT|2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 |
| (?!pattern) | 正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“Windows(?!95|98|NT|2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始 |
| (?<=pattern) | 反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,“(?<=95|98|NT|2000)Windows”能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。 |
| (?<!pattern) | 反向否定预查,与正向否定预查类似,只是方向相反。例如“(?<!95|98|NT|2000)Windows”能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”。 |
| x|y | 匹配x或y。例如,“z|food”能匹配“z”或“food”。“(?:z|f)ood”则匹配“zood”或“food”。 |
| [xyz] | 字符集合(character class)。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”。特殊字符仅有反斜线\保持特殊含义,用于转义字符。其它特殊字符如星号、加号、各种括号等均作为普通字符。脱字符\^如果出现在首位则表示负值字符集合;如果出现在字符串中间就仅作为普通字符。连字符 - 如果出现在字符串中间表示字符范围描述;如果如果出现在首位则仅作为普通字符。 |
| [\^xyz] | 排除型字符集合(negated character classes)。匹配未列出的任意字符。例如,“[\^abc]”可以匹配“plain”中的“plin”。 |
| [a-z] | 字符范围。匹配指定范围内的任意字符。例如,“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。 |
| [\^a-z] | 排除型的字符范围。匹配任何不在指定范围内的任意字符。例如,“[\^a-z]”可以匹配任何不在“a”到“z”范围内的任意字符。 |
| [:name:] | 增加命名字符类(named character class)[注 1]中的字符到表达式。只能用于方括号表达式。 |
| [=elt=] | 增加当前locale下排序(collate)等价于字符“elt”的元素。例如,[=a=]可能会增加ä、á、à、ă、ắ、ằ、ẵ、ẳ、â、ấ、ầ、ẫ、ẩ、ǎ、å、ǻ、ä、ǟ、ã、ȧ、ǡ、ą、ā、ả、ȁ、ȃ、ạ、ặ、ậ、ḁ、ⱥ、ᶏ、ɐ、ɑ。只能用于方括号表达式。 |
| [.elt.] | 增加排序元素(collation element)elt到表达式中。这是因为某些排序元素由多个字符组成。例如,29个字母表的西班牙语,"CH"作为单个字母排在字母C之后,因此会产生如此排序“cinco, credo, chispa”。只能用于方括号表达式。 |
| \b | 匹配一个单词边界,也就是指单词和空格间的位置。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。 |
| \B | 匹配非单词边界。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。 |
| \cx | 匹配由x指明的控制字符。例如,\cM匹配一个Control-M或回车符。x的值必须为A-Z或a-z之一。否则,将c视为一个原义的“c”字符。 |
| \d | 匹配一个数字字符。等价于[0-9]。 |
| \D | 匹配一个非数字字符。等价于[\^0-9]。 |
| \f | 匹配一个换页符。等价于\x0c和\cL。 |
| \n | 匹配一个换行符。等价于\x0a和\cJ。 |
| \r | 匹配一个回车符。等价于\x0d和\cM。 |
| \s | 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。 |
| \S | 匹配任何非空白字符。等价于[\^ \f\n\r\t\v]。 |
| \t | 匹配一个制表符。等价于\x09和\cI。 |
| \v | 匹配一个垂直制表符。等价于\x0b和\cK。 |
| \w | 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。 |
| \W | 匹配任何非单词字符。等价于“[\^A-Za-z0-9_]”。 |
| \ck | 匹配控制转义字符。k代表一个字符。等价于“Ctrl-k”。用于ECMA语法。 |
| \xnn | 十六进制转义字符序列。匹配两个十六进制数字nn表示的字符。例如,“\x41”匹配“A”。“\x041”则等价于“\x04&1”。正则表达式中可以使用ASCII编码。. |
| \num | 向后引用(back-reference)一个子字符串(substring),该子字符串与正则表达式的第num个用括号围起来的捕捉群(capture group)子表达式(subexpression)匹配。其中num是从1开始的十进制正整数,其上限可能是9[注 2]、31、[注 3]99甚至无限。[注 4]例如:“(.)\1”匹配两个连续的相同字符。 |
| \n | 标识一个八进制转义值或一个向后引用。如果\n之前至少n个获取的子表达式,则n为向后引用。否则,如果n为八进制数字(0-7),则n为一个八进制转义值。 |
| \nm | 3位八进制数字,标识一个八进制转义值或一个向后引用。如果\nm之前至少有nm个获得子表达式,则nm为向后引用。如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用。如果前面的条件都不满足,若n和m均为八进制数字(0-7),则\nm将匹配八进制转义值nm。 |
| \nml | 如果n为八进制数字(0-3),且m和l均为八进制数字(0-7),则匹配八进制转义值nml。 |
| \un | Unicode转义字符序列。其中n是一个用四个十六进制数字表示的Unicode字符。例如,\u00A9匹配版权符号(©)。 |
### 转义
当需要匹配元字符本身的时候,为元字符加上“ \ ”转义即可。
### 匹配字符集合
方括号“ [ ] ”用于表示要匹配的字符所属的字符集合。可以将所有可能匹配到的字符枚举出来,如:
```[ios789]

可以匹配到 i 或者 o 或者 s 或者 7 或者 8 或者 9 。

也可以根据 ASCII码的顺序,将某个范围内的字符都包括在内,如上述的正则表达式等价于:

1
2
3
同时也能够在其中使用元字符,如:
```[\w,\n]

可以匹配一个单词字符,或者一个逗号,或者一个换行符。

上述三个例子中,一次都只能匹配一个字符,如果要匹配多个字符,可以后接数量的修饰,如:

1
2
表示匹配两个或者三个字母;
```[a-zA-Z]+

表示匹配至少一个字母;

此外,也可以用

1
```[^0-9]

表示匹配一个数字以外的所有字符。

匹配定位点

定位点能够将正则表达式固定到一行或整个字符串的起始位置或结尾。它们还能够创建匹配一个单词的开头、结尾或内部字符的表达式。

需要注意的是,定位点匹配到的并不是一个实际的字符,而只是一个位置。

例如,在表达式

匹配单词边界。 该表达式与 “never” 中的 “er” 匹配,但与 “verb” 中的 “er” 不匹配。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
下表包含正则表达式定位点以及它们的含义:
| 字符 | 说明 |
| --- | --- |
| ^ | 匹配输入字符串开始的位置。 如果标志中包括 m(多行搜索)字符,^ 还将匹配 \n 或 \r 后面的位置。 |
| $ | 匹配输入字符串结尾的位置。 如果标志中包括 m(多行搜索)字符,$ 还将匹配 \n 或 \r 前面的位置。 |
| \b | 匹配一个字边界,即字与空格间的位置。 |
| \B | 非字边界匹配。|
### 子串的捕获
使用括号可以捕获其中的子串,并将其保存作为变量,以用于后续的匹配或替换。假设正则表达式是一个小型计算机程序,那么捕获子串就是它输出的一部分。
在实际使用中,可能会捕获很多子串,被捕获的子串从左向右编号,也就是只需要对左括号计数。引用时使用```\x```的格式,子串编号从```\1```开始,```\0```表示原字符串本身。
假设正则表达式如下:

(\w+) had a ((\w+) \w+)

1
2
那么对于字符串:

I had a nice day

1
2
该正则表达式捕获到的子串如下:

\0: I had a nice day
\1: I
\2: nice
\3: nice day

1
2
3
4
5
6
### 或
正则表达式中允许对多个匹配选项之间进行分组,相当于“或”的作用。
如正则表达式:
```(Chapter|Section) [1-9][0-9]{0,1}

在匹配字符串:

3 Section 90```
1
时,可以匹配到 ```Chapter``` 和 ```Section

Objective-C 中的正则表达式

Objective-C 中有专门的一个正则表达式类 —— NSRegularExpression,使用较为方便。此外在 NSPredicate 中也支持用正则表达式进行查询。OC 头文件中的注释对它们的具体用法作了详细的介绍,本文就再赘述了。

值得注意的是,OC 中的正则表达式中,匹配到的子串是用

1
2
3
4
5
6
## 示例
### 将表格替换为 Markdown 格式
上文中从维基百科上拷贝下来的表格:

字符 描述
\ 将下一个字符标记为一个特殊字符(File Format Escape)、或一个原义字符(Identity Escape)、或一个向后引用(backreferences)、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。序列“\”匹配“\”而“(”则匹配“(”。
^ 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。
$ 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。

  • 匹配前面的子表达式零次或多次。例如,zo能匹配“z”、“zo”以及“zoo”。等价于{0,}。
  • 匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。
    1
    2
    Markdown 格式的表格:
字符 描述
\ 将下一个字符标记为一个特殊字符(File Format Escape)、或一个原义字符(Identity Escape)、或一个向后引用(backreferences)、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。序列“\”匹配“\”而“(”则匹配“(”。
\^ 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,\^也匹配“\n”或“\r”之后的位置。
$ 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。
* 匹配前面的子表达式零次或多次。例如,zo能匹配“z”、“zo”以及“zoo”。等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。
1
2
3
匹配的正则表达式:
``` ^(.*?)\t(.*)$

替换的正则表达式:

\1 | \2 | ```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
### 美拍滤镜 plist 格式替换
原格式:
```xml
<dict>
<key>id</key>
<integer>3002</integer>
<key>inputSource</key>
<array>
<dict>
<key>index</key>
<integer>1</integer>
<key>source</key>
<string>3002/gleam.png</string>
</dict>
</array>
<key>name</key>
<string>晨露</string>
<key>nameEN</key>
<string>Shimmer</string>
<key>nameTW</key>
<string>微光</string>
<key>percent</key>
<real>0.7</real>
<key>shaderType</key>
<integer>1</integer>
<key>statisticsId</key>
<string>fli3002</string>
<key>thumb</key>
<string>gleam2.png</string>
</dict>

目标格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dict>
<key>ColorFilter</key>
<integer>58</integer>
<key>ColorFilterConfigPath</key>
<string>preFilters/3030/filterConfig.plist</string>
<key>Icon</key>
<string>perfume2.png</string>
<key>MVID</key>
<string>fli3030</string>
<key>Title</key>
<string>嘉年华</string>
<key>TitleTranslation</key>
<dict>
<key>zh-Hant</key>
<string>嘉年華</string>
<key>en</key>
<string>Carnival</string>
</dict>
</dict>

查找的正则表达式:

1
<dict>[\w|\W]*?<key>name</key>\W+<string>([\w|\W]*?)</string>\W+<key>nameEN</key>\W+<string>([\w|\W]*?)</string>\W+<key>nameTW</key>\W+<string>([\w|\W]*?)</string>[\w|\W]*?<key>statisticsId</key>\W+<string>([a-zA-Z]+([0-9]+))</string>\W+<key>thumb</key>\W+<string>([\w|\W]*?)</string>\W+</dict>

替换的正则表达式:

1
<dict>\n<key>ColorFilter</key>\n<integer>58</integer>\n<key>ColorFilterConfigPath</key>\n<string>preFilters/\5/filterConfig.plist</string>\n<key>Icon</key>\n<string>\6</string>\n<key>MVID</key>\n<string>\4</string>\n<key>Title</key>\n<string>\1</string>\n<key>TitleTranslation</key>\n<dict>\n<key>zh-Hant</key>\n<string>\3</string>\n<key>en</key>\n<string>\2</string>\n</dict>\n</dict>

###歌词信息读取

歌词文件的样式(尖括号中为时间戳,后接一段歌词):

1
<0,626>killing <627,626>spring<627,626>kill ing<627,626>我是一段 歌词### <627,626>我是一句歌词~ ~~

用于匹配的正则表达式:

1
<([0-9]+),([0-9]+)>([\w|\W]+?)(?=<|$)

可以将匹配到的子串转换为 JSON 格式:

1
{\n"stamp1": \1,\n"stamp2": \2,\n"content": \3\n},\n