“来势汹汹,似懂非懂,风吹草动都让我心事重重。”
正则表达式 Regex 及 Java 相关使用
前言
累了=。=
最近在学 Linux,自然有grep
,sed
的一些命令
于是自然接触到了正则表达式,还挺好用
正则表达式 Regex
什么是正则表达式
正则表达式居然还有百科,那就不多讲了,太官方了看着也头疼
要说我自己的理解,正则表达式就是一系列用于匹配字符的规则,而明白这些规则能让我们快速筛选字符,特别是一大串里的一小段字符。
正则表达式定义了一些语法符号来表示特殊的字符集和匹配方式,这也是正则表达式的主体。要是知道这些符号是什么用,那么正则你就会了 90%了。剩下 10%就是自己上手用一用正则。
语法符号
最好的情况是看一个语法符号然后到正则里去使用它,这样最能加深印象。不过那样就会导致文章篇幅很长。
所以我决定先给出一些基础的语法符号,然后用一些例子来匹配。
字符 | 说明 |
---|---|
^ | 匹配字符串开始的位置 |
$ | 匹配字符串结束的位置 |
\b | 匹配单词边界,即匹配字符和空格或开始/结束之间的位置 |
{n} | 对前面的字符匹配 n 次 |
{n, } | 对前面的字符匹配 n 次及以上 |
{n, m} | 对前面的字符匹配 n 次到 m 次 |
* | 对前面的字符匹配 0 次或多次(匹配任意次),相当于{0, } |
+ | 对前面的字符匹配 1 次或多次(至少匹配一次),相当于{1, 0} |
? | 对前面的字符匹配 0 次或 1 次,相当于{0, 1} |
x|y | 匹配 x 或 y。“c|dog”匹配“c”或“dog”,“(c|d)og”匹配”cog”或“dog” |
[xyz] | 字符集,匹配 x 或 y 或 z,相当于“x|y|z” |
[^xyz] | 反向字符集,匹配除了其中的字符 |
[a-z], [A-Z], [0-9] | 分别表示小写字母集,大写字母集,数字集 |
\d | 数字集,相当于[0-9] |
\D | 非数字集,相当于[\^0-9] |
\w | 字母集合+下划线,相当于[a-zA-Z0-9_] |
\W | 非(字母集合+下划线),相当于[\^a-zA-Z0-9] |
\n,\r,\t | 分别表示换行符,回车符,制表符(tab) |
. | 除了换行符以外的任意单个字符 |
简单例子
对上面一些语法的举例,要是看迷糊了可以往前翻一下。
为了方便,这里在 Linux 中进行举例,因为grep
命令可以支持正则。
其实,严格来说单单一个不带任何语法的字符串也算是一个正则表达式,只不过它没有任何其他意义,仅仅是匹配和这个字符一模一样的字串罢了。
而一个比较有意思的表达式就是.*
,它可以匹配任何字符串。.
表示任意字符,*
表示重复任意次嘛。
root # echo "doge is a cat" | grep -o "cat"
cat
root # echo "doge is a cat" | grep -o ".*"
doge is a cat
首先是^
和$
,因为不能显示颜色,所以这里用文字阐述一下(-o
表示只输出匹配到的内容,而非输出整行)
root # echo "doge is a dog" | grep -o "dog"
dog (匹配doge中的dog)
dog (匹配最后的dog)
root # echo "doge is a dog" | grep -o "^dog"
dog (匹配doge中的dog,因为它在起始位置)
root # echo "doge is a dog" | grep -o "dog$"
dog (匹配最后的dog,因为它在末尾)
比如我们想获取以b
开头,以t
结尾的单词,那么中间不管有什么,我们都需要进行匹配。
比如"the bat is the best"
中,我们要匹配到“bat”
和“best
”
一开始我们想到了.*
,但是……
root #echo "the bat is the best" | grep -o "b.*t"
bat is the best
发现它匹配了全部内容。这是因为.
代表任意字符,包括了空格。为此,我们就改用字符集\w
而且我们可以保证bt
不是一个单词,即b
和t
中间一定有内容,所以我们改用+
而不是*
。(*
可以匹配 0 次,即中间没有内容也会匹配,因此会匹配到bt
)
为了让grep
识别\w
,这里用了参数-E
启用grep
的扩展正则表达式
root #echo "the bat is the best" | grep -o -E "b\w+t"
bat
best
可以看到是成功匹配了,但是这里还不完善,因为\w
字符集包括数字和下划线,如果原字符串变成了 "the bat is the best. but b4t is not b_t"
,就不能正常匹配出单词了。
root #echo "the bat is the best. the b4t is not b_t" | grep -E -o "b\w+t"
bat
best
b4t
b_t
不是单词的b4t
和b_t
也被错误匹配到了。因此,我们改用字符集[a-z]
来明确我们只要英文字母
root #echo "the bat is the best. the b4t is not b_t" | grep -E -o "b[a-z]+t"
bat
best
这样就成功匹配到了我们需要的单词。
还有一个比较难理解的是\b
,它难以言喻,但是举个例子就很好表示了
root #echo "never is not a verb" | grep -o "er"
er (never中的er)
er (verb中的er)
root #echo "never is not a verb" | grep -o "er\b"
er (never中的er)
由于never
和verb
都含有er
,所以两个都会被匹配到。
但是如果用"er\b"
,这表示er
之后该单词结束了,要么是空格,要么是空行。符合这个条件的就只有never
中的er
。
获取邮箱
现在我们来假设一个情景,我有一串信息存在info.txt
中,这个其中存储了我的邮箱(学校邮箱是我瞎编的):
root #cat info.txt
This is the text.
in the text i put in my email.
my email is blackdn233@outlook.com haha.
i wont tell you my email.
and i have school email
this is not mine 20180000000@gdufs.edu.cn, cool.
dont forget netease email
blackdawn233@163.com is mine.
咳咳,假装这串信息很长,我们一下子看不到邮箱在哪,现在呢我们就尝试用正则表达式来获取邮箱
(这里规定邮箱名(@
前面)可以用数字、字母、下划线开头,同时也只能包含这三种东西。并且假定@之后的域名只有数字或字母)
首先我们直观感受一下邮箱的特点,无非就是xxx@xxx.xxx
,其中末尾常见的有.com
和.cn
,当但会有其他的形式。
那么第一步我们就通过@
来获取信息:
root #cat info.txt | grep -o ".*@.*"
my email is blackdn233@outlook.com haha.
it is 20180000000@gdufs.edu.cn, cool.
blackdawn233@163.com is mine.
可以看到,我们用".*@.*"
获取到了@
前后所有的内容,即@
所在的一整行。
我们进一步观察,由于规定了@
前面只能有数字、字母、下划线,那么我们将这些作为一个字符集进行匹配,转头发现这些字符集不就是\w
嘛:
root #cat info.txt | grep -o -E "(\w+)@.*"
blackdn233@outlook.com haha.
20180000000@gdufs.edu.cn, cool.
blackdawn233@163.com is mine.
反正@
前面必须得有东西,所以我们顺便把*
改成了+
,变成"\w+@.*"
。
这样我们成功放弃了邮箱前面的空格,匹配到了前半段。
至于后半段,我们一步步来。普遍情况是在@
后面是一段字母,我们就用字符集[0-9a-zA-Z]
表示:
root #cat info.txt | grep -o -E "(\w+)@([0-9a-zA-Z]+)"
blackdn233@outlook
20180000000@gdufs
blackdawn233@163
这样我们成功匹配到了@
后面第一部分的内容。但是还有后面的.com
呢
不过由于.
在正则中表示匹配任意一个字符,为了让他只匹配这个点,需要进行转移,变为“\\.”
。
root #cat info.txt | grep -o -E "(\w+)@([0-9a-zA-Z]+)\.com"
blackdn233@outlook.com
blackdawn233@163.com
这样我们成功匹配到了.com
,不过遗憾的是并非所有邮箱都是.com
,还有.cn
结尾的呢,现在匹配不到学校邮箱了咋整
为了方便起见,我们在这认为邮箱的末尾仅由小写字母组成,并且这段长度在 2~6 个字符之间,这样就有了“([a-z]{2,6})”
:
root #cat info.txt | grep -o -E "(\w+)@([0-9a-zA-Z]+)\.([a-z]{2,6})"
blackdn233@outlook.com
20180000000@gdufs.edu
blackdawn233@163.com
这下我们匹配到了学校邮箱,但没完全匹配。原来学校邮箱存在子域名,也就是说中间可能存在很多个.
为此,我们将“.([a-z]{2,6})”
认为是匹配顶级域名(邮箱末尾)的部分,这样,我们就需要在“([0-9a-zA-Z]+)”
中进行修改
怎么修改呢?无非就是多了个.
嘛,所以我们给他加到字符集中,变成“([0-9a-zA-Z\.]+)”
:
root #cat info.txt | grep -o -E "(\w+)@([0-9a-zA-Z\.]+)\.([a-z]{2,6})"
blackdn233@outlook.com
20180000000@gdufs.edu.cn
blackdawn233@163.com
这样我们就能正确提取出信息中的所有邮箱啦。
不过在实际应用中还需要经过情况讨论,毕竟我也没仔细琢磨邮箱的格式=。=
Java 中使用正则
Java 中的正则一般要用到两个类,分别为Pattern和Matcher
简单来说,Pattern就是将我们的字符串处理为正则表达式,让程序明白我这串字符是正则而不是字符串。Matcher就是将Pattern和待匹配的内容进行处理,得到最后的匹配内容。
String regex = "regex"; //正则,匹配‘regex’内容
String text = "I am a regex string."; //待匹配内容
Pattern pattern = Pattern.compile(regex); //Pattern将字符串变为正则表达式
Matcher matcher = pattern.matcher(text); //Matcher进行匹配
然后,我们就可以从Matcher中对匹配结果进行进一步操作,比如是否匹配成功,匹配到了什么内容等。
是否匹配
我们想判断是否成功匹配,Mathcer给我们提供了两个方法find()
和matches()
find()
是部分匹配,即待匹配内容中有一部分匹配到了我们的正则,就表示成功匹配;
matches()
则是完全匹配,需要待匹配内容完全匹配正则内容才算成功匹配。
String regex = "regex"; //正则,匹配‘regex’内容
String text = "I am a regex string."; //待匹配内容
Pattern pattern = Pattern.compile(regex); //Pattern将字符串变为正则表达式
Matcher matcher = pattern.matcher(text); //Matcher进行匹配
//判断是否匹配成功
boolean isFindMatched = matcher.find(); //用find()匹配
System.out.println("isFindMatched? : " + isFindMatched);
boolean isMatchesMatched = matcher.matches(); //用matches()匹配
System.out.println("isMatchesMatched? : " + isMatchesMatched);
//输出:
//isFindMatched? : true
//isMatchesMatched? : false
因为待匹配的text
里只有“regex”
的部分匹配我们的正则,所以find()
显示成功匹配,matches()
则显示失败。
我们可以用“.*”
的表达式来测试一下,因为“.*”
表示任何字符串,所以无论 find()还是matches()
都会成功配对。
Pattern p = Pattern.compile(".*");
Matcher m = p.matcher("I am a regex string.");
System.out.println(m.matches());
//输出:
//true
由于find()
和matches()
返回的都是boolean
,所以可以放在循环或判断里作为条件,方便进一步操作。
还有一个不怎么常用的是lookingAt()
,它和find()
类似,只要部分匹配即可,但是它的要求更严格,必须在待匹配内容的开头成功匹配才算成功。
比如当待匹配内容为String = "cat is not dog"
的时候用m.lookingAt()
,regex = "cat"
时匹配成功,但是regex = "dog"
就匹配失败了。
之前我们判断是否匹配用的都是Matcher里的方法,因为Matcher本身就是设计来根据regex
对文本进行操作的类。
不过如果想要偷懒不用Matcher,Pattern本身也提供了判断是否匹配的方法:Pattern.matches()
不过当看到源码的时候我们恍然大悟,实际上这个方法里仍然是使用Mathcer的matches()
方法进行判断:
//in Pattern.class
public static boolean matches(String regex, CharSequence input) {
Pattern p = compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
所以说最好还是老老实实把Pattern和Matcher都一起实现
Pattern处理正则,Matcher处理文本,各司其职,偷懒反而还容易把自己搞晕了呢。
转义
这里要注意一点,如果我们单纯想匹配这个星号“*”
,我们都知道需要进行转义,因为“*”
是正则中的保留符号;而 Java 本身也有转义的机制,因此在进行转义的时候需要加2 个“\”。
比如就匹配一个“*”
,我们的正则表达式应该为“\*”
,在这个表达式中,\
和*
都是普通的字符,它们合起来表示正则中转义的*
。
而在 Java 中, 它会优先处理自己的转义机制,也就是说如果我们只写“\*”
,会被 Java 理解成Java 中转义的*
,而 Java 中转义的*
没有任何意义,这就会导致报错。实际上由于\
也是 Java 中的保留符号,所以我们要先对\
转义,把它变成普通的\
,然后和后面的*
合起来:\\*
。
这样,\\*
在 Java 字符串中经过转义处理后就表示单纯的两个字符“\*”
,这个两个字符再经过Pattern处理成正则,就表示一个普通的*
Pattern p = Pattern.compile("\\*");
Matcher m = p.matcher("i have a *.");
System.out.println(m.find());
//输出:
//true
好像有点说复杂了,总之就是\
在 Java 中是个保留符号,所以在正则里用的时候要先"\\"
转义。
获取匹配内容
Matcher获取匹配内容的能力很强,不仅可以得到匹配的内容,还可以获取该内容在原字符串中的起始位置和结束位置。
利用find()
进行匹配后,可以用matcher.group()
方法,它返回的就是匹配到的全部内容。
而start()
和end()
方法可以获得匹配内容的开始位置和结束位置,不过要注意结束位置是不包括匹配内容的,即匹配的内容为text[start()]~text[end() - 1]
。
String regex = "regex"; //正则,匹配‘regex’内容
String text = "I am a regex string."; //待匹配内容
Pattern pattern = Pattern.compile(regex); //Pattern将字符串变为正则表达式
Matcher matcher = pattern.matcher(text); //Matcher进行匹配
if (matcher.find()) {
System.out.println("matched context: " + matcher.group());
System.out.println(matcher.group(0));
System.out.println("start: " + matcher.start() + ", end: " + matcher.end());
}
//输出:
//matched context: regex
//start: 7, end: 12
而matcher.group()
是可以对匹配内容进行分组的,就像这个方法的名字一样
首先在正则表达式里需要我们用括号来表示分组,比如有以下文本:
String textGroup = "I have 2 pans.";
我们打算匹配“数量+物品”,并让数字为一组,物品为一组。
首先我们用“任意长度的数字+空格+任意长度的字母”作为匹配格式,构造正则表达式:
String regexGroup = "\\d+ \\w+";
然后,把我们想要的分组用括号括起来
String regexGroup = "(\\d+) (\\w+)";
再然后我们就可以用group(1)
和group(2)
来表示第一组(第一个括号里的东西)和第二组(第二个括号里的东西)
当然可能会有好奇宝宝会问那group(0)
表示的是啥,它和group()
是一样的,表示的匹配的全部内容
String textGroup = "I have 2 pans.";
String regexGroup = "(\\d+) (\\w+)";
Pattern p = Pattern.compile(regexGroup);
Matcher m = p.matcher(textGroup);
while (m.find()) {
System.out.println("matched context: " + m.group());
System.out.println("start: " + m.start() + ", end: " + m.end());
System.out.println("group 1: " + m.group(1));
System.out.println("group 2: " + m.group(2));
}
//输出
//matched context: 2 pans
//start: 7, end: 13
//group 1: 2
//group 2: pans
可以看到,我们成功将数字和物品进行了分组,可以分别获取了
循环匹配并获取内容
最后,当我们的待匹配内容中有多个符合表达式的内容时,我们需要通过循环调用find()
来不断获取
每当调用find()
后,Matcher会丢弃当前匹配到的内容,继续向下尝试匹配,直到匹配失败,返回false
比如我们把上面的字符串改一下
String textGroup = "I have 2 pans, 3 rulers and 4 books.";
在保持分组的情况下,除了“2 pans”
,后面的“3 rulers”
和“4 books”
我们都想要成功匹配,这就需要循环调用find()
了
String textGroup = "I have 2 pans, 3 rulers and 4 books.";
String regexGroup = "(\\d+) (\\w+)";
Pattern p = Pattern.compile(regexGroup);
Matcher m = p.matcher(textGroup);
while (m.find()) {
System.out.println("matched context: " + m.group());
System.out.println("start: " + m.start() + ", end: " + m.end());
System.out.println("group 1: " + m.group(1));
System.out.println("group 2: " + m.group(2));
}
//输出
//matched context: 2 pans
//start: 7, end: 13
//group 1: 2
//group 2: pans
//matched context: 3 rulers
//start: 15, end: 23
//group 1: 3
//group 2: rulers
//matched context: 4 books
//start: 28, end: 35
//group 1: 4
//group 2: books
替换
既然我们能够取到匹配内容的开始和结束位置,当然也就可以自己写替换了,但这样多少有点麻烦,所以 Java 很贴心地给我们提供了替换的方法。
比较简单的就是Matcher的replaceFirst()
和replaceAll()
的方法,返回的就是替换后的字符串。
replaceFirst()
就是替换第一个匹配的内容,而replaceAll()
则替换所有匹配的内容。
String regex = "regex"; //正则,匹配‘regex’内容
String text = "I am a regex string. I like regex very much."; //待匹配内容
String replaceText = "NEW";
Pattern pattern = Pattern.compile(regex); //Pattern将字符串变为正则表达式
Matcher matcher = pattern.matcher(text); //Matcher进行匹配
if (matcher.find()) {
System.out.println("matched context: " + matcher.group());
System.out.println("replace first: " + matcher.replaceFirst(replaceText));
System.out.println("replace All: " + matcher.replaceAll(replaceText));
}
//输出:
//matched context: regex
//replace first: I am a NEW string. I like regex very much.
//replace All: I am a NEW string. I like NEW very much.
除了这两个简单的替换方法外,Matcher还提供了两个方法,appendReplacement()
和appendTail()
,它们都是对StringBuffer的构建。
在源码中,appendReplacement()
方法被描述为non-terminal step
,而appendTail()
则为terminal step
。
当我们生成一个 Matcher 后,我们传入了待匹配的字符串,而这两个方法用来将字符串匹配替换后变为StringBuffer之后再传出来。
appendReplacement(stringBuffer, replaceText)
会进行一次匹配,匹配成功后它进行替换,替换完了将之前到当前成功位置的内容存入StringBuffer。如果我们想要多次匹配替换, 就需要循环调用appendReplacement()
。相当于find()
一次后replaceFirst()
一次,然后存入StringBuffer
而appendTail(stringBuffer)
就比较简单,他把appendReplacement()
结束后剩下下来没匹配上的东西加入StringBuffer 。
看起来有些难理解,我们举个例子:
待匹配内容为"I have a fat cat. I like fat cat. Fat cat is cute."
,我们想把cat
替换成DOG
执行第一次appendReplacement()
,匹配到第一个cat
,StringBuffer的内容为“I have a fat DOG”
;
执行第二次appendReplacement()
,匹配到第二个cat
,StringBuffer的内容为“I have a fat DOG. I like fat DOG”
;
执行第三次appendReplacement()
,匹配到第三个cat
,StringBuffer的内容为“I have a fat DOG. I like fat DOG. Fat DOG”
然后就不会再执行了,因此已经找不到cat
了,匹配失败。
但是这时候StringBuffer里的内容和原文还是有点出入,最后部分没有加上,这时候调用,StringBuffer的内容就为“I have a fat DOG. I like fat DOG. Fat DOG is cute.”
String regex = "cat"; //正则,匹配‘regex’内容
String text = "I have a fat cat. I like fat cat. Fat cat is cute."; //待匹配内容
String replaceText = "DOG";
Pattern pattern = Pattern.compile(regex); //Pattern将字符串变为正则表达式
Matcher matcher = pattern.matcher(text); //Matcher进行匹配
StringBuffer stringBuffer = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(stringBuffer, replaceText);
System.out.println(stringBuffer.toString());
}
matcher.appendTail(stringBuffer);
System.out.println(stringBuffer.toString());
//输出
//I have a fat DOG
//I have a fat DOG. I like fat DOG
//I have a fat DOG. I like fat DOG. Fat DOG
//I have a fat DOG. I like fat DOG. Fat DOG is cute.
后话
到此其实 Java 正则基本使用就没啥问题了,主要是这两个类和一些方法的使用
正则的内容是独立于语言的,所以还是要靠自己多多学习动手看看
然后日常用的话其实一搜就会有很多正则表达式速查表,像是邮箱啊手机号啊啥的都有