6

正则表达式Regex及Java相关使用

 2 years ago
source link: https://blackdn.github.io/2022/03/13/Regex-and-Java-2022/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

“来势汹汹,似懂非懂,风吹草动都让我心事重重。”

正则表达式Regex及Java相关使用

累了=。=
最近在学Linux,自然有grepsed的一些命令
于是自然接触到了正则表达式,还挺好用

正则表达式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不是一个单词,即bt中间一定有内容,所以我们改用+而不是*。(*可以匹配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

不是单词的b4tb_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)

由于neververb都含有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 [email protected] haha.
i wont tell you my email.
and i have school email
this is not mine [email protected], cool.
dont forget netease email
[email protected] is mine.

咳咳,假装这串信息很长,我们一下子看不到邮箱在哪,现在呢我们就尝试用正则表达式来获取邮箱
(这里规定邮箱名(@前面)可以用数字、字母、下划线开头,同时也只能包含这三种东西。并且假定@之后的域名只有数字或字母)

首先我们直观感受一下邮箱的特点,无非就是[email protected],其中末尾常见的有.com.cn,当但会有其他的形式。
那么第一步我们就通过@来获取信息:

root #cat info.txt | grep -o ".*@.*"
my email is [email protected] haha.
it is [email protected], cool.
[email protected] is mine.

可以看到,我们用".*@.*"获取到了@前后所有的内容,即@所在的一整行。
我们进一步观察,由于规定了@前面只能有数字、字母、下划线,那么我们将这些作为一个字符集进行匹配,转头发现这些字符集不就是\w嘛:

root #cat info.txt | grep -o -E "(\w+)@.*"
[email protected] haha.
[email protected], cool.
[email protected] 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"
[email protected]
[email protected]

这样我们成功匹配到了.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})"
[email protected]
[email protected]
[email protected]

这下我们匹配到了学校邮箱,但没完全匹配。原来学校邮箱存在子域名,也就是说中间可能存在很多个.
为此,我们将“.([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})"
[email protected]
[email protected]
[email protected]

这样我们就能正确提取出信息中的所有邮箱啦。

不过在实际应用中还需要经过情况讨论,毕竟我也没仔细琢磨邮箱的格式=。=

Java中使用正则

Java中的正则一般要用到两个类,分别为PatternMatcher
简单来说,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对文本进行操作的类。
不过如果想要偷懒不用MatcherPattern本身也提供了判断是否匹配的方法:Pattern.matches()
不过当看到源码的时候我们恍然大悟,实际上这个方法里仍然是使用Mathcermatches()方法进行判断:

//in Pattern.class
public static boolean matches(String regex, CharSequence input) {
    Pattern p = compile(regex);
    Matcher m = p.matcher(input);
    return m.matches();
}

所以说最好还是老老实实把PatternMatcher都一起实现
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很贴心地给我们提供了替换的方法。 比较简单的就是MatcherreplaceFirst()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(),匹配到第一个catStringBuffer的内容为“I have a fat DOG”
执行第二次appendReplacement(),匹配到第二个catStringBuffer的内容为“I have a fat DOG. I like fat DOG”
执行第三次appendReplacement(),匹配到第三个catStringBuffer的内容为“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正则基本使用就没啥问题了,主要是这两个类和一些方法的使用
正则的内容是独立于语言的,所以还是要靠自己多多学习动手看看
然后日常用的话其实一搜就会有很多正则表达式速查表,像是邮箱啊手机号啊啥的都有



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK