正则表达式必知必会
第一章 什么事正则表达式?
从根本上来讲,正则表达式的两种基本用途:搜索和替换。
简单来讲,正则表达式是一些用来匹配和处理文本的字符串!
第二章 匹配单个字符
2.1 匹配纯文本
正则表达式可以包含纯文本,甚至可以只包含纯文本。
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/20 16:47 */ public class Main { public static void main(String[] args) { String content = "Hello, my name is foo, Please visit my website at https://www.cnblogs.com/iuyy."; String regex = "my"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println("Found value: " + matcher.group()); } } }
绝大多数正则表达式引擎的默认行为是只返回第一个匹配的结果。但是,同时绝大多数正则表达式的实现都提供了一种能把所有的匹配结果全都找出来的机制。
正则表达式是区分字母的大小写。
2.2 匹配任意个字符
.字符可以匹配任何一个单个的字符。
在绝大多数的正则表达式实现里,.只能匹配出换行符以外的任何单个字符。
人们常用模式来表示实际的正则表达式,正则表达式其实是由字符构成的字符串。
注意:匹配的并不总是整个字符串,而是与某个模式相匹配的字符,即使它们只是整个字符串的一部分。
2.3 匹配特殊字符
.字符在正则表达式里有着特殊的含义。如果模式里需要一个.,就要想办法来告诉正则表达式你需要的是.字符本身而不是它在正则表达式里的特殊含义。
为此,你必须在.的前面加上一个\(反斜杠)字符来对它进行转义。\(反斜杠)是一个元字符。
如果需要搜索\本身,就必须对\字符进行转义。相应的转义序列是连续的反斜杠字符\\。
第三章 匹配一组字符
3.1 匹配多个字符中的某一个
在正则表达式里,我们可以使用元字符[和]来定义一个字符集合。在使用[和]定义的字符集合里,这两个元字符之间的所有字符都是该集合的组成部分,字符集合的匹配结果是能够与该集合里的任何一个成员相匹配的文本。
3.2 利用字符集合区间
在使用正则表达式的时候,会频繁地用到一些字符区间(0-9、A-Z等)。
为了简化字符区间的定义,正则表达式提供了一个特殊的元字符,字符区间可以用-(连字符)来定义。
字符区间并不仅限于数字。
- A-Z,匹配从A到Z的所有大写字母
- a-z,匹配从a到z的所有小写字母
- A-z,匹配从ASCII字符A到ASCII字符z的所有字母。这个模式一般不常用,因为它还包含着[和^等在ASCII字符表里排列在Z-a之间的字符。
在定义一个字符区间的时候,一定要避免让这个区间的尾字符小于它的首字符(例如[3-1]),这种区间没有意义。
注意:-(连字符)是一个特殊的元字符,作为元字符它只能用在[和]之间。在字符集合以外的地方,-只是一个普通字符,只能-本身匹配。
因此,在正则表达式里,-字符不需要被转义。
在同一个字符集合里可以给出多个字符区间。
例如,[A-Za-z0-9]。
3.3 取非匹配
字符集合通常用来指定一组必须匹配其中之一的字符。但是在某些场合,我们需要反过来做,给出一组不需要得到的字符。换句话说,除了那里字符集合的字符,其他字符都可以匹配。元字符^来表明你想对一个字符集合进行取非匹配。这与逻辑非运算很相似,只是这里操作数是字符集合而已。
第四章 使用元字符
4.1 对特殊字符进行转义
元字符是一些在正则表达式里有着特殊含义的字符。
在元字符的前面加上一个反斜杠就可以对它进行转义:转义序列\.将匹配.本身。
在一个完整的正则表达式里,字符\的后面永远跟着另一个字符。
4.2 匹配空白字符
\f |
换叶符 |
\n |
换行符 |
\r |
回车符 |
\t |
水平制表符(Tab键) |
\v |
垂直制表符 |
4.3 匹配特定的字符类别
4.3.1 匹配数字(与非数字)
元字符 |
说明 |
\d |
任何一个数字字符(等价于[0-9]) |
\D |
任何一个非数字字符(等价于[^0-9]) |
4.3.2 匹配字母数字和下划线(与非字母数字下划线)
字母和数字,A到Z、a到z、数字0-9,再加上下换线字符(_),是一个比较常用到的字符集合。
元字符 |
说明 |
\w |
任何一个字母数字或者下划线字符等价于[A-Za-z0-9_] |
\W |
任何一个非字母数字或者下换线字符等价于[^A-Za-z0-9_] |
4.3.3 匹配空白字符(与非空白字符)
一个常见的字符集合是空白字符。
元字符 |
说明 |
\s |
任何一个空白字符(等价于[\f\n\r\t\v]) |
\S |
任何一个非空白字符(等价于[^\f\n\r\t\v]) |
4.4 使用POSIX字符集
POSIZ字符类是许多(但不是所有)正则表达式实现都支持的一种简写形式。
字符类 |
说明 |
[:alnum:] |
任何一个字母或者数字(等价于[A-Za-z0-9]) |
[:alpha:] |
任何一个字母(等价于[A-Za-z]) |
[:blank:] |
空格或者制表符(等价于[\t ]) |
[:cntrl:] |
ASCII控制字符(ASCII0到31,再加上ASCII127) |
[:digit:] |
任何一个数字(等价于[0-9]) |
[:graph:] |
和[:print:]一样,但不包括空格 |
[:lower:] |
任何一个小写字母(等价于[a-z]) |
[:print:] |
任何一个可打印字符 |
[:punct:] |
即不属于[:alnum:]也不属于[:cntrl:]的任何一个字符 |
[:space:] |
任何一个空白字符,包括空格(等价于[^\f\n\r\t\v ]) |
[:upper:] |
任何一个大写字母(等价于[A-Z]) |
[:xdigit:] |
任何一个16进制的数字(等价于[A-Fa-f0-9]) |
注意:一般来说,支持POSIX标准的正则表达式实现都支持表所列出来的那12个POSIX字符类,但是在一些细节方面可能有所差别!
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 13:00 */ public class POSIX { public static void main(String[] args) { String content = "1123 3432 322"; Pattern pattern = Pattern.compile("\\p{Alnum}+"); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println(matcher.group()); } } }
第五章 重复匹配
5.1 有多少个匹配
5.1.1 匹配一个或者多个字符
想要匹配同一个字符(或者字符集合)的多次重复,只要简单地给这个字符(或字符集合)加上一个+字符作为后缀就行了。
+匹配一个或者多个字符(至少一个,不匹配0个字符的情况)。
5.1.2 匹配零个或多个字符
可以使用*元字符来匹配一个字符或者字符集合出现零次或者多次的情况。
5.1.3 匹配零个或者一个字符
可以使用?元字符来匹配一个字符或者字符集合出现零次或者一次的情况。
5.2 匹配的重复次数
为了解决对重复性匹配有更多的控制,正则表达式语言提供了一个用来设定重复次数的语法。重复次数要用{和}元字符来给出,把数值写在它们之间。
5.2.1 为重复匹配次数设定一个精确的值
如果你想为重复匹配次数设定一个精确的值,把那个数字写在{和}之间即可。例如,{3}意味着模式里的前一个字符或者字符集合必须在原始文本里连续出现3次才算一个匹配。
5.2.2 为重复匹配次数设定一个区间
{}语法还可以用来为重复匹配次数设定一个区间,也就是为重复匹配次数设定一个最小值和最大值。
例如,{2,4}意味着模式里的前一个字符或者字符集合必须在原始文本里连续出现2次、3次或者4次才算一个匹配。
5.2.3 匹配“至少重复多少次”
{}语言的最后一种用法是给出一个最小的重复次数(但不必给出一个最大值)。{}的这种用法与我们用来为重复匹配次数设定一个区间的{}语法很相似,只是省略了最大值部分而已。
例如,{3,}表示至少重复三次,与之等价的说法是“必须重复三次或者更多次”。
5.3 防止过度匹配
?只能匹配零个或一个字符,{m},{m, n}也有一个重复次数的上限。
换句话说,这几种语法所定义的重复次数都是有限的。但是本章介绍的其他重复匹配语法在重复次数方面没有上限值,而这样做有时候会导致过度匹配的现象。
例如:一个原始文本里买呢包含着两个HTML<B>标签,而我们的任务是用一个正则表达式把那两个<B>标签里买的文本匹配出来。
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 13:40 */ public class Main { public static void main(String[] args) { String content = "This Book is not available to customers living in <B>AK</B> and <B>HI</B>."; System.out.println("贪婪型匹配!"); Pattern pattern0 = Pattern.compile("<[Bb]>.*</[Bb]>"); Matcher matcher0 = pattern0.matcher(content); while (matcher0.find()) { System.out.println(matcher0.group()); } System.out.println("懒惰型匹配!"); Pattern pattern1 = Pattern.compile("<[Bb]>.*?</[Bb]>"); Matcher matcher1 = pattern1.matcher(content); while (matcher1.find()) { System.out.println(matcher1.group()); } } }
贪婪型元字符 |
懒惰型元字符 |
* |
*? |
+ |
+? |
{m,} |
{m,}? |
第六章 位置匹配
6.1 边界
位置匹配用来解决在什么地方进行字符串匹配操作的问题。
6.2 单词边界
第一种边界(也是最常用的边界)是由限定符\b指定的单词边界。顾明思意,\b用来匹配一个单词的开始和结尾。
注意:\b到底匹配什么东西呢?
简单来说,\b匹配的是这样一个位置,这个位置 位于一个能够用来构成单词的字符(字母、数字和下划线,也就是与\w匹配的字符)和一个不能用来构成单词的字符(也就是与\W相匹配的字符)之间。
b是英文boundary(边界)的首字母。
注意:如果你想匹配一个完整的单词,就必须在你想要匹配的文本的前后都加上\b限定符。
注意:\b匹配且只匹配一个位置,不匹配任何字符。
例如,\bdog\b匹配到的字符串的长度是三个字符(c、a、t),不是5个字符。
6.3 字符串边界
单词边界可以用来进行与单词有关的位置匹配(单词的开头、单词的结束、整个单词等等)。字符串边界有着类似的用途,字符串的边界有着类似的用途,只不过是用来进行与字符串有关的位置匹配而已(字符串的开头,字符串的结束,整个字符串等等)。用来定义字符串边界的元字符有两个:一个是用来定义字符串开头的^,另一个是用来定义字符串结尾的$。
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 17:20 */ public class Main { public static void main(String[] args){ String content[] = {"hello", "_goods", "100","world&"}; Pattern pattern = Pattern.compile("^[A-Za-z_]\\w*\\w$"); for (int i = 0; i < content.length; i++) { Matcher matcher = null; if ((matcher = pattern.matcher(content[i])).find()) { System.out.println(content[i]); } } } }
第七章 使用子表达式
7.1 什么是字表达式?
7.2 子表达式
子表达式是一个更大的表达式的一部分。把一个子表达式划分为一系列字表达式的目的是为了把那些字表达式当做一个独立的元素来使用。字表达式必须用(和)括起来。
7.3 子表达式的嵌套
字表达式允许嵌套。事实上,字表达式允许多重嵌套,这种嵌套的层次在理论上没有限制,但是实际工作中还是应当遵循适可而止的原则。
在分析字表达时的时候,应该按照先内后外的原则来进行而不是从第一个字符开始一个一个字符去尝试。
第八章 回朔引用:前后一致匹配
回朔引用由什么用?
需求:对于某个Web页面,我们需要把某个Web页面里的所有标题文字全部查找出来,而不挂它的级别[1-6]是多少。
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 15:48 */ public class Main { public static void main(String[] args){ String content = "<html>\n" + "<body>\n" + "<h1>This is heading 1</h1>\n" + "<h2>This is heading 2</h2>\n" + "<h3>This is heading 3</h3>\n" + "<h4>This is heading 4</h4>\n" + "<h5>This is heading 5</h5>\n" + "<h6>This is heading 6</h6>\n" + "<h1>This is heading x</h6>\n" + "</body>\n" + "</html>"; Pattern pattern = Pattern.compile("<[hH][1-6]>.*?</[hH][1-6]>"); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println(matcher.group()); } } }
分析:
模式<[hH][1-6]>.*?</[hH][1-6]>
在这个例子里,原始文本里面有一个标题是以<H2>开头、以</H3>结束的。这显然不是一个合法的标题,但是它与我们所使用的模式匹配上了。
8.2回朔引用匹配
看一个简单的例子,这个问题如果不使用回朔引用将根本无法解决问题。
假设你有一段文本,你想把这段文本里所有连续重复出现的单词找出来。显然,在搜索某个单词第二次出现的时候,这个单词必须是已知的。回朔引用允许正则表达式模式引用前面的匹配结果。
import com.sun.org.apache.xerces.internal.impl.xpath.regex.Match; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 16:02 */ public class Main { public static void main(String[] args){ String content = "Nice To To See See You!"; Pattern pattern = Pattern.compile("\\s+(\\w+)\\s+\\1"); Matcher matcher = pattern.matcher(content); while (matcher.find()){ System.out.println(matcher.group()); } } }
分析:这个模式找到了我们需要的东西,这个模式的最后一部分是\\1,这是一个回朔引用,而它引用的正是前面划分出来的那个字表达式。
\\1到底代表着什么?它代表着这个模式里的第一个表达式,\\2代表着第二个字表达式,以此类推。
在看看第一个问题的正确解法:
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 15:48 */ public class Main { public static void main(String[] args){ String content = "<html>\n" + "<body>\n" + "<h1>This is heading 1</h1>\n" + "<h2>This is heading 2</h2>\n" + "<h3>This is heading 3</h3>\n" + "<h4>This is heading 4</h4>\n" + "<h5>This is heading 5</h5>\n" + "<h6>This is heading 6</h6>\n" + "<h1>This is heading x</h6>\n" + "</body>\n" + "</html>"; Pattern pattern = Pattern.compile("<([hH][1-6])>.*?</\\1>"); Matcher matcher = pattern.matcher(content); while (matcher.find()) { System.out.println(matcher.group()); } } }
8.3 回朔引用在替换操作中的应用
/** * @Author:Liu * @Data:2021/2/20 16:27 */ public class Replace { public static void main(String[] args){ String string = "Hello,ben@forta.com is my email address."; String NewString = string.replaceAll("(\\w+[\\w\\.]*@[\\w\\.]+\\w+)","<A href =\"mailto:$1\">$1</A>"); System.out.println(NewString); } }
第九章 前后查找
9.1 前后查找
例子:你要把一个Web页面的页面标题提取出来。HTML页面标题是出现在<title>和</title>标签之间的文字,而这个标签又必须嵌套在HTML代码的<head>部分。
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 16:27 */ public class Main { public static void main(String[] args) { String content = "<html>\n" + "<head>\n" + "<title>我的第一个 HTML 页面</title>\n" + "</head>\n" + "<body>\n" + "<p>body 元素的内容会显示在浏览器中。</p>\n" + "<p>title 元素的内容会显示在浏览器的标题栏中。</p>\n" + "</body>\n" + "</html>"; Pattern pattern = Pattern.compile("<[tT][iI][tT][lL][eE]>.*</[tT][iI][tT][lL][eE]>"); Matcher matcher = pattern.matcher(content); while (matcher.find()){ System.out.println(matcher.group()); } } }
分析:
模式<[tT][iI][tT][lL][eE]>.*</[tT][iI][tT][lL][eE]>所匹配的是<title>标签、</title>标签以及这两个标签之间的任何文字。这个模式的效果与我们的预期基本相符,但是不够理想。因为页面标题才是我们需要的,而找到的匹配里面还包含<title>和</title>标签。能不能只返回页面标题的文字部分呢?
方法一:子表达式。
方法二:前后查找。
9.2 向前查找
一个前向查找模式其实就是一个以?=开头的子表达式,需要匹配的文本跟在=的后面。
例如:
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 16:27 */ public class Main { public static void main(String[] args) { String content[] = {"http://xxx.xxx.xxx", "https://xxx.xxx.xxx", "ftp://xxx.xxx.xxx"}; Pattern pattern = Pattern.compile(".*?(?=:)"); for (int i = 0; i < content.length; i++) { Matcher matcher = null; if ((matcher = pattern.matcher(content[i])).find()) { System.out.println(matcher.group()); } } } }
9.3 向后查找
向后查找的操作符是?<=。
?<=和?=的具体使用方法大同小异,它必须用在一个子表达式里,而且后跟要匹配的文本。
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 16:39 */ public class Main { public static void main(String[] args){ String content[] = {"Goods: $15.0", "Goods: $20.5", "Goods: $10.0"}; Pattern pattern = Pattern.compile("(?<=\\$)[0-9.]+"); for (int i = 0; i < content.length; i++) { Matcher matcher = null; if ((matcher = pattern.matcher(content[i])).find()) { System.out.println(matcher.group()); } } } }
9.4 把向前查找和向后查找结合起来
import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Author:Liu * @Data:2021/2/22 16:27 */ public class Main { public static void main(String[] args) { String content = "<html>\n" + "<head>\n" + "<title>我的第一个 HTML 页面</title>\n" + "</head>\n" + "<body>\n" + "<p>body 元素的内容会显示在浏览器中。</p>\n" + "<p>title 元素的内容会显示在浏览器的标题栏中。</p>\n" + "</body>\n" + "</html>"; Pattern pattern = Pattern.compile("(?<=<[tT][iI][tT][lL][eE]>).*(?=</[tT][iI][tT][lL][eE]>)"); Matcher matcher = pattern.matcher(content); while (matcher.find()){ System.out.println(matcher.group()); } } }
9.5 对前后查找取非
操作符 |
说明 |
(?=) |
正向前查找 |
(?!) |
负向前查找 |
(?<=) |
正向后查找 |
(?<!) |
负向后查找 |
补充:https://docs.microsoft.com/zh-cn/dotnet/standard/base-types/backtracking-in-regular-expressions