正则表达式入门
1、前言
正则表达式,对大家来说既熟悉又陌生。熟悉是因为工作中有很多场景能用到,比如手机号、邮箱、密码等规则校验。
陌生则是因为正则表达式看上去就是一堆乱码,且一眼看上去很难看懂匹配规则。有时候在网上去找一个特定规则的正则表达式,搜出来的结果各不相同,执行效果更是不尽人意,想自己去修改,感觉也无从下手。
今天就和大家一起来学习正则表达式
2、几个常用规则
1、 完全匹配
正则表达式是用来匹配文本的,现在假设我们要匹配 "abcdefg" 这一字符串中的 "abc" 三个字母,我们该怎么写呢?
abc
对,不要怀疑,就是这么简单。
如果想匹配 "def",那么就是
def
2、 符号 |
那我们再继续往后看。
还是 "abcdefg" 字符串,我们想匹配其中的 "ab" 和 "ef" 两个子串,怎么写呢?按照上面的写法,就是写两个正则 "ab" 和 "ef" 来匹配。这样可以,不过略显麻烦。我们写法如下:
ab|ef
这里我们新加了 "|" 这个字符,表示 或,即匹配 ab 或者 ef,是不是就方便了许多。
我们在 "acabcdefgh acbbcdefgh accbcdefgh" 字符串中,要匹配 "abcdef","bbcdef","cbcdef" 三个子串。
这题非常简单,用上面的方法,正则表达式如下:
abcdef|bbcdef|cbcdef
写法正确,不过略显繁琐。我们的写法如下:
(a|b|c)bcdef
3、 符号 []
上面的例子,我们如果不用 "|",可以实现么?
答案是可以的,写法如下:
ac[abc]bcdef
我们这里引入了一个新的字符 "[]"。
这对中括号是字符集的意思,里面包含的字符,都是或("|")的关系。
我们还可以这样写:
ac[a-c]bcdef
"[a-c]" 表示的是匹配 a-c 之间的任意字符。
[] 除了可以放字符集合以及字符范围以外,还可以做取反操作。
比如 "[^a-c]" 表示的是匹配 a-c 范围以外其他的字符。
"[^0-8]" 表示的是匹配 0-8 范围以外的其他字符。
到了这里,我们已经可以写一些简单的匹配规则了。
4、 限制符号 * + ? {}
我们现在想匹配 类似 "fooooooooooooooooof" 这样子的字符串,中间的 "o" 个数我们不能确定,上面讲到的规则就有点无能为力了。
那我们该怎么写呢?
fo*f
这里的 "*" 表示匹配前面的子表达式零次或多次。
如果想匹配至少有一个 "o" 该怎么写呢?
fo+f
这里的 "+" 表示匹配前面的子表达式一次或多次。
如果想匹配 0 个或者者 1 个 该怎么写呢?
fo?f
这里的 "?" 表示匹配前面的子表达式零次或一次。
如果想匹配确定次数的 "o" 该怎么写呢?
fo{2}f
上面的规则,会匹配 foof,即匹配 2 个 "o"。
所以,"{n}" 表示匹配前面子表达式 n 次。
那如果想匹配 最少 n 个,最多 m 个呢?
"{n,m}" 表示匹配前面子表达式至少 n 次,至多 m 次。
所以 "*" 等价于 "{0,}","+" 等价于 "{1,}","?" 等价于 "{0,1}"。
5、 几个转义字符
在正则表达式里,也有一些转义字符,表示一些特殊的含义。
"\d" 表示匹配数字字符,等价于 "[0-9]"。
"\D" 表示匹配非数字字符,等价于 "[^0-9]"。
"\n" 表示匹配换行符。
"\s" 表示匹配任何空白字符,等价于 "[\f\n\r\t\v]"。
"\S" 表示匹配任何非空白字符,等价于 "[^\f\n\r\t\v]"。
6、 符号 .
符号 "." 会匹配 "\n" 以外的所有字符,也是比较常用的。
7、 符号 ?
不过使用正则表达式时,限制字符 "* + ? {}" 默认的匹配规则是贪婪的。什么意思呢?
比如说有一个字符串 "abbbbb",我们使用正则表达式 "ab+" 去匹配,匹配的结果是 "abbbbbbb",会尽可能多的去匹配满足的字符。
我们如果想尽可能少的匹配满足的字符,那么就在限定符后面加 "?"。
"ab+?" 匹配的结果就是 ab。
"?" 符号的非贪婪匹配,在某些情况下也是很有用的。
8、 符号 ()
最后我们再来看一个符号 "()",小括号的含义是分组,可以理解为被小括号包含的子表达式就是一个分组,在前面介绍 "|" 的时候我们使用过。
不过分组也分很多类型。
"(pattern)" 小括号单纯的包含了子表达式,表示此分组可以被捕获。捕获的意思就是此分组的内容会被保存下来,后续可以从匹配的集合中获取到。
"(?:pattern)" 小括号中增加了 ?: 前缀,表示此分组不可以被捕获。后续匹配的集合中就不会包含此分组信息。
"(?=pattern)" 小括号中增加了 ?= 前缀,表示的是匹配此分组,但是此分组里的内容不需要被获取。
"(?!pattern)" 小括号中增加了 ?! 前缀,表示的是匹配此分组之外的内容,但是此分组里的内容不需要被获取。
举个栗子:
正则表达式 "Windows(?=95|98|NT|2000)" 可以匹配字符串 "Windows2000" 中的 "Windows",但是不能匹配 "Windows3.1" 中的 Windowds。
正则表达式 "Windows(?!95|98|NT|2000)" 可以匹配字符串 "Windows3.1" 中的 "Windows",但是不能匹配 "Windows2000" 中的 Windowds。
以上就是正则表达式里一些常见的规则解析。
3、达式中的优先级
| 普通拼接 | abc |
|---|---|
| 括号 | (abc) |
| 量词 | a*b |
| 多选结构 | a|b |
正则表达式的优先级
| 优先级 | 组合 | 说明 | |
|---|---|---|---|
| 1 | (regex) | 整个括号内的字表达式称为单个元素 | |
| 2 | * ? + | 限定之前紧邻的元素 | |
| 3 | abc | 普通拼接,元素相继出现 | |
| 4 | a | bc | 多选结构 |
注:数字越小,优先级越高。
4、分组
1、概念
正则表达式可以通过”()”来进行分组,更专业的表达就是捕获组,每个完整的”()”可以分为一组,同时,”()”中还可以嵌套”()”,即组之间还可以存在更小的组,以此类推。而编号为0的组,则是正则表达式匹配到的整体,这个规则只要支持正则表达式中捕获组的语言基本上都适用。
2、分组编号
普通捕获组编号规则
如果没有显式为捕获组命名,即没有使用命名捕获组,那么需要按数字顺序来访问所有捕获组。在只有普通捕获组的情况下,捕获组的编号是按照“(”出现的顺序,从左到右,从1开始进行编号的 。
正则表达式分组举例:(\d{4})-(\d{2}-(\d\d))
3、分组捕获
| 分类 | 代码/语法 | 说明 |
|---|---|---|
| 捕获 | (exp) | 匹配exp,并捕获文本到自动命名的组里 |
(?<name>exp) | 匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name'exp) | |
(?:exp) | 匹配exp,不捕获匹配的文本,也不给此分组分配组号 | |
| 零宽断言 | (?=exp) | 匹配exp前面的位置 |
(?<=exp) | 匹配exp后面的位置 | |
(?!exp) | 匹配后面跟的不是exp的位置 | |
(?<!exp) | 匹配前面不是exp的位置 | |
| 注释 | (?#comment) | 这种类型的分组不对正则表达式的处理产生任何影响,用于提供注释让人阅读 |
4、后向引用
当我们使用小括号指定一个子表达式之后,就要对这个子表达式的文本进行匹配,即此分组捕获的内容,可以在表达式或其它程序中作进一步的处理。一般情况下,每个分组都会自动拥有一个组号,它的规则是:从左到右以分组的左括号作为标志,把第一次出现的分组的组号定为1,第二个即2,以此类推下去。
后向引用用于重复搜索前面某个分组匹配的文本。例如,\1代表分组1匹配的文本。我们根据示例来深刻理解:
\b(\w+)\b\s+\1\b可以用来匹配重复的单词,像go go, 或者kitty kitty。这个表达式首先是一个单词,也就是单词开始处和结束处之间的多于一个的字母或数字(\b(\w+)\b),这个单词会被捕获到编号为1的分组中,然后是1个或几个空白符(\s+),最后是分组1中捕获的内容(也就是前面匹配的那个单词)(\1)。
5、匹配示例分析
1、匹配电子邮箱:
"([a-z0-9_.-]+)@([\da-z.-]+).([a-z.]{2,6})"
也是分段来看。
第一段 "([a-z0-9_.-]+)",表示匹配 a-z 范围内的字母,0-9 范围的数字,以及 "_",".","-" 三个字符,"+" 表示至少有一个字符。
第二段 "@" 表示匹配 "@" 字符。
第三段 "([\da-z.-]+)","\d" 表示匹配任意数字,a-z 范围的字母,".","-" 两个字符,"+" 至少有一个字符。
第四段 "." 表示匹配 "." 字符。
第五段 "([a-z.]{2,6})",表示匹配 a-z 范围的字母,"." 字符,"{2,6}" 表示至少 2 个字符,至多 6 个字符。
6、代码中使用
1、使用java原生api
在 Java 中,关于正则表达式的主要有两个类,Pattern 和 Matcher。
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ReTest {
/**
* 如果pattern是固定的,尽量写成静态的,编译一次,\
不要每次调用都重新编译
*/
static Pattern r = Pattern.compile( "boy ([ 0-9 ]) " ) ;
public static void main(String[] args) {
String str = "grilboy1girlboy2boy3girl";
// 创建 matcher 对象
Matcher m = r.matcher(str);
while (m.find()) {
// 第0个分组代表匹配的整体
System.out.println("match boy: " + m.group(0));
System.out.println("match boy number: " + m.group(1));
}
}
}
java代码里写的正则表达式,如果pattern是固定的,尽量写成静态的,编译一次,不要每次调用都重新编译
上面代码运行的结果如下:
match boy: boy1
match boy number: 1
match boy: boy2
match boy number: 2
match boy: boy3
match boynumber: 3
2、hutool 工具包中的使用
String str = "grilboy1girlboy2boy3girl";
String pattern = "boy([0-9])";
List < String > group0 = ReUtil.findAll( pattern, str, 0 ) ;
List < String > group1 = ReUtil.findAll( pattern, str, 1 ) ;
System.out.println ( group0 ) ;
System.out.println ( group1 ) ;
输出如下:
[boy1, boy2, boy3]
[1, 2, 3]
7、应用小案例
1、字符串提取
1.1 提取fileId
https://download.dameng.com/eco-file-server/file/eco/preview/20210714101131T7ADFZE3BGPQL0K4QB
这个字符串中 fileId 为 20210714101131T7ADFZE3BGPQL0K4QB
String str = "https://download.dameng.com/eco-file-
server/file/eco/preview/20210714101131T7ADFZE3BGPQL0K4QB";
// 添加?是保证如果是http请求,同样可以获取
// (.*)这所以没有改为(.*?)是如果不是贪婪匹配,则只匹配
// https://download.dameng.com/eco-file-server/file/eco/preview/
String reg = "https?://download.dameng.com/eco-file-server/file/eco/preview/(.*)";
System.*out*.println *(* ReUtil.*getGroup1(* reg, str *))* ;
输出如下:
1.2 提取id值
输入字符串 end.jpg
提取 20220210140807XWG90B 值
String str = "<download id=20220210140807XWG90B>end.jpg</download>";
String reg = "<download id=(.*)>(.*)</download>";
// 分组1获取的是 20220210140807XWG90B
System.out.println ( ReUtil.getGroup1( reg, str )) ;
// 分组2 获取的是 end.jpg
System.out.println ( ReUtil.get( reg, str,2 )) ;
输出如下:
2、字符串替换
2.1 原字符串是:中文1234,把1234换成(1234)
String str = "中文1234";
// $1 代表第一个分组匹配上的内容
String s = ReUtil.replaceAll( str, "(\d+)", "($1)" ) ;
System.out.println ( s ) ;
输出如下:
3、notepad++ 中应用
3.1 给所有的字符串添加双引号
原字符串如下:
替换后如下:
3.2 多行合并为一行
原字符串如下:
替换后如下:
3.3 给一行的字符添加单引号
原字符 123,566,789
目标字符 '123','566','789'
7、通用校验总结
1、校验数字的表达式
1 数字:^[0-9]*$
2 n位的数字:^\d{n}$
3 至少n位的数字:^\d{n,}$
4 m-n位的数字:^\d{m,n}$
5 零和非零开头的数字:^(0|[1-9][0-9]*)$
6 非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$
7 带1-2位小数的正数或负数:^(-)?\d+(.\d{1,2})?$
8 正数、负数、和小数:^(-|+)?\d+(.\d+)?$
9 有两位小数的正实数:^[0-9]+(.[0-9]{2})?$
10 有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$
11 非零的正整数:^[1-9]\d* 或 ^+?[1-9][0-9]*$
12 非零的负整数:^-[1-9][]0-9"*
13 非负整数:^\d+
14 非正整数:^-[1-9]\d*|0
15 非负浮点数:^\d+(.\d+)?
16 非正浮点数:^((-\d+(.\d+)?)|(0+(.0+)?))
17 正浮点数:^[1-9]\d*.\d*|0.\d*[1-9]\d*
18 负浮点数:^-([1-9]\d*.\d*|0.\d*[1-9]\d*)
19 浮点数:^(-?\d+)(.\d+)?
2、校验字符的表达式
1 汉字:^[\u4e00-\u9fa5]{0,}$
2 英文和数字:^[A-Za-z0-9]+
3 长度为3-20的所有字符:^.{3,20}$
4 由26个英文字母组成的字符串:^[A-Za-z]+$
5 由26个大写英文字母组成的字符串:^[A-Z]+$
6 由26个小写英文字母组成的字符串:^[a-z]+$
7 由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$
8 由数字、26个英文字母或者下划线组成的字符串:^\w+
9 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+
11 可以输入含有^%&',;=?\"等字符:[^%&',;=?\x22]+
12 禁止输入含有的字符:[^\x22]+
3、特殊需求表达式
1 Email地址:^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)*$
2 域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
3 InternetURL:[a-zA-z]+://[^\s]* 或 ^http://([\w-]+.)+[\w-]+(/[\w-./?%&=]*)?$
4 手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$
5 电话号码("XXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"和"XXXXXXXX):^((\d{3,4}-)|\d{3.4}-)?\d{7,8}$
6 国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
7 身份证号(15位、18位数字):^\d{15}|\d{18}$
8 短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?
9 帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$
10 密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$
11 强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.\d)(?=.[a-z])(?=.*[A-Z]).{8,10}$
12 日期格式:^\d{4}-\d{1,2}-\d{1,2}
13 一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$
14 一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$
15 钱的输入格式:
16 1.有四种钱的表示形式我们可以接受:"10000.00" 和 "10,000.00", 和没有 "分" 的 "10000" 和 "10,000":^[1-9][0-9]*$
17 2.这表示任意一个不以0开头的数字,但是,这也意味着一个字符"0"不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$
18 3.一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$
19 4.这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$
20 5.必须说明的是,小数点后面至少应该有1位数,所以"10."是不通过的,但是 "10" 和 "10.2" 是通过的:^[0-9]+(.[0-9]{2})?$
21 6.这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$
22 7.这样就允许用户只写一位小数.下面我们该考虑数字中的逗号了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$
23 8.1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$
24 备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
25 xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+\.[x|X][m|M][l|L]$
26 中文字符的正则表达式:[\u4e00-\u9fa5]
27 双字节字符:[^\x00-\xff] (包括汉字在内,可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1))
28 空白行的正则表达式:\n\s*\r (可以用来删除空白行)
29 HTML标记的正则表达式:<(\S*?)[^>]>.?</\1>|<.*? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)
30 首尾空白字符的正则表达式:^\s*|\s*) (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)
31 腾讯QQ号:[1-9][0-9]{4,} (腾讯QQ号从10000开始)
32 中国邮政编码:[1-9]\d{5}(?!\d) (中国邮政编码为6位数字)
33 IP地址:\d+.\d+.\d+.\d+ (提取IP地址时有用)
34 IP地址:((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))