正则表达式

正则表达式

前言

相信大家对正则的态度就是

就这就这

但是当每次在需要做校验或者的时候,都会十分狼狈的去百度,去CSDN

将找到的正则表达式直接copy过来,也不会去看里面写的啥

直接就信心满满的去运行

然后就要么报错,要么啥也没匹配到

看别人写的java代码,我们是痛苦的

看别人写的正则表达式,我们感受不到痛苦,是绝望的,直接放弃了

实在受不了这种任人宰割的感觉,所以就花了几天彻彻底底地将正则学了一遍

最真实地感觉就是,正则不简单,得把它当作一种语言去学

这样才可以拿出态度去接受它

下面是学习笔记

介绍

正则表达式是由普通字符(如英文字母)以及特殊字符(也称为元字符)组成的文字模式

该模式对文本查找时需要匹配的一个或多个字符串描述,给出一个匹配模板。

它可以大大简化文本识别工作,现已超出了某种语言或某个系统的局限,成为被人们广为使用的工具。

先给大家介绍一个在线检测正则表达式的网站,十分好用!

链接如下:https://regex101.com

声明:以下我使用的某些概念在不同网站上,不同视频中,都会有不同的叫法,我会尽量将所有的讲法都标注

修饰符

下面在开始正题之前,我要先给大家讲一个修饰符的概念。

我自己在学这个的时候,没有哪一个视频或者教程会将修饰符放到最前面

但是在我学下来,我觉得如果将这个知识点放在最前面,而且这个知识点也不难,只是一个概念

这可能会有利于大家的学习,所以我就尝试将此知识点前置

在正则表达式中有很多修饰符

我们只看常用的四个

大家注意这个地方,这个地方显示的就是当前开启的修饰符

global

可以看到这其中的区别就是

非全局模式只匹配了第一个匹配成功的

而全局模式则匹配了所有匹配成功的

multi line

这里只演示^的情况

这里要有个前置知识,就是

^匹配文本开头

$匹配文本结尾

我们都知道文本只有一个开头和一个结尾

但是开启了multi line之后

^不仅可以匹配文本开头而且可以匹配行首

$不仅可以匹配文本结尾而且可以匹配行尾

single line

这里也需要有前置知识

.可以匹配除了\n之外的所有字符

开启了single line之后

.可以匹配所有的字符,包括\n

insensitive

该修饰符就是关闭大小写敏感

aA是一样的

简单匹配

固定字符串

假如我要在下列文本中匹配export字符串,我们可以这么写

这就是对固定字符串的匹配,这很简单

范围字符

又叫元字符

在正则表达式中,我们有以下表示一定范围的字符

符号 名称
. 匹配除 \n 以外的任何一个字符
\d 匹配单个数字字符,相当于[0-9]
\D 匹配单个非数字字符,相当于[^0-9]
\w 匹配单个数字、大小写字母、下划线字符,相当于[0-9a-zA-Z_]
\W 匹配单个非数字、非大小写字母字符,非下划线,相当于[^0-9a-zA-Z_]
\s 匹配任意一个\n、空格、tab
\S 与\s匹配的相反
\b 匹配每一个单词的前和后
\B 与\b匹配的相反

注意:一个范围字符只可以匹配一个属于该范围的字符

一个一个一个

​ 输入.发现除了\n全亮了是因为开启了global修饰符!

这里没什么好说的,背下,下面给一张图帮助记忆

自定义范围字符

对于十六进制的颜色,#后面只能出现数字和a-f的字符

我们发现内置的这些范围字符都不太好用

这时候就需要使用自定义范围字符

想要定义自定义范围字符,就需要使用[]

下面我们来写匹配十六进制的自定义范围字符

  • 自己写全
    • [0123456789abcdef]
  • 使用-连接符
    • [0-9a-f]

[]还有几种写法,我们使用案例来说明

栗一

数据如下,匹配80年代和90年代以外的

1
2
3
4
5
6
姓名  生日
芝麻 2001-05-22
章一 1993-08-09
沈家怡 1999-05-22
陆力丹 1989-02-19
王子怡 2002-12-15

[]内部的开头写^表示匹配除了方括号里面出现的字符

栗二

匹配特殊字符^或者$或者.

但是小伙伴可能写的顺序不是这样的,你可能是这样写的[^$.]

这里如果^放在第一位,他就会使用栗一种的功能

如果我就是想要用这种顺序写,但是还想匹配^特殊字符本身,可不可以呢?

当然可以啦,这里需要使用转义字符\登场了

栗三

这个案例就有点难了,我们来匹配正确的车牌号,匹配规则如下

普通车牌特征:

  1. 第1位是表籍贯的汉字
  2. 第2位是表城市的大写字母,不包括I和O。因为容易和数字0和1混淆
  3. 后5位是字母加数字,也不包括I和O

测试数据如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
湘C4ASF7
湘G9AWSE
湘O392LEY
鲁E2S1EF
湘D219U5
湘AS2BKN
皖91L2IZ
鲁I0H8F
鲁R10Y2F
A湘1R9GJ3
鲁REF02H
鲁2319G7
鲁3RGN90
N鲁23G90K

基本逻辑控制

在正则表达式中只有这两种逻辑关系,我们这里用一个案例来引出

匹配规则:匹配所有的http状态码

  • 401:未授权
  • 403:无权限访问
  • 404:找不到资源
  • 500:服务内部错误

测试数据如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
404
500
309
403
230
401
400
234
325
401
923
482
340
325

那且呢?

其实我们早就使用过

就比如我们这个案例中,404不就是么?

4并且0并且4

这三个数字同时满足,才匹配出来404

数量控制

我们回顾一下之前的匹配十六进制颜色的案例

测试数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
.el-header {
background-color: #BC0D13
color: #CCCCCC
line-height: 60px;
}

.el-aside {
color: #5fe3a1
}

.active {
background-color: #0086b3;
}

{}就是数量控制的语法,我们来看看不使用数量控制的时候应该怎么写

{n} 指定出现固定次数

{n,} 指定至少出现n次

{n,m} 指定出现n到m次

简写形式

符号 等价
* {0,}
+ {1,}
? {0,1}

懒惰匹配和贪婪匹配

该知识点我们配合案例来食用

匹配所有的span标签

测试数据

1
<span>span1</span>kongge<span>span2</span>

我们先来尝试一下

我们发现,它竟然全匹配了,这是为什么呢?

解释

+会尽量一直往后匹配,一直到无法匹配.为止

本来匹配到span1后面的<符号的时候,就可以停止匹配了

但是因为默认开启的是贪婪匹配,啥意思呢?

就是<还是符合.的吧,那就继续匹配

直到匹配到span2后面的<的时候,发现如果再匹配span2后面的<

那么我们正则表达式中最后的</span>就没法匹配了,所以.就匹配到span2中的2为止

那让我们开启懒惰模式看看结果

解释

+会尽量一直往后匹配,一直到无法匹配为止

匹配到span1后面的<符号的时候

发现其实这个<已经可以匹配正则最后的<

那就结束匹配吧,所以就匹配出了第一组span标签

注意点:

开启懒惰模式中的?和我们在数量控制中的?不一样

懒惰模式中的?只能写在数量控制{}后面

数量控制{}只可以写在范围字符后面[a-f]或者\d这种

这里需要好好理解一下

删除所有的HTML注释

测试数据如下

有三种注释哦

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<div>
<!-- <div></div>-->
<input type="text" v-model:value="test"/>
<!--
{{test}}
-->
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
let app = new Vue({
el: '#app',
data: {
test: ''
},
/* methods: {

},*/
watch: {
test: {
handler: function (newData,oldData) {
console.log(newData);
console.log(oldData);
},
// immediate: true
}
}
});
</script>
</body>
</html>

进阶

分组

分组指将匹配的内容,使用()划分成多个组块,分好的组可用于在匹配后提取、反向引用以及替换操作。

下面使用两个案例来做说明

提取信息转sql

什么意思呢?就是将提取出来的数据转化为DML语句

栗:insert into user(name,age,sex,birthday) values('芝麻','19','男','2001-05-22')

测试数据如下

1
2
3
4
5
芝麻   2001/05/22 19岁  男
章一 1993-08-09 20岁 女
沈家怡 1999.05.22 21岁 女
陆力丹 1989-02-19 19岁 男
王子怡 2002-12-15 19岁 男

$组号可以拿到括号中匹配的内容

匹配正确的自闭和标签

测试数据如下

1
2
3
4
5
6
7
<span>span1</span>
<h2>h22</h2>
<h2>h23</h3>
<h3>h32</h2>
<h1>h11</h1>
<div>d iv1</div>
<span>wrong</dspan>

在正则表达式中使用\组号反向引用前面匹配的组

$0表示整个匹配的字符串

注意反向引用只用在表达式中引用之前的分组

匹配所有的JavaScript中的字符串

JavaScript中有三种字符串

1
2
3
1''
2""
3``

测试文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer();

server.on("request",(req,resp)=>{
let url = req.url;
if(url==='/favicon.ico'){
resp.end();
return;
}
resp.setHeader('Content-type','text/html;charset=utf-8');
console.log(`访问的网址的${url},访问方法是${req.method}`);
let data = fs.readFileSync(path.join(__dirname,url));
resp.end(data);
})
server.listen('80',()=>{
console.log('服务启动\"成功\",80端口');
})

下面做个拓展

分组还有以下四种情况

  1. ?<名称>命名分组
  2. (?:)移除分组
  3. (())嵌套分组
  4. (\d)+分组中使用量词
命名分组

默认情况下通过组号来取值,此外也可以自定义命名组,语法是(?<名称> )

然后在程序中就可以通过<>中的名称来取值。

如:<(?<title>(\S+?))>.*<\/\1> 该表达式就命名了一个title的组,在js的结果中就可通过title属性取值。

注意:

这种命名组只能用于在程序中提取操作,不能进行反向引用,也不能用在替换操作中。

也正因为这种局限性所以命名组使用的很少。

移除分组

()即用于子表达式,同时也是一个分组。

如果只想用作子表达式,而不想用于分组就可以使用(?: )从分组列表中移除。

比如(?:\d{4})-(\d{2})-(\d{2}) 该表达式就只存在两个组,月$1和日$2

嵌套分组

比如:((\d{4})-(\d{2})-(\d{2})) 其组号的命名顺序是以开括号出现顺序为准。

$1指的是一个整体

$2指的是年

$3指的是月

$4指的是日

按照(的顺序进行编组

大小写转换

在Idea、VS Code、Sublime、Notepad++等工具进行替换操作时,还可以使用下表中操作符进行大小写转换

操作符 描述
\u 单个转大写 转换下一个字符为
\U 全部转大写 转换\U后所有字符转
\U…\E 区间转大写 \U\E区间的内容转
\l 单个转小写 转换一下个字符为小写
\L 全部转小写 转换\L后所有字符转小写
\L…\E 区间转小写 \L\U区间的内容转小写

边界断言

介绍

是边界断言让正则表达式有了条件判断的能力

先来看个栗子感受一下

匹配所以姓名,不能带着表头中的姓名

测试数据

1
2
3
4
5
6
姓名	生日			年龄 性别
芝麻 2001/05/22 19岁 男
章一 1993-08-09 20岁 女
沈家怡 1999.05.22 21岁 女
陆力丹 1989-02-19 19岁 男
王子怡 2002-12-15 19岁 男

这里面的(?!)就是前置否定断言

在断言这里,网上的,视频的教程对其的称呼真的是层出不穷,

表达式 环视 预查 边界断言 零宽断言
(?= ) 向前肯定环视 正向肯定预查 边界前置肯定断言 零宽度正预测先行断言
(?! ) 向前否定环视 正向否定预查 边界前置否定断言 零宽度负预测先行断言
(?<= ) 向后肯定环视 反向肯定预查 边界后置肯定断言 零宽度正回顾后发断言
(?<! ) 向后否定环视 反向否定预查 边界后置否定断言 零宽度负回顾后发断言

管他叫什么呢,反正我们只要知道它是断言,然后怎么使用就行了

这里只需要四组图就可以记住这四个断言的作用了

(?=芝)就是芝前面的那个地方

(?!芝)就是除了芝前面地方的其他所有地方

(?=芝)就是芝后面的那个地方

(?!芝)就是除了芝后面地方的其他所有地方

虽然案例很简陋,但是只要理解了这四句话,边界断言就轻轻松松拿下

匹配错误的十六进制颜色

测试数据如下

正确的16进制颜色规则

1、#开头

2、6个16进制数字或者3个

1
2
3
4
5
6
7
8
#BC0D13
#a3e32d
#a656e3
#e00
#91e376
#as
sdas#
#14e3ce

实现这种题目的步骤就是

先找到符合要求的16进制颜色

然后使用向前否定环视

即可找到不符合要求的16进制颜色

找出所有符合条件的密码

  1. 密码长度是8-20位
  2. 大小写字母以及数字必须都有一个。

测试数据

1
2
3
4
5
6
7
8
e3c3d2D3223
86e374W2
e3a34fDas
213498h9
4DIOJ239830239ur23u90asdasasd
5fe362IKds
2ab93F
bf45bbaSd9

提取JavaScript中的合法变量名

测试数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http');
const fs = require('fs');
const path = require('path');
const 1server = http.createServer();

server.on('request',(req,resp)=>{
var url = req.url;
if(url==='/favicon.ico'){
resp.end();
return;
}
resp.setHeader('Content-type','text/html;charset=utf-8');
console.log(`访问的网址的${url},访问方法是${req.method}`);
let _data = fs.readFileSync(path.join(__dirname,url));
resp.end(data);
})
server.listen('80',()=>{
console.log('服务启动成功,80端口');
})

添加千分号

每三位加入一个,做分割

测试数据

1
2
3
4
5
6
7
12349978
13241230981240941270
4912771
240912470
143874131299
329087234
51972

JAVA中的正则

java.util.regex 是一个用正则表达式所定制的模式来对字符串进行匹配工作的类库包

它主要包括两个类:PatternMatcher

  • Pattern: 一个 Pattern 是一个正则表达式经编译后的表现模式。
  • Matcher:一个 Matcher 对象是一个状态机器,它依据 Pattern 对象做为匹配模式对字符串展开匹配检查。

首先一个 Pattern 实例定制了一个所用语法与 PERL 类似的正则表达式经编译后的模式

然后一个 Matcher 实例在这个给定的 Pattern 实例的模式控制下进行字符串匹配后的后续工作,比如替换啊

Pattern

Pattern 的主要方法如下

方法名称 解释
static Pattern compile(String regex) 将给定的正则表达式编译并赋予给 Pattern
static Pattern compile(String regex, int flags) 对指定字符串的截取,参数配置如下
int flags() 返回当前 Pattern 的匹配 flag 参数
Matcher matcher(CharSequence input) 生成一个给定命名的 Matcher 对象
static boolean matches(String regex, CharSequence input) 编译给定的正则表达式并对输入的字串以该正则表达式为模开展匹配
String pattern() 返回该Patter对象所编译的正则表达式

一个正则表达式,也就是一串有特定意义的字符

可以首先要编译成为一个 Pattern 类的实例

这个 Pattern 对象可以使用 matcher(String str) 方法来生成一个 Matcher 实例

接着便可以使用该 Matcher 实例对目标字符串进行匹配后的后续工作

现在我们先来看一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
// 生成一个 Pattern,同时编译一个正则表达式
Pattern p = Pattern.compile("[/\\]+");

//用 Pattern 的 split() 方法把字符串按 "/" 和 "\"分割
String[] result = p.split(
"Kevin has seen《LEON》seveal times,because it is a good film."
+"/ 凯文已经看过《这个杀手不太冷》几次了\因为它是一部"
+"好电影。/名词:凯文。");

for (int i = 0; i < result.length; i++){
System.out.println(result[i]);
}

上述代码的运行结果为:

Kevin has seen《LEON》seveal times,because it is a good film.
凯文已经看过《这个杀手不太冷》几次了,因为它是一部好电影。
名词:凯文。

Matcher

Matcher 对象是一个状态机器,它依据 Pattern 对象做为匹配模式对字符串展开匹配检查

例:下面是一个对单词 dog 出现在输入字符串中出现次数进行计数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
String regex = "\\bdog\\b";
String input = "dog dog dogtie";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input); // 获取 matcher 对象
int count = 0;

while (m.find()) {
count++;
System.out.println("Match number " + count);
System.out.println("start(): " + m.start());
System.out.println("end(): " + m.end());
}

上述代码的执行结果为:

Match number 1 // 第一次出现 (下标 0-2)
start(): 0 // 开始下标 0
end(): 3 // 结束下标 3
Match number 2 // 第二次出现(下标 4-6)
start(): 4 // 开始下标 4
end(): 7 // 结束下标 7

dogtie中的dog没有被匹配是因为我们要的是单独的单词dog而不是在其他单词中的dog

matcheslookingAt

这两个方法都用来尝试匹配一个输入序列模式。

它们的不同是 matches 要求整个序列都匹配

lookingAt 方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。

1
2
3
4
5
6
7
8
9
10
11
String regex = "foo";
String input = "fooooooooooooooooo";
String input2 = "ooooofoooooooooooo";

Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
Matcher matcher2 = pattern.matcher(input2);

System.out.println("lookingAt(): " + matcher.lookingAt());
System.out.println("matches(): " + matcher.matches());
System.out.println("lookingAt(): " + matcher2.lookingAt());

上述代码的执行结果为:

lookingAt(): true
matches(): false
lookingAt(): false

replaceFirstreplaceAll

这两个方法用来替换匹配正则表达式的文本。

不同的是,replaceFirst 只会替换第一次匹配

replaceAll 替换所有匹配。

下面的例子来解释这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String regex = "dog";
String input1 = "The dog says meow. All dogs say meow.";
String input2 = "The dog says meow. All dogs say meow.";
String replace = "cat";

Pattern p = Pattern.compile(regex);

Matcher m1 = p.matcher(input1);
Matcher m2 = p.matcher(input2);

input1 = m1.replaceAll(replace);
input2 = m2.replaceFirst(replace);

System.out.println(input1);
System.out.println(input2);

上述代码的执行结果为:

The cat says meow. All cats say meow.
The cat says meow. All dogs say meow.

从上面的结果,我们可以发现,replaceFirst 方法只将第一个 dog 替换成了 cat。而 replaceAll 方法却将两个 dog 都替换成了 cat

好啦,结束了

正则差不多就这些内容了

如果你发现了错误

望留言批评指正!

给作者买杯咖啡吧~~~