Java中的正则表达式指南

137 阅读14分钟

简介

正则表达式(RegEx)是编程中最强大的工具之一,但它们通常也被误解了。它们帮助你以灵活、动态和有效的方式匹配模式,并允许你根据结果进行操作。

这可以包括验证某个文本中存在的某些模式,找到这些匹配,提取和替换它们,等等。例如,你是否曾经试图注册一个网站,却发现他们因为不包括数字或大写字母而拒绝你的密码?这个网站很有可能使用了正则表达式来确保你输入正确的字符。

在本指南中,我们将深入了解正则表达式,它们如何工作,以及如何在Java中使用它们。regex 我们将主要看一下PatternMatcher 包的类,然后是一些实际的例子和常见的任务。

什么是正则表达式?

正则表达式(RegEx)是用来匹配一些文本中的字符的模式。这些模式被称为搜索模式,允许我们在某个字符串或字符串集中找到一个给定的模式。我们可以验证这个模式的存在,计算它的实例,然后在找到后,轻松地提取它或替换它。

Java正则表达式类

Java的标准API为我们提供了几个类,可以直接使用正则表达式。

  1. MatchResult 接口
  2. Matcher
  3. Pattern
  4. PatternSyntaxException

所有这些都很适合放在java.util.regex 包里,可以很容易地导入为。

// Importing all of the classes/interfaces from the regex package
import java.util.regex.*;
// You can alternatively import certain classes individually
// To reduce overhead
import java.util.regex.Pattern;
import java.util.regex.Matcher;

模式

一个Pattern 实例是某个正则表达式的编译表示。Pattern 没有任何公共构造函数,而是使用.compile() 方法来创建并返回一个Pattern 实例。

.compile() 方法接收一些参数,但主要使用两个参数。第一个参数是字符串格式的正则表达式,第二个参数是匹配标志。匹配标志可以设置为包括CASE_INSENSITIVE,LITERAL,MULTILINE, 或其他几个选项。

让我们用一个字符串表示的正则表达式来创建一个Pattern 实例。

Pattern p = Pattern.compile("Stack|Abuse"); 
System.out.println(p);

这将输出以下内容:

Stack|Abuse

这并不是一个太令人惊讶的输出--它与我们传入Pattern 构造函数的字符串基本相同。不过,这个类本身并不能帮助我们,我们必须使用一个Matcher 来实际匹配编译的RegEx和一些字符串。

PatternMatcher 实例可以通过Pattern 实例的matcher() 方法轻松创建。

Pattern p = Pattern.compile("Stack|Abuse", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("If you keep calling the method many times, you'll perform abuse on the stack.");

这个Matcher ,然后就可以用来使用编译后的模式了。

匹配器类

Matcher 类有几个方法,允许我们实际使用一个编译过的模式。

方法说明返回
.match()它检查Regex是否与给定的输入匹配。布尔型
.group()它提取匹配的子序列。字符串
.start()获取匹配子序列的起始索引。int
.end()获得匹配的子序列的结束索引。int
.find()它找到与Regex模式相匹配的下一个可用表达式。布尔型
.find(int start)找到下一个符合Regex模式的可用表达式,从给定的索引开始。布尔型
.groupCount()它可以找到匹配的总数量。编码

有了这些方法,你可以在逻辑上有相当大的创造性--找到序列的起始索引、匹配的总数、序列本身,甚至提取并返回它们。然而,这些方法可能并不像它们看起来那样直观。

**注意:**请注意,matches() 检查整个字符串,而不是某个部分。find() 遍历字符串,并在每次出现时返回真。通常情况下,它是和while() 循环一起使用的。

while (m.find()) {
    System.out.println(String.format("Matched sequence: %s", m.group()));
    System.out.println(String.format("Start and end of sequence: %s %s \n", m.start(), m.end()));
}

这就导致了:

Matched sequence: abuse
Start and end of sequence: 58 63

Matched sequence: stack
Start and end of sequence: 71 76

此外,每个组是Pattern 中用括号限定的值。在我们的例子中,没有组,因为没有括号包括Stack|Abuse 。因此,groupCount() 的调用将总是在我们的Pattern 上返回0group() 方法也依赖于这种区别,你甚至可以通过在编译模式中传递它们的索引来获得给定的组。

让我们把这个RegEx变成两个组。

Pattern p = Pattern.compile("(Stack)|(Abuse)", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("If you keep calling the method many times, you'll perform abuse on the stack.");

System.out.println("Number of groups: " + m.groupCount());

while (m.find()) {
    System.out.println(String.format("Matched sequence: %s", m.group()));
    System.out.println(String.format("Start and end of sequence: %s %s\n", m.start(), m.end()));
}
Number of groups: 2
Matched sequence: abuse
Start and end of sequence: 58 63

Matched sequence: stack
Start and end of sequence: 71 76

group() 方法允许你从一个给定的字符串中提取组,甚至基于它们的索引或名称,在它被匹配之后。但要注意迭代--以免你最终遇到null 匹配或IllegalStateExceptions

一旦你开始对一个模式进行迭代,它就会被全局改变。

因此,如果你想获得不同的组,比如说,提取字符串日期时间表示的组或电子邮件地址的主机,你应该通过find() 迭代字符串,通过m.group() 获得下一个可用的组,或者运行matches() ,手动获得组。

Pattern p = Pattern.compile("(Stack)(Abuse)", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("StackAbuse");

System.out.println("Number of groups: " + m.groupCount());
if(m.matches()) {
    System.out.println(String.format("Group 1: '%s' \nGroup 2: '%s'", m.group(1), m.group(2)));
}
Number of groups: 2
Group 1: 'Stack' 
Group 2: 'Abuse'

matches() 类只有在整个序列与 RegEx 匹配时才会返回true ,而在我们的例子中,这是它唯一会启动的输入。

更多关于组的信息在后面的章节。

正则表达式的剖析

一旦熟悉了Java用来表示正则表达式的类和用来实际匹配字符串中的序列的类,让我们来了解一下正则表达式本身。

正则表达式不仅仅由字符串字面意义组成,就像我们到目前为止所使用的那样。它们包括元字符量词转义字符。让我们单独看看这些。

元字符

元字符,顾名思义,提供关于RegEx的元信息,并允许我们创建动态表达式,而不仅仅是字面的静态表达式。元字符在正则表达式中具有特殊的含义,不会作为字面字符串被匹配,它们被用作通配符或各种序列模式的替补。

一些最常用的元字符是。

元字符意义
.查找一个字符的匹配
^在一个字符串的开头找到一个匹配项
$查找字符串结尾处的匹配项
\d查找一个数字
\DFind a non-digit (寻找一个非数字
\sFind a whitespace character (查找空白字符
\SFind a non-whitespace character (查找非空格字符
\wFind a word character [a-zA-Z_0-9] (查找一个字)。
\W查找一个非单词字符
\b查找一个以单词为界限的匹配
\B查找一个非词的边界匹配

你可以使用任何数量的这些元字符,尽管对于较长的表达式--它们可能会变得有点混乱。

例如,让我们把之前的正则表达式模式改为搜索一个以大写字母开始的序列,之后包含一个4个字母的序列,然后以 "Stack "结束。

Pattern p = Pattern.compile("^(H)(....)(Stack)$", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("HelloStack");

while (m.find()) {
    System.out.println(String.format("Matched sequence: %s", m.group()));
    System.out.println(String.format("Start and end of sequence: %s %s\n", m.start(), m.end()));
}
Matched sequence: HelloStack
Start and end of sequence: 0 10

不过,仅仅使用元字符在一定程度上限制了我们。如果我们想检查任何字符序列,而不是4个字符,会怎样?

定量词

量词是一组字符,允许我们定义匹配的元字符的数量

量词意义
n+找到至少一个或多个n的匹配项
n*找到一个匹配的0或更多的n
n?找到1或根本不是n的匹配项
n{x}找到一个包含x倍的n的序列的匹配物
n{x, y}找到一个包含x和y的n次方序列的匹配项
n{x,}寻找一个包含至少x次的n序列的匹配项

因此,我们可以很容易地用这些来调整我们之前的RegEx。例如,让我们尝试在另一个字符串中匹配一个以 "Hello "开头的字符串,后面是任何字符序列,最后是三个惊叹号。

Pattern p = Pattern.compile("(Hello)(.*)(!{3})$", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("I wake up and think go myself: Hello Wonderful World!!!");

while (m.find()) {
    System.out.println(String.format("Matched sequence: %s", m.group()));
    System.out.println(String.format("Start and end of sequence: %s %s\n", m.start(), m.end()));
}

这样的结果是

Matched sequence: Hello Wonderful World!!!
Start and end of sequence: 31 55

转义字符

如果你想逃避任何特殊字符的影响,比如元字符或量化符--你可以通过在它们前面加一个\ 来逃避它们。 然而,由于我们是在一个字符串中定义一个RegEx,你必须同时逃避逃避字符。例如,如果你想匹配美元符号,这通常意味着如果在一个字符串的末尾发现一个给定的序列就可以匹配--你要转义其效果,并转义转义字符本身。

Pattern p = Pattern.compile("$", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("It costs $2.50");

Pattern p2 = Pattern.compile("\\$", Pattern.CASE_INSENSITIVE);
Matcher m2 = p.matcher("It costs $2.50");

第一个匹配器匹配字符串是否以$ 字符前缀的序列结束,在这种情况下是空白。这是true ,因为字符串的结尾是,嗯,什么都没有--模式将在结尾处,即第14个索引上找到。在第一个匹配器中,我们匹配的是实际的美元符号,这与我们输入的正确索引中的字符串相匹配。

这两个代码片断都不会导致异常,所以要小心检查你的正则表达式是否会像第一种情况那样无声无息地失败。

到目前为止,我们已经使用了一些组--它们允许我们找到多个集合的匹配。你可以将任何数量的组放在一起,或者作为独立的组。很多时候,组被用来让你把一些输入隔离成已知的部分,然后提取它们,比如把一个电子邮件地址剖析成名字符号主机

组0表示整个模式,而所有其他组被命名为组1组2组n...。

Pattern → (A)(B)(C) 

0组表示整个模式,1组A2组B3组是 C

String email = "someone@gmail.com";

// The entire expresion is group 0 -> Trying to match an email value
// The first group is trying to match any character sequence
// The second group is trying to match the @ symbol
// The third group is trying to match the host name as any sequence of characters
// The final group is trying to check whether the organization type consists of 3 a-z characters
String email = "someone@gmail.com";

Pattern pattern = Pattern.compile("(.*)(@)(.*)(.[a-z]{3})");
Matcher matcher = pattern.matcher(email);

if (matcher.find()) {
    System.out.println("Full email: " + matcher.group(0));
    System.out.println("Username: " + matcher.group(1));
    System.out.println("Hosting Service: " + matcher.group(3));
    System.out.println("TLD: " + matcher.group(4));
}

注: \w 表示一个,是[a-zA-Z_0-9] 的速记。任何包含小写和/或大写字符以及数字的任何组合的单词。

这个代码的结果是:

Full email: someone@gmail.com
Username: someone
Hosting Service: gmail
TLD: com

正则表达式的用途和Java实例

正则表达式的一些最常见的使用情况是验证搜索和提取以及替换。在这一节中,让我们使用我们到目前为止制定的规则来验证、搜索和提取以及替换某些模式的文本。在这些任务之后,我们将执行一些常见的任务,如匹配数字、单个或多个字符等。

用正则表达式在Java中验证字符串

你可以验证文本中是否存在某种模式,这种模式可以简单到一个单词,也可以是你用不同的元字符、字符和量词产生的各种组合之一。一个简单的例子可以是查找某个单词是否存在于某些文本中。

在这一部分,我们将检查某个模式,在本例中只是一个词,是否出现在一个文本中。当然,你仍然可以验证某个模式在文本中的存在。我们将在一个样本文本中寻找 "validate "这个词。

Pattern pattern = Pattern.compile("validate");
String longText = "Some sort of long text that we're looking for something in. " +
 "We want to validate that what we're looking for is here!";

Matcher matcher = pattern.matcher(longText);
boolean found = matcher.find();
System.out.println(found); 

这样的结果是:

true

一个更现实的例子是验证一个电子邮件地址,以检查某人是否真的输入了一个有效的地址或只是使用了一些垃圾邮件的值。一个有效的电子邮件包含一些字符序列,然后是一个@ 符号,一个主机名(另一个字符序列)和一个组织符号,它包含三个字母,可以是任何组合 -edu,com,org, 等等。

了解了这一点,为了在Java中使用RegEx验证一个电子邮件地址,我们将编译表达式,并使用matches() 方法来检查它是否有效。

Pattern pattern = Pattern.compile("\\w*[@]\\w*[.][a-z]{3}");

Matcher matcher = pattern.matcher("someone@gmail.com");
boolean match = matcher.matches();
System.out.println(match);

这样做的结果是:

true

在Java中用正则表达式查找和提取模式

很多时候,除了验证之外--你想找到一个给定序列的起点和终点。有了这个,你可以为文本编辑器应用程序创建高性能的查找功能,使搜索过程自动化。此外,你还可以通过找到你感兴趣的序列来缩短对页面、申请人信或任何类型文本的关键词的搜索,例如,为人类操作员突出显示它们。

Matcher 要使用正则表达式找到一个序列的开始和结束,正如我们之前看到的,我们可以使用start()end() 实例的方法。

Pattern pattern = Pattern.compile("(search|match)");

String searchText = "You can easily search for a keyword in text using RegEx. " +
                "A keyword is just a sequence of characters, that are easy to match.";

Matcher matcher = pattern.matcher(searchText);

while (matcher.find()) {
    System.out.println("Found keyword: " + matcher.group());
    System.out.println("Start index is: " + matcher.start());
    System.out.println("End index is: " + matcher.end() + "\n");
}

输出结果将如下:

Found keyword: search
Start index is: 15
End index is: 21

Found keyword: match
Start index is: 118
End index is: 123

在这里,我们还提取了关键词--你可以将它们记录下来用于分析,将它们输出到终端,比如这样,或者以其他方式处理它们或对它们采取行动。你可以把文本中的某些关键词作为运行其他方法或命令的门径。

例如,在创建聊天室或其他用户可以与其他用户交流的应用程序时,某些词语可以被审查,以保持积极的体验。在其他情况下,某些词可能会引起人类操作员的注意,在那里可能会出现一个特定的用户正在煽动不应该煽动的行为。

Pattern pattern = Pattern.compile("(fudge|attack)");

String message = "We're launching an attack at the pudding palace." +
                "Make way through all the fudge, the King lies beyond the chocolate!";

Matcher matcher = pattern.matcher(message);

while (matcher.find()) {
    System.out.println("Found keyword: " + matcher.group());
    System.out.println("Start index is: " + matcher.start());
    System.out.println("End index is: " + matcher.end());
            
    if(matcher.group().equals("fudge")) {
        System.out.println("This word might be inappropriate!");
    } else if(matcher.group().equals("attack")) {
        System.out.println("911? There's an attack going on!");
    }
}

虽然,事情可能并不像你想象的那样严峻。

Found keyword: attack
Start index is: 19
End index is: 25
911? There's an attack going on!

Found keyword: fudge
Start index is: 73
End index is: 78
This word might be inappropriate!

审查制度并不酷。

从文本中提取电子邮件地址

如果你刚刚得到一堆包含电子邮件地址的文本,并且你想提取它们,如果它们是有效的地址,该怎么办?这在搜刮网页的时候并不罕见,比如说联系信息。

**注意:**在进行网络搜刮时,应该以道德的方式进行,而且只有在网站的robot.txt 文件允许你这样做。确保你符合ToS规定,并且你不会滥用网站的流量和连接,对其他用户和网站的所有者造成损害。

Pattern pattern = Pattern.compile("\\w*[@]\\w*[.][a-z]{3}");
String text = "We want to extract all email in this text. " +
                "Yadda yadda, some more text." +
                "wiza.april@treutel.com\n" +
                "walker.arvid@larkin.net\n" +
                "wrowe@quigley.org\n";
Matcher matcher = pattern.matcher(text);

List<String> emailList = new ArrayList<>();
while(matcher.find()) {
    emailList.add(matcher.group());
}

System.out.println(emailList);

输出结果将是文本中发现的所有电子邮件。

[april@treutel.com, arvid@larkin.net, wrowe@quigley.org]ß

匹配单个字符

要匹配一个单一的字符,正如我们之前所看到的,我们只需将其表示为.

Pattern pattern = Pattern.compile(".tack");
Matcher matcher = pattern.matcher("Stack");
boolean match = matcher.matches();
System.out.println(match);

这样的结果是:

true

匹配多个字符

多个字符的匹配可以归结为一个量化的. ,但更常见的是--你会使用一个字符范围来代替。例如,让我们检查一个给定的字符串是否有任何数量的字符,属于字母表的范围内。

Pattern pattern = Pattern.compile("[a-z]+");
Matcher matcher = pattern.matcher("stack");
boolean match = matcher.matches();
System.out.println(match);

Pattern pattern2 = Pattern.compile("[a-z]+");
Matcher matcher2 = pattern2.matcher("stack99");
boolean match2 = matcher2.matches();
System.out.println(match2);

这就导致了:

true
false

第二次检查返回false ,因为输入的字符串不仅包含属于小写字母的字符,还包含数字。

匹配单词序列

除了字母表范围,你还可以匹配\w 的模式--它是[a-zA-Z_0-9] 的缩写。

Pattern pattern = Pattern.compile("\\w*");
Matcher matcher = pattern.matcher("stack");
boolean match = matcher.matches();
System.out.println(match);

Pattern pattern2 = Pattern.compile("\\w*");
Matcher matcher2 = pattern2.matcher("stack!");
boolean match2 = matcher2.matches();
System.out.println(match2);

这就导致了:

true
false

匹配非字序列

\w 相似,\W 是另一个简写。它是一个非字序列的速记版本。它基本上是\w 的反面,排除所有属于[a-zA-Z_0-9] 的字符。

Pattern pattern = Pattern.compile("\\W*");
Matcher matcher = pattern.matcher("stack");
boolean match = matcher.matches();
System.out.println(match);

Pattern pattern2 = Pattern.compile("\\W*");
Matcher matcher2 = pattern2.matcher("?????");
boolean match2 = matcher2.matches();
System.out.println(match2);

这就导致了:

false
true

? 不在 范围内,所以第二个匹配器返回 。[a-zA-Z_0-9] false

匹配数位和非数位

检查是否有一个数字,我们可以使用\d ,而检查任何数量的数字就像对其应用通配符一样。按照前面的惯例,\D 表示非数字,而不是数字。

Pattern pattern = Pattern.compile("\\d*"); 
Matcher matcher = pattern.matcher("999");
boolean match = matcher.matches();
   
Pattern pattern2 = Pattern.compile("\\D*");
Matcher matcher2 = pattern2.matcher("https://www.youtube.com/watch?v=dQw4w9WgXcQ");
boolean match2 = matcher2.matches();
   
System.out.println(match);
System.out.println(match2);

输出结果如下:

true
true

结论

正则表达式(RegEx)是编程中最强大的工具之一,但它们也经常被误解。它们帮助你以灵活、动态和有效的方式匹配模式,并允许你根据结果执行操作。

它们可能令人生畏,因为复杂的序列往往变得非常难读,然而,它们仍然是当今最有用的工具之一。在本指南中,我们已经介绍了正则表达式的基本知识,以及如何使用regex 包在Java中进行模式匹配。