正则表达式简单、强大,它可以极大地提高我们工作中的文本处理效率。现在,各大操作系统、编程语言、文本编辑器都已经支持正则表达式
正则其实就是一种描述文本内容组成规律的表示方式。正则表达式真正重要的是字符组、多选结构、量词等等这些概念
正则常常用来简化文本处理的逻辑。在 Linux 命令中,它也可以帮助我们轻松地查找或编辑文件的内容,甚至实现整个文件夹中所有文件的内容替换,比如 grep、
egrep、sed、awk、vim 等。另外,在各种文本编辑器中,比如 Atom,Sublime Text 或VS Code 等,在查找或替换的时候也会使用到它。
正则常见的三种功能:
1> 校验数据的有效性(如校验手机号 邮箱)、
2> 查找符合要求的文本内容(查找符合某规则的号码)
3> 对文本进行切割和替换(比如用连续的空白符切割)等操作。比如 Word、Excel 中查找替换
正则表达式的基本单元——元字符, 元字符是指具有特殊意义的专用字符,元字符是构成正则表达式的基本元件。正则就是由一系列的元字符组成的
例中提到的 \d 和 {11}。
元字符的分类与记忆技巧
正则表达式中有很多的“元字符”,比如刚刚提到的 \d,它在正则中不代表 \(反斜杠)
加字母 d,而是代表任意数字,这种表示特殊含义的字符表示,就是元字符。
元字符大致分成这几类:表示单个特殊字符的,表示空白符的,表示某个范
围的,表示次数的量词,另外还有表示断言的,我们可以把它理解成边界限定
1. 特殊单字符
比如英文的点 . 表示换行以外的任意单个字符,\d 表示任意单个数字,\w 表示任意单个数字或字母或下划线,\s 表示任意单个
空白符。另外,还有与之对应的三个 \D、\W 和 \S,分别表示着和原来相反的意思。
https://regex101.com/r/PnzZ4k/1 \d的使用
https://regex101.com/r/PnzZ4k/2 \w的使用
1> \d 正则匹配所有的数字
12345
67890
abcde
fghij
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main {
public static void main(String[] args) {
//+符号表示匹配前面的字符一次或多次
//^和$是正则表达式中的两个特殊符号,分别表示字符串的开头和结尾。在匹配时,如果不加这两个符号,那么就有可能会出现匹配到部分符合要求的字符串的情况。
String regex = "^[0-9]+$";
String input = "123"; // 假设输入为 "123"
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
System.out.printf("%s 匹配结果: %b%n", input, true);
} else {
System.out.printf("%s 匹配结果: %b%n", input, false);
}
}
}
2> \d{11} 表示单个数字出现 11 次,即 11 位数字
在后面再加上量词,就可以表示单个的数字出现了几次。如果文本中只有姓名和手机号,我们就可以利用这个查找出文本中的手机号了。
张三 11380013800
李四 13500138000
王五 15900138000
2. 空白符
除了特殊单字符外,你在处理文本的时候肯定还会遇到空格、换行等空白符。如换行符 \n,TAB 制表符 \t 等。
不同的系统在每行文本结束位置默认的“换行”会有区别。比如在 Windows 里是 \r\n,在 Linux 和 MacOS 中是 \n。
在正则中,也是类似于 \n 或 \r 等方式来表示空白符号。平时使用正则,
大部分场景使用 \s 就可以满足需求,\s 代表任意单个空白符号。
\r 回车符
\n 换行符
\f 换页符
\t 制表符
\v 垂直制表符
\s 任意空白符
3. 量词
需要匹配单个字符,或者某个部分“重复 N次”“至少出现一次”“最多出现三次”等等这样的字符,
这就需要用到表示量词的元字符了。英文的星号(*)代表出现 0 到多次,加号(+)代表 1 到多次,问号(?)代
表 0 到 1 次,{m,n}代表 m 到 n 次。
比如,在文本中“颜色”这个单词,可能是带有u的colour,也可能是不带u的color,
我们使用 colou?r 就可以表示两种情况了。 todo 如果两个以上的单词呢? --括号括起来
*: 0到多次
+: 1到多次
?: 0到1次,如colou?r
{m}: 出现m次, 出现一次可以省略
{m,}: 出现至少m次
{m,n}:m到n次
4. 范围
学习了量词,我们就可以用 \d{11} 去匹配所有手机号,但同时也要明白,这个范围比较
大,有一些不是手机号的数字也会被匹配上,比如 11 个 0,那么我们就需要在一个特殊的
范围里找符合要求的数字。
再比如,我们要找出所有元音字母 aeiou 的个数,这又要如何实现呢?在正则表达式中,
表示范围的元字符可以轻松帮我们搞定这样的问题。
|或,如 ab|bc 代表ab或bc
[...]多选一,括号中任意单个元素
[a-z]匹配a到z之同任意单介元素(投ASCII表,包含a,z)
[^…]取反,不能是括号中的任意单个元素
eg: 比如某个资源可能以 http:// 开头,或者 https:// 开头,也可能以 ftp:// 开头,那么资源的
协议部分,我们可以使用 (https?|ftp):\/\/ 来表示。 https://regex101.com/r/PnzZ4k/5
^
和$
是正则表达式中的两个特殊符号,分别表示字符串的开头和结尾。在匹配时,如果不加这两个符号,那么就有可能会出现匹配到部分符合要求的字符串的情况。
eg: 第 1 位固定为数字 1;
第 2 位可能是 3,4,5,6,7,8,9;
第 3 位到第 11 位我们认为可能是 0-9 任意数字。
^1[3-9]{1}\d{9}$ = ^1[3-9]\d{9}$ 匹配13888888888
^ | 匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与"\n"或"\r"之后的位置匹配。 | |
$ | 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。 | |
\b | 匹配单词的开始或结束 | |
^ 和 $ | 在单行模式下匹配整段输入,匹配字符串的开始/结束, 同 \A 和 \z,在多行模式下匹配行,可以分辨终止子 | |
\A 和 \Z | 匹配的是整段输入,结尾终止子可有可无,不管在单行模式还是多行模式下 | |
\A 和 \z | 匹配的是整段输入,完完整整,不偏不倚,不管在单行模式还是多行模式下 |
元字符 | 同义表示方法 | 示例 |
* | {0,} | ab* 可以匹配 a或 abbb |
+ | {1,} | 正则ab+ 可以匹配ab或 abbb 但不能匹配a |
? | {0,1} | 正则 (\+86-)?\d{11} 可以匹配 +86-13800138000 或 13800138000 ()包括一个整体, \+ 使用转义字符表示+ |
贪婪模式/非贪婪模式
在正则中,表示次数的量词默认是贪婪模式,模式会尝试尽可能最大长度去匹配。
非贪婪匹配(Lazy)
在量词后面加上英文的问号 (?),正则就变成了 a*?。找到长度最小且满足条件的
独占模式(Possessive)
不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。但是在一些场景
下,我们不需要回溯,匹配不上返回失败就好了,它类似贪婪匹配,但匹配过程不会发生回溯,因此在一些场合下性能会更好。独占模式和贪婪模式很像,独占模式会尽可能多地去匹配,如果匹配失败就结束,不会进行回溯,这样的话就比较节省时间。在量词后面加上加号(+)。
独占模式性能比较好,可以节约匹配的时间和 CPU 资源,但有些情况下并不能满足需求,要想使用这个模式还要看具体需求(比如我们接下来要讲的案例),另外还得看你当前使用的语言或库的支持程度。独占模式“不吐出已匹配字符”的特性,会使得一些场景不能使用它。另外,只有少数编程语言支持独占模式。
^代表以这个正则开头,$代表以正则结尾。
括号在正则中的功能就是用于分组。可以用括号括起来表示一个整体, 被括号括起来的部分“子表达式”会被保存成一个子组, 可以在括号里面使用 ?: 不保存子组。不保存子组可以提高正则的性能和子组计数时也更不容易出错。可以使用 “反斜扛 + 编号”,即 \number 的方式来进行引(JavaScript 中是通过$编号)。
正则 | 示例 | |
保存正则 | (正则) | \d{15}(\d{3})? 表示后面三位有或无, 可以匹配15/18位 |
不保存正则 | (?:正则) | \d{15}(?:\d{3})? |
数左括号(开括号)是第几个,就可以确定是第几个子组。命名分组的格式为(?P<分组名>
正则), 不过命名分组并不是所有语言都支持的
分组引用在查找中使用:
前面出现的单词再次出现, 以使用 \w+ 来表示一个单词,
sublime替换: (\w+) \1, 输入子组的引用 \1 只能匹配出现两次的, 三次以上的替换不了
内容: the little cat cat is in the hat hat,we like it.
里面有一些单词连续出现了多次,我们认为连续出现多次的单词应该是一次,比如:
the little cat cat is in the hat hat hat, we like it.
其中 cat 和 hat 连接出现多次,要求处理后结果是
the little cat is in the hat, we like it.
正则:(\w+)(\s\1)+ (\s\1)+ ------ \s任意空白字符, \1表示组引用 +表示1次或多次
替换:\1
正则中常见的四种匹配模式分: 不区分大小写、点号通配模式、多行模式和注释模式。
使用说明: 模式修饰符是通过 (? 模式标识)正则
不区分大小写的 cat 就可以写成 (?i)cat == [Cc][Aa][Tt]
尝试匹配两个连续出现的 cat ,即便是第一个cat 和第二个 cat 大小写不一致 (?i)(cat) \1
第一个和第二个区分大小写, 要用括号把修饰符和正则 cat 部分括起来,加括号相当于作用范围的限定,让不区分大小写只作用于这个括号里的内容。 ((?i)cat) \1
匹配真正的“任意”符号的时候,可以使用 [\s\S] 或 [\d\D] 或 [\w\W] 等。
多行匹配模式。通常情况下,^ 匹配整个字符串的开头,匹配整个字符串的结尾。 的匹配行为。
多行匹配模式改变的就是和的匹配行为。
正则中还有 \A 和 \z(Python 中是 \Z) 这两个元字符容易混淆,\A 仅匹
配整个字符串的开始,\z 仅匹配整个字符串的结束,在多行匹配模式下,它们的匹配行为
不会改变,如果只想匹配整个字符串,而不是匹配每一行,用这个更严谨一些。
正则中注释模式是使用(?#comment) 来表示。 (\w+)(?#word) \1(?#word repeat again)
什么是断言呢?简单来说,断言是指对匹配到的文本位置有要求。比如查找 tom,但其他单词tomorrow 中也包含了 tom。正则中提供了一些结构叫做断言,只用于匹配位置,而不是文本内容本身。常见的断言有三种:单词边界、行的开始或结束以及环视。
1> 单词边界(Word Boundary) \b\w+\b
2> 在默认的单行模式(也称为单行模式)下,^
匹配字符串的开头,$
匹配字符串的结尾。在多行模式下,^ 和 $ 符号可以匹配每一行的开头或结尾。大部分实现默认不是多行匹配模式,但也有例外,比如 Ruby 中默认是多行模式。对于校验输入数据来说,一种更严谨的做法是,使用 \A 和 \z (Python 中使用\Z)来匹配整个文本的开头或结尾。(由于这种行为可能会导致预期之外的匹配结果,因此在正则表达式中最好不要使用 ^
和 $
,尤其是进行多行匹配时。)
3> 环视就是要求匹配部分的前面或后面要满足(或不满足)某种规则
邮政编码的规则是第一位是 1-9,一共有 6 位数字组成。除了文本本身组成符合这 6 位数的规则外,这 6 位数左边或右边都不能是数字。正确: (?<!\d)[1-9]\d{5}(?!\d)
在Python 中,可以在正则前面加上小写字母 r 来表示使用原生字符串。
不使用 Unicode 编码时,正则会被编译成其它编码表示形
式。比如,在 macOS 或 Linux 下,一般会编码成 UTF-8,而在 Windows 下一般会编码
成 GBK。
Java 中目前还没有原生字符串,正则需要经过字符串转义和正则转义两个步骤,因此在用到反斜扛的地方,比如表示数字的\d,就得在字符串中表示成\\d,转义会让书写正则变得稍微麻烦一些,在使用的时候需要留意一下。 todo /A /z的作用
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class Main {
public static void main(String[] args) {
//方法1,可以不加 \A 和 \z
System.out.println(Pattern.matches("\\d{4}-\\d{2}-\\d{2}", "2020-06-01"));
//方法2,可以不加 \A 和 \z
System.out.println("2020-06-01".matches("\\d{4}-\\d{2}-\\d{2}")); // true
//方法3,必须加上 \A 和 \z
Pattern pattern = Pattern.compile("\\A\\d{4}-\\d{2}-\\d{2}\\z");
System.out.println(pattern.matcher("2020-06-01").find()); // true
//正则切割, \\W 任意非字母下划线
Pattern pattern2 = Pattern.compile("\\W+");
for(String s : pattern2.split("apple, pear! orange; tea")) {
System.out.println(s);
}
}
}