掰碎了的正则表达式 : Java 篇

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 https://juejin.cn/post/6941642435189538824
Github : 👉 https://github.com/black-ant
CASE 备份 : 👉 https://gitee.com/antblack/case

一 . Get Start

正则测试网站 : https://regex101.com/

Git 案例 : Git 不易 , 给个Star 吧

二 . 知识点

2.1 标识符

2.1.1 开始结束
^ 表示开始
$ 表示结束

/^a.*b$/ -- : 以a开始 , 以b结束
2.1.2 元字符符号
符号含义补充
\d配置一个数字字符[0~9] digit
\D匹配一个非数字字符
\w匹配一个单词字符word
\W匹配一个非单词字符
\s匹配一个空格space
\S匹配一个非空格
.匹配换行符以外的任意字符
[^x]匹配除了x 以外的任意字符
[first-last]与从 firstlast 的范围中的任意单个字符匹配[A-Z]
[abc]匹配 其中的任何单个字符
\p{name}name 指定的 Unicode 通用类别或命名块中的任何单个字符匹配
\P{name}与不在 name 指定的 Unicode 通用类别或命名块中的任何单个字符匹配
2.1.3 定位字符
符号含义补充
^匹配字符串的开始
$匹配字符串的结束
\A匹配必须出现在字符串的开头
\Z匹配必须出现在字符串的末尾或出现在字符串末尾的 \n 之前。
\b配置单词的开始或者结束
\B配置不是单词开头或者结束的位置
\G上一个匹配结束的地方
\z匹配必须出现在字符串的末尾-\d{3}\z-901-333"中的"-333
\A匹配必须出现在字符串的开头\A\d{3}901-333-"中的"901
2.1.4 量词符号
符号作用补充
+表示前面字符的一次或多处出现1 ~ n / 前面字符
*表示前面字符的零次或者多次出现0 ~ n
?表示前面字符可能出现 , 也可能不出现0 ~ 1
{m,n}表示出现次数从 m 到 nm <= x <= n
{m}表示一定且必须出现三次x = m
{m , }表示至少出现 m 次m <= x
{0 , }至少出现 0 次无意义
{0 , 1}出现 0 次 或者 1 次
()分组 ,将多种规则进行分组表示见后文详细介绍
2.1.5 其他符号
符号作用补充
[]多选 , 表示匹配其中的一个字符类似于or 概念
()分组详情可看后文

2.2 使用方式

2.2.1 分组

分组案例 详见 Git

// 使用方式
通过小括号来指定子表达式,然后再指定子表达式的相关设置
    
// 组的切分
group 通过 () 来包含 , 每一个 () 代表一个组 , 组由外向内 , 由前向后 编号 , 分组的边好主要为了引用 
    - 0 是特殊分组 , 是整个表达式
    
// 分组的作用: 
    - 组可以在正则中进行使用 , 使用 matcher2.group(i) 可以获取分组对应的匹配结果
    - 分组可以通过反向引用实现不同的效果
    
// 分组的常规概念 : 
1 正常分组 : a(bc)d , 其中 bc 就是一个分组 
2 分组配置数量 : ^a(bc){2}s$ -->  abcbcs    
3 嵌套分组 : ^a(b(c)?){2}s$ --> abcbcs / abcbs / abbs
    ?- 嵌套分组时 多个括号嵌套
    ?- 其中有2个组 (c) ,? 表示可能出现 , (b(c)?){2} 表示该组会出现24 捕获分组 : 
	?- 分组0是一个特殊分组,内容是整个匹配的字符串
	?- 每次分组后分组匹配的子字符串可以在后续访问,好像被捕获了一样
5 分组多选 (http|ftp|file)
	? () + | 表示可以匹配其中一个分组即可
	? ^a(bc|ef|g){2}s$ -> abcbcs / abcgs
    
// 分组的理解 
- 分组的括号如果没有量词匹配或者没有特殊含义匹配 , 可以忽略括号 : (ab) = ab

2.2.2 分组的演示
// 案例一 : group 在没有量词匹配时 , 不具有含义 
String source0 = "aaaa bbbb ccco";
Pattern pattern0 = Pattern.compile("\\w*?\\s");
Pattern pattern01 = Pattern.compile("(\\w)*?\\s");

// 案例二 : Group 匹配量词
String pattern = "^a(bc){2}s$";
logger.info("------> this abf is {} <-------", Pattern.matches(pattern, "abcbcs"));



group001.png

2.2.3 分组引用
// 引用是需要配合分组共同使用的 , 再次说明分组的规则
	- 分组 0 对应整个表达式
	- 顺序 由左到右
	- 命名的组号大于未命名的组号(第一遍给未命名的分组,第二遍给命名的分组)
        
// 什么才是捕获 ? 
	有一点需要提前理解 , 分组等同于在大表达式中生成了一个小表达式 , 但是捕获不是说按照小表达式再去匹配一次 
        而是说!!! -> 把整个文本中 , 符合小表达式的那一段单独提取出来 , 例如下面的代码
        
        
Pattern pattern03 = Pattern.compile("(a+)\\s(b+)\\s(c+)");
String source03 = "aaaa bbbb cccc";
Matcher matcher03 = pattern03.matcher(source03);
if (matcher03.matches()) {
	System.out.println("group 0" + ":" + matcher03.group(0));
	System.out.println("group 1" + ":" + matcher03.group(1));
	System.out.println("group 2" + ":" + matcher03.group(2));
	System.out.println("group 3" + ":" + matcher03.group(2));
}

// ---- 打印结果 , 1 ,2 ,3 分别对应匹配的字符中和group 对应的数据
group 0:aaaa bbbb cccc
group 1:aaaa
group 2:bbbb
group 3:bbbb
    
// 捕获能做什么 -> 例如 , 你有一系列邮箱 , 你想知道邮箱的类型分别有哪些 ?        
 Pattern pattern01 = Pattern.compile("([^@]+)?@([^@]+)?,");
 Matcher matcher01 = pattern01.matcher(source);
List<String> userList = new ArrayList<>();
List<String> emailList = new ArrayList<>();
while (matcher01.find()) {
	userList.add(matcher01.group(1));
	emailList.add(matcher01.group(2));
}

logger.info("------> 邮箱用户有 : {} <-------", userList.toString());
logger.info("------> 邮箱类型有 : {} <-------", emailList.toString());
邮箱用户有 : [123, 124, 125, 126, 128, 129] <-------
邮箱类型有 : [qq.com, qq.com, qq.com, qq.com, 163.com, 163.com] <-------

// 捕获类型  : 捕获包括下方表格的前三行 , 主要的作用是捕获
	- (?<name>exp)   -> 正常分组使用 0 , 1 , 2 命名 ,该方式可以通过 name 命名



    

类型特殊语法说明 exp : 需要处理的文本
正常分组(exp)捕获括号中的值,并且自动命名
非捕获分组( ?: exp )匹配exp , 但是不捕获文本,也不分配组号
捕获并且命名(?<name>subexpression) (?’namesubexpression)匹配exp,并捕获文本到名称为name的组里,也可以写成(?'name’exp)
平衡组(?<name1-name2>subexpression)
(?’name1-name2subexpression)
原子分组( ?> exp )原子分组是贪婪的匹配,当文本和这个分组匹配的成功后
正则表达式引擎在匹配后面的表达式时不会发生回溯行为及尽可能多的匹配
正前向查找分组
零宽度正预测先行断言
( ?= exp )在正前向分组里面的表达式匹配成功后
正则表达式引擎回溯到正前向分组开始匹配的字符处再进行后面正则表达式的匹配
如果后面的正则表达式也匹配成功,整个匹配过程才算成功
负前向查找分组
零宽度负预测先行断言
( ?! exp )这种分组功能和正前向查找分组一样
唯一的不同就是当前向查找分组里面的正则表达式匹配失败的时候才继续后面的匹配过程
正后向查找分组
零宽度正回顾后发断言
( ? <= exp )可以理解成在正后向查找分组前面的正则表达式匹配成功后
正则表达式引擎从最后的位置往字符串左边进行回溯然后和(?<=regex)进行匹配
如果匹配失败则整个匹配过程失败;
如果匹配成功,则将指针移动到正后向查找分组开始进行匹配的位置继续进行后面正则表达式的匹配过程
负后向查找分组
零宽度负回顾后发断言
( ? < ! exp )这种分组功能和正负向查找分组一样
唯一的不同就是当负后向查找分组里面的正则表达式匹配失败的时候才继续后面的匹配过程
(#comment)文本注释
// 查找断言 : 基于断言在断言前面或者后面匹配

// 解释 : 
	这个实现要理解断言的目的 , 断言不是为了匹配 ,断言是坐标 , 用于前后方的匹配 
        
// 案例一  : 原子分组
String source3 = "543543   bcc";
Pattern pattern3 = Pattern.compile("(\\d+)\\s+(?>bc|b)(\\w)");
group 1:543543
group 2:c
    
// 案例二 :  正前向查找分组 ,会先匹配分组前面的正则 , 再匹配分组后的正则
\w+(?=ing) + I'm singing while you're dancing = sing / danc
Pattern pattern5 = Pattern.compile("(\\d+)\\s+(?=s)(\\w+)");
String source5 = "543543   streets";  
group 1:543543
group 2:streets
    
// !! 这个过程很难用语言说清楚 , 所有的步骤都在该显目中 , 建议自行摸索    

2.2.5 反向引用
表达式作用案例
\ number匹配Group 编号(\w)\1
\k命名后向引用。 匹配命名表达式的值(?\w)\k

2.2.6 贪婪 懒惰
// 贪婪的主要表现形式是 ? 
// 贪婪表达式的意思在于一个正则是以何种匹配数量来匹配,包括最大匹配和最小匹配
	- 例如  /d*  ->  0000 / 00000000 
        

        
标识作用
*?重复任意次,但是尽可能少
+?重复一次或者更多次,尽可能少
??重复 0 -1 次 ,尽可能少
{n,m}?重复 n 到 m 次 ,尽可能少
{n,}?重复 n 次以上,尽可能少
// 注意事项 : 
当量符和 ? 交替使用时 , 一定要注意 
正则默认时贪婪的 ,通过 ? 表示懒惰匹配 
    
// 演示 : 懒惰是最小匹配
贪婪 ->  Pattern.compile(".?o"); -> zoboco
懒惰 ->  Pattern.compile(".*?o"); -> zo / bo / co
Matcher matcher123 = pattern123.matcher("zoboco");
while (matcher123.find()) {
	System.out.println(matcher123.group(0));
}
    
2.2.7 转义
\\ : 用于字符转义
    
\\\\ : 用于匹配 \\ 本省
    
! 转义的操作可能超过你的想象 ,它还可以配合多进制 , Unicode,ASCII 码使用

        // \\040 八进制匹配空格 \ nnn
        String source = "a bc d";
        Pattern pattern01 = Pattern.compile("\\w\\040\\w");
        Matcher matcher01 = pattern01.matcher(source);
        while (matcher01.find()) {
            System.out.println(matcher01.group(0));
        }

        // 十六进制 Unicode \\u nnnn
        String source2 = "a bc d";
        Pattern pattern2 = Pattern.compile("\\w\\u0020\\w");
        Matcher matcher2 = pattern2.matcher(source2);
        while (matcher2.find()) {
            logger.info("------> 十六进制 Unicode  source [{}]  group [{}]<-------", source2, matcher2.group(0));
        }


转义 Git 案例

2.2.8 替代

// 使用 : $num -> Group Num

// ------> this is response :John Doe <-------
public void replaceSample() {
	String source = "Doe, John";
	Pattern pattern1 = Pattern.compile("(\\w+),\\s*(\\w+)");
	Matcher matcher1 = pattern1.matcher(source);
	String response = matcher1.replaceAll("$2 $1");
	logger.info("------> this is response :{} <-------", response);
}

// 从案例可以看出来 , 替换中 $2 $1 分别指向的 Group2 Group1

符号含义补充
$num通过组编号替换
$name通过 group Name 匹配替换
2.2.9 注释
// 注释的方式
(?# comment ) 
	--- 内联注释。 该注释在第一个右括号处终止
        
# [至行尾]	
	--- X 模式注释。 该注释以非转义的 # 开头,并继续到行的结尾
    
2.2.10 正则表达式选项值
TODO : 感觉没有使用场景 , 暂时不录入

三 . 使用

3.2 常规写法


// 准备 Pattern , 准备 Source ,通过 Match 匹配
Pattern pattern123 = Pattern.compile(".*?o");
Matcher matcher123 = pattern123.matcher("zoboco");
while (matcher123.find()) {
	System.out.println(matcher123.group(0));
}

// 以下2种方式均可以搜索出数据 , 然后通过 matcher123.group(0) 返回数据
-> matcher123.find()
-> matcher.matches()   

// 注意 , 使用前一定需要匹配一次 , 否则会抛出 No match found    
while (matcher123.find()) {
	System.out.println(matcher123.group(0));
}
// -----------> 直接 group 会返回 No match found    
System.out.println(matcher123.group(0));


3.2 Java 中的用法

> Pattern 对象 : 一个正则表达式可以定义为一个 Pattern 对象
String regex = "<(\\w+)>(.*)</\\1>";
Pattern pattern = Pattern.compile(regex);


C- Pattern
    M- compile(String regex) : 
    M- compile(String regex, int flags) : 指定匹配模式 : 
		- Pattern.DOTALL : 单行模式(点号模式)
		- Pattern.MULTILINE : 多行模式
		- Pattern.CASE_INSENSITIVE :大小写无关模式
    M- quote
              


3.3 Group 语法

public int groupCount( )返回matcher对象中的group的数目。不包括group0。
public String group( ) 返回上次匹配操作(比方说find( ))的group 0(整个匹配)
public String group(int i)返回上次匹配操作的某个group。如果匹配成功,但是没能找到group,则返回nullpublic int start(int group)返回上次匹配所找到的,group的开始位置。
public int end(int group)返回上次匹配所找到的,group的结束位置,最后一个字符的下标加一。
3.4 其他
start( ) : 返回此次匹配的开始位置
end( ) : 返回此次匹配的结束位置
3.5 切分
// 将以正则表达式为界,将字符串分割成String数组。
String[] split(CharSequence charseq)
String[] split(CharSequence charseq, int limit)

3.6 替换
replaceFirst(String replacement)将字符串里,第一个与模式相匹配的子串替换成replacement。
replaceAll(String replacement),将输入字符串里所有与模式相匹配的子串全部替换成replacement。
appendReplacement(StringBuffer sbuf, String replacement)对sbuf进行逐次替换,而不是像replaceFirst( )replaceAll( )那样,只替换第一个或全部子串。

替换案例 Git

3.7 查询和匹配
find() : 尝试在目标字符串里查找下一个匹配子串。
find(int start) : 重设Matcher对象,并且尝试在目标字符串里从指定的位置开始查找下一个匹配的子串。

matches() : 尝试对整个目标字符展开匹配检测,也就是只有整个目标字符串完全匹配时才返回真值

四 . 源码梳理

TODO 

# 附录

文档参考 : 
https://www.cnblogs.com/xyou/p/7427779.html
https://docs.microsoft.com/zh-cn/dotnet/standard/base-types/regular-expression-language-quick-reference

基础案例

常见案例

// . 用于匹配任何字符 -- a.f 
-> abf
-> acf
    
// [] 用于匹配其中任意字符
[abcd] --> 匹配a, b, c, d中的任意一个字符
[0123456789] -> 匹配任意一个数字字符
[0-9a-zA-Z_]
    
// 排除指定字符 (^ 如果不在开头 , 应该看为一个正常字符)
[^abcd] --> 匹配除了a, b, c, d以外的任意一个字符
[^0-9] --> 匹配一个非数字字符
    
// 字符组运算
[[abc][def]] === [abcdef]
[a-z&&[^de]] --> 匹配的字符是a到z,但不能是d或e
    
// 匹配以 xxx 开头
    

数字匹配

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})?$

71-2位小数的正数或负数:
-->  ^(-)?\d+(.\d{1,2})?$

8 正数、负数、和小数:
-->  ^(-|+)?\d+(.\d+)?$

9 有两位小数的正实数:
-->  ^[0-9]+(.[0-9]{2})?$

101~3位小数的正实数:
-->  ^[0-9]+(.[0-9]{1,3})?$

11 非零的正整数:
-->  ^[1-9]\d$ 或 ^([1-9][0-9]){1,3}$ 或 ^+?[1-9][0-9]*$

12 非零的负整数:
-->  ^-[1-9][]0-9″$ 或 ^-[1-9]\d$

13 非负整数:
-->  ^\d+$ 或 ^[1-9]\d*|0$

14 非正整数:
-->  ^-[1-9]\d*|0$ 或 ^((-\d+)|(0+))$

15 非负浮点数:
-->  ^\d+(.\d+)?$ 或 ^[1-9]\d.\d|0.\d[1-9]\d|0?.0+|0$

16 非正浮点数:
-->  ^((-\d+(.\d+)?)|(0+(.0+)?))$ 或 ^(-([1-9]\d.\d|0.\d[1-9]\d))|0?.0+|0$

17 浮点数:
-->  ^(-?\d+)(.\d+)?$ 或 ^-?([1-9]\d.\d|0.\d[1-9]\d|0?.0+|0)$

国内邮政编码

/^[0-9]{6}$/.test(100000)

匹配指定开头结尾得字符串

-> 匹配网址
https://.+.com/

-> 前后匹配
error code.*?: LdapErr

-> 指定字符串之间的数据
(?<=error code ).*?(?=: LdapErr)

(?<="access_token": ").*?(?=","token_type": "bearer",)

字符匹配

1. 汉字:^[\u4e00-\u9fa5]{0,}$
2. 英文和数字:^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$
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+$ 或 ^\w{3,20}$
9. 中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$
10. 中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$ 或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$
11. 可以输入含有^%&',;=?$\"等字符:[^%&',;=?$\x22]+
12. 禁止输入含有~的字符:[^~\x22]+

特殊需求表达式

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}$

6. 国内电话号码(0511-4405222021-87888822):\d{3}-\d{8}|\d{4}-\d{7}
7. 身份证号(15位、18位数字)^\d{15}|\d{18}$
8. 短身份证号码(数字、字母x结尾)^([0-9]){7,18}(x|X)?$ 或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$
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个月(0109112)^(0?[1-9]|1[0-2])$
14. 一个月的31(0109131)^((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.13个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$
	备注:这就是最终结果了,别忘了"+"可以用"*"替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反斜杠,一般的错误都在这里
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*$或(^\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))
5. 电话号码("XXX-XXXXXXX""XXXX-XXXXXXXX""XXX-XXXXXXX""XXX-XXXXXXXX""XXXXXXX"和"XXXXXXXX)^(\(\d{3,4}-)|\d{3.4}-)?\d{7,8}$

附录 : 思维导图

正则案例.png

正则表达式匹配.png

正则表达式位置匹配.png

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值