Java正則表達(dá)式API指南

1. 概述

在本教程中,我們將討論 Java 正則表達(dá)式 API,以及如何在 Java 編程語(yǔ)言中使用正則表達(dá)式。

在正則表達(dá)式的世界中,有許多不同的風(fēng)格可供選擇,例如 grep、Perl、Python、PHP、awk 等等。

這意味著在一種編程語(yǔ)言中可以工作的正則表達(dá)式,可能在另一種語(yǔ)言中無(wú)法工作。Java 中的正則表達(dá)式語(yǔ)法與 Perl 中的語(yǔ)法最為相似。

2. 設(shè)置

在 Java 中使用正則表達(dá)式,我們不需要任何特殊設(shè)置。JDK 包含了一個(gè)專門用于正則表達(dá)式操作的包,java.util.regex,只需在代碼中導(dǎo)入即可。

此外,java.lang.String 類也內(nèi)置了正則表達(dá)式的支持,通常在我們的代碼中使用。

3. Java 正則表達(dá)式包

java.util.regex 包由三個(gè)類組成:Pattern、MatcherPatternSyntaxException

  • Pattern 對(duì)象是已編譯的正則表達(dá)式。Pattern 類沒(méi)有公共構(gòu)造函數(shù)。要?jiǎng)?chuàng)建模式,首先必須調(diào)用其公共靜態(tài)方法 compile,該方法返回一個(gè) Pattern 對(duì)象。這些方法接受正則表達(dá)式作為第一個(gè)參數(shù)。
  • Matcher 對(duì)象解釋模式,并針對(duì)輸入字符串執(zhí)行匹配操作。它也沒(méi)有公共構(gòu)造函數(shù)。我們通過(guò)在 Pattern 對(duì)象上調(diào)用 matcher 方法來(lái)獲取 Matcher 對(duì)象,并傳遞我們想要匹配的文本。
  • PatternSyntaxException 對(duì)象是未檢查的異常,表示正則表達(dá)式模式中的語(yǔ)法錯(cuò)誤。

我們將詳細(xì)探討這些類,但首先必須了解如何在 Java 中構(gòu)造正則表達(dá)式。

如果我們已經(jīng)熟悉了其他環(huán)境中的正則表達(dá)式,可能會(huì)發(fā)現(xiàn)一些差異,但它們非常小。

4. 簡(jiǎn)單示例

讓我們從最簡(jiǎn)單的正則表達(dá)式使用案例開(kāi)始。如前所述,當(dāng)我們將正則表達(dá)式應(yīng)用于字符串時(shí),它可能會(huì)匹配零次或多次。

最基本的模式匹配形式是對(duì)字符串字面的匹配。例如,如果正則表達(dá)式是 foo 且輸入字符串是 foo,匹配將成功,因?yàn)閮蓚€(gè)字符串是相同的:

@Test
public void givenText_whenSimpleRegexMatches_thenCorrect() {
    Pattern pattern = Pattern.compile("foo");
    Matcher matcher = pattern.matcher("foo");
    assertTrue(matcher.find());
}

我們首先通過(guò)調(diào)用其靜態(tài)方法 compile 并傳遞要使用的模式,來(lái)創(chuàng)建一個(gè) Pattern 對(duì)象。

然后我們通過(guò)調(diào)用 Pattern 對(duì)象的 matcher 方法并傳遞要檢查匹配的文本來(lái)創(chuàng)建一個(gè) Matcher 對(duì)象。

最后,我們調(diào)用 Matcher 對(duì)象中的 find 方法。

find 方法在輸入文本中逐步前進(jìn)并為每次匹配返回 true,因此我們可以使用它來(lái)計(jì)算匹配的次數(shù):

@Test
public void givenText_whenSimpleRegexMatchesTwice_thenCorrect() {
    Pattern pattern = Pattern.compile("foo");
    Matcher matcher = pattern.matcher("foofoo");
    int matches = 0;
    while (matcher.find()) {
        matches++;
    }
    assertEquals(matches, 2);
}

由于我們將運(yùn)行更多測(cè)試,可以將找到匹配次數(shù)的邏輯抽象為一個(gè)名為 runTest 的方法:

public static int runTest(String regex, String text) {
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(text);
    int matches = 0;
    while (matcher.find()) {
        matches++;
    }
    return matches;
}

當(dāng)我們得到 0 次匹配時(shí),測(cè)試應(yīng)該失?。环駝t,它應(yīng)該通過(guò)。

5. 元字符

元字符影響模式的匹配方式;它們?cè)谝欢ǔ潭壬蠟樗阉髂J教砑恿诉壿嫛ava API 支持幾個(gè)元字符,最簡(jiǎn)單的就是點(diǎn)“.”,它匹配任何字符:

@Test
public void givenText_whenMatchesWithDotMetach_thenCorrect() {
    int matches = runTest(".", "foo");
    assertTrue(matches > 0);
}

讓我們考慮前面的示例,其中正則表達(dá)式 foo 匹配了文本 foo 以及 foofoo,兩次。如果我們?cè)谡齽t表達(dá)式中使用點(diǎn)元字符,在第二種情況下我們不會(huì)得到兩次匹配:

@Test
public void givenRepeatedText_whenMatchesOnceWithDotMetach_thenCorrect() {
    int matches = runTest("foo.", "foofoo");
    assertEquals(matches, 1);
}

注意正則表達(dá)式中 foo 后面的點(diǎn)。匹配器匹配每個(gè) foo 之后的文本,因?yàn)樽詈蟮狞c(diǎn)部分表示任何字符。所以在找到第一個(gè) foo 后,其余部分被視為任意字符。這就是為什么只有一次匹配的原因。

API 還支持其他元字符 <([{\\^-=$!|]})?*+.>,我們將在本文進(jìn)一步探討。

6. 字符類

瀏覽官方的 Pattern 類規(guī)范時(shí),我們會(huì)發(fā)現(xiàn)支持的正則表達(dá)式結(jié)構(gòu)的摘要。在字符類下,我們有大約 6 種結(jié)構(gòu)。

6.1 與或類

我們將其構(gòu)造為 [abc]。它匹配集合中的任意一個(gè)元素:

@Test
public void givenORSet_whenMatchesAny_thenCorrect() {
    int matches = runTest("[abc]", "b");
    assertEquals(matches, 1);
}

如果它們都出現(xiàn)在文本中,將分別匹配每個(gè)元素,而不考慮順序:

@Test
public void givenORSet_whenMatchesAnyAndAll_thenCorrect() {
    int matches = runTest("[abc]", "cab");
    assertEquals(matches, 3);
}

它們也可以作為字符串的一部分進(jìn)行交替。在下面的示例中,當(dāng)我們通過(guò)將集合中的每個(gè)元素與第一個(gè)字母交替創(chuàng)建不同的單詞時(shí),所有這些都匹配:

@Test
public void givenORSet_whenMatchesAllCombinations_thenCorrect() {
    int matches = runTest("[bcr]at", "bat cat rat");
    assertEquals(matches, 3);
}

6.2 NOR類

上述集合通過(guò)在第一個(gè)元素位置添加插入符進(jìn)行否定:

@Test
public void givenNORSet_whenMatchesNon_thenCorrect() {
    int matches = runTest("[^abc]", "g");
 
    assertTrue(matches > 0);
}

這里是另一個(gè)例子:

@Test
public void givenNORSet_whenMatchesAllExceptElements_thenCorrect() {
    int matches = runTest("[^bcr]at", "sat mat eat");
 
    assertTrue(matches > 0);
}

6.3  范圍

我們可以通過(guò)使用連字符(-)定義一個(gè)匹配文本應(yīng)落入的范圍。我們也可以對(duì)范圍進(jìn)行否定。

匹配大寫字母:

@Test
public void givenUpperCaseRange_whenMatchesUpperCase_
  thenCorrect() {
    int matches = runTest(
      "[A-Z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 2);
}

匹配小寫字母:


@Test
public void givenLowerCaseRange_whenMatchesLowerCase_
  thenCorrect() {
    int matches = runTest(
      "[a-z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 26);
}

匹配大寫和小寫字母:

@Test
public void givenBothLowerAndUpperCaseRange_
  whenMatchesAllLetters_thenCorrect() {
    int matches = runTest(
      "[a-zA-Z]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 28);
}

匹配給定的數(shù)字范圍:


@Test
public void givenNumberRange_whenMatchesAccurately_
  thenCorrect() {
    int matches = runTest(
      "[1-5]", "Two Uppercase alphabets 34 overall");
 
    assertEquals(matches, 2);
}

匹配另一個(gè)數(shù)字范圍:

@Test
public void givenNumberRange_whenMatchesAccurately_
  thenCorrect2(){
    int matches = runTest(
      "3[0-5]", "Two Uppercase alphabets 34 overall");
  
    assertEquals(matches, 1);
}

6.4 聯(lián)合類

聯(lián)合字符類是通過(guò)組合兩個(gè)或更多字符類產(chǎn)生的結(jié)果:

@Test
public void givenTwoSets_whenMatchesUnion_thenCorrect() {
    int matches = runTest("[1-3[7-9]]", "123456789");
 
    assertEquals(matches, 6);
}

上面的測(cè)試只會(huì)匹配九個(gè)整數(shù)中的六個(gè),因?yàn)槁?lián)合集跳過(guò)了4、5和6。

6.5 交叉點(diǎn)類

類似于聯(lián)合類,這個(gè)類是通過(guò)從兩個(gè)或更多集合中挑選公共元素得到的。要應(yīng)用交集,我們使用&&:

@Test
public void givenTwoSets_whenMatchesIntersection_thenCorrect() {
    int matches = runTest("[1-6&&[3-9]]", "123456789");
 
    assertEquals(matches, 4);
}

我們會(huì)得到四個(gè)匹配,因?yàn)檫@兩個(gè)集合的交集中只有四個(gè)元素。

6.6 減法類

我們可以使用減法來(lái)否定一個(gè)或多個(gè)字符類。例如,我們可以匹配一組奇數(shù)十進(jìn)制數(shù):

@Test
public void givenSetWithSubtraction_whenMatchesAccurately_thenCorrect() {
    int matches = runTest("[0-9&&[^2468]]", "123456789");
 
    assertEquals(matches, 5);
}

只有1、3、5、7、9會(huì)被匹配。

7. 預(yù)定義字符類

Java正則表達(dá)式API還接受預(yù)定義的字符類。上述的一些字符類可以用更簡(jiǎn)短的形式表示,雖然這會(huì)讓代碼不那么直觀。Java正則表達(dá)式的一個(gè)特殊方面是轉(zhuǎn)義字符。

正如我們將看到的,大多數(shù)字符將以反斜杠開(kāi)頭,這在Java中有特殊的含義。要使這些由Pattern類編譯,前導(dǎo)反斜杠必須被轉(zhuǎn)義,即\\d變?yōu)閈\\\d。

匹配數(shù)字,相當(dāng)于[0-9]:

@Test
public void givenDigits_whenMatches_thenCorrect() {
    int matches = runTest("\\\\d", "123");
 
    assertEquals(matches, 3);
}

匹配非數(shù)字,相當(dāng)于[^0-9]:

@Test
public void givenNonDigits_whenMatches_thenCorrect() {
    int mathces = runTest("\\\\D", "a6c");
 
    assertEquals(matches, 2);
}

匹配空格:

@Test
public void givenWhiteSpace_whenMatches_thenCorrect() {
    int matches = runTest("\\\\s", "a c");
 
    assertEquals(matches, 1);
}

匹配非空格:

@Test
public void givenNonWhiteSpace_whenMatches_thenCorrect() {
    int matches = runTest("\\\\S", "a c");
 
    assertEquals(matches, 2);
}

匹配單詞字符,相當(dāng)于[a-zA-Z_0-9]:


@Test
public void givenWordCharacter_whenMatches_thenCorrect() {
    int matches = runTest("\\\\w", "hi!");
 
    assertEquals(matches, 2);
}

匹配非單詞字符:

@Test
public void givenNonWordCharacter_whenMatches_thenCorrect() {
    int matches = runTest("\\\\W", "hi!");
 
    assertEquals(matches, 1);
}

8. 量詞

Java 正則表達(dá)式 API 也允許我們使用量詞。這使我們可以通過(guò)指定匹配的出現(xiàn)次數(shù)來(lái)進(jìn)一步調(diào)整匹配的行為。

要匹配文本零次或一次,我們使用?量詞:

@Test
public void givenZeroOrOneQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("\\\\a?", "hi");
    assertEquals(matches, 3);
}

或者,我們可以使用大括號(hào)語(yǔ)法,Java 正則表達(dá)式 API 也支持這種語(yǔ)法:

@Test
public void givenZeroOrOneQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\\\a{0,1}", "hi");
    assertEquals(matches, 3);
}

這個(gè)示例引入了零長(zhǎng)度匹配的概念。當(dāng)量詞的匹配閾值為零時(shí),它總是會(huì)匹配文本中的所有內(nèi)容,包括每個(gè)輸入末尾的空字符串。這意味著即使輸入為空,它也會(huì)返回一個(gè)零長(zhǎng)度的匹配。

這解釋了為什么我們?cè)谏鲜鍪纠械玫搅巳纹ヅ?,盡管字符串的長(zhǎng)度只有兩個(gè)。第三次匹配是零長(zhǎng)度的空匹配。

要匹配文本零次或無(wú)限次,我們使用*量詞,它類似于?:


@Test
public void givenZeroOrManyQuantifier_whenMatches_thenCorrect() {
     int matches = runTest("\\\\a*", "hi");
     assertEquals(matches, 3);
}

支持的替代方案:


@Test
public void givenZeroOrManyQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\\\a{0,}", "hi");
    assertEquals(matches, 3);
}

有區(qū)別的量詞是+,它的匹配閾值為1。如果所需的字符串根本沒(méi)有出現(xiàn),則不會(huì)有任何匹配,甚至連零長(zhǎng)度的字符串也不會(huì)匹配:


@Test
public void givenOneOrManyQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("\\\\a+", "hi");
    assertFalse(matches);
}

支持的替代方案:


@Test
public void givenOneOrManyQuantifier_whenMatches_thenCorrect2() {
    int matches = runTest("\\\\a{1,}", "hi");
    assertFalse(matches);
}

與 Perl 和其他語(yǔ)言一樣,我們可以使用大括號(hào)語(yǔ)法來(lái)匹配特定次數(shù)的文本:


@Test
public void givenBraceQuantifier_whenMatches_thenCorrect() {
    int matches = runTest("a{3}", "aaaaaa");
    assertEquals(matches, 2);
}

在上述示例中,我們得到了兩次匹配,因?yàn)橹挥挟?dāng)a連續(xù)出現(xiàn)三次時(shí)才會(huì)產(chǎn)生匹配。然而,在下一個(gè)測(cè)試中,我們不會(huì)得到匹配,因?yàn)槲谋局贿B續(xù)出現(xiàn)了兩次:


@Test
public void givenBraceQuantifier_whenFailsToMatch_thenCorrect() {
    int matches = runTest("a{3}", "aa");
    assertFalse(matches > 0);
}

當(dāng)我們?cè)诖罄ㄌ?hào)中使用范圍時(shí),匹配將是貪婪的,從范圍的高端開(kāi)始匹配:


@Test
public void givenBraceQuantifierWithRange_whenMatches_thenCorrect() {
    int matches = runTest("a{2,3}", "aaaa");
    assertEquals(matches, 1);
}

在這里我們指定了至少兩次出現(xiàn),但不超過(guò)三次,所以我們得到了一次匹配,匹配器看到的是一個(gè)aaa和一個(gè)孤立的a,它無(wú)法匹配。

然而,API 允許我們指定惰性或非貪婪的方法,使得匹配器可以從范圍的低端開(kāi)始匹配,匹配兩個(gè)連續(xù)的aa:


@Test
public void givenBraceQuantifierWithRange_whenMatchesLazily_thenCorrect() {
    int matches = runTest("a{2,3}?", "aaaa");
    assertEquals(matches, 2);
}

9. 捕獲組

API 還允許我們通過(guò)捕獲組將多個(gè)字符視為一個(gè)單元。它會(huì)為捕獲組附加編號(hào),并允許使用這些編號(hào)進(jìn)行反向引用。

在本節(jié)中,我們將看到一些如何在 Java 正則表達(dá)式 API 中使用捕獲組的示例。

讓我們使用一個(gè)捕獲組,僅當(dāng)輸入文本包含兩個(gè)相鄰的數(shù)字時(shí)才進(jìn)行匹配:


@Test
public void givenCapturingGroup_whenMatches_thenCorrect() {
    int matches = runTest("(\\\\d\\\\d)", "12");
    assertEquals(matches, 1);
}

上面匹配的編號(hào)是1,使用反向引用告訴匹配器我們要匹配文本的另一部分。這樣,輸入不會(huì)有兩個(gè)單獨(dú)的匹配:


@Test
public void givenCapturingGroup_whenMatches_thenCorrect2() {
    int matches = runTest("(\\\\d\\\\d)", "1212");
    assertEquals(matches, 2);
}

我們可以獲得一次匹配,但通過(guò)反向引用將相同的正則表達(dá)式匹配擴(kuò)展到整個(gè)輸入的長(zhǎng)度:


@Test
public void givenCapturingGroup_whenMatchesWithBackReference_thenCorrect() {
    int matches = runTest("(\\\\d\\\\d)\\\\1", "1212");
    assertEquals(matches, 1);
}

我們必須重復(fù)正則表達(dá)式而不使用反向引用才能實(shí)現(xiàn)相同的效果:


@Test
public void givenCapturingGroup_whenMatches_thenCorrect3() {
    int matches = runTest("(\\\\d\\\\d)(\\\\d\\\\d)", "1212");
    assertEquals(matches, 1);
}

同樣,對(duì)于任何其他數(shù)量的重復(fù),反向引用可以使匹配器將輸入視為一次匹配:


@Test
public void givenCapturingGroup_whenMatchesWithBackReference_thenCorrect2() {
    int matches = runTest("(\\\\d\\\\d)\\\\1\\\\1\\\\1", "12121212");
    assertEquals(matches, 1);
}

但如果我們更改最后一個(gè)數(shù)字,匹配將失?。?/p>


@Test
public void givenCapturingGroupAndWrongInput_whenMatchFailsWithBackReference_thenCorrect() {
    int matches = runTest("(\\\\d\\\\d)\\\\1", "1213");
    assertFalse(matches > 0);
}

請(qǐng)務(wù)必記住轉(zhuǎn)義反斜杠,它們?cè)?Java 語(yǔ)法中至關(guān)重要。

10. 邊界匹配器

Java 正則表達(dá)式 API 也支持邊界匹配。如果我們關(guān)心匹配應(yīng)該出現(xiàn)在輸入文本的確切位置,那么這就是我們要找的。在前面的示例中,我們關(guān)心的只是是否找到了匹配。

要僅在所需正則表達(dá)式在文本開(kāi)頭為真時(shí)匹配,我們使用插入符號(hào)^。

由于文本dog可以在開(kāi)頭找到,這個(gè)測(cè)試將通過(guò):


@Test
public void givenText_whenMatchesAtBeginning_thenCorrect() {
    int matches = runTest("^dog", "dogs are friendly");
    assertTrue(matches > 0);
}

以下測(cè)試將失?。?/p>


@Test
public void givenTextAndWrongInput_whenMatchFailsAtBeginning_thenCorrect() {
    int matches = runTest("^dog", "are dogs are friendly?");
    assertFalse(matches > 0);
}

要僅在所需正則表達(dá)式在文本末尾為真時(shí)匹配,我們使用美元符號(hào)$。我們將在以下情況下找到匹配:


@Test
public void givenText_whenMatchesAtEnd_thenCorrect() {
    int matches = runTest("dog$", "Man's best friend is a dog");
    assertTrue(matches > 0);
}

而在這里我們不會(huì)找到匹配:


@Test
public void givenTextAndWrongInput_whenMatchFailsAtEnd_thenCorrect() {
    int matches = runTest("dog$", "is a dog man's best friend?");
    assertFalse(matches > 0);
}

如果我們只想在找到所需文本時(shí)進(jìn)行匹配,我們?cè)谡齽t表達(dá)式的開(kāi)頭和結(jié)尾使用\\b:

空格是一個(gè)單詞邊界:


@Test
public void givenText_whenMatchesAtWordBoundary_thenCorrect() {
    int matches = runTest("\\bdog\\b", "a dog is friendly");
    assertTrue(matches > 0);
}

而這個(gè)測(cè)試將失?。?/p>


@Test
public void givenTextAndWrongInput_whenMatchFailsAtWordBoundary_thenCorrect() {
    int matches = runTest("\\bdog\\b", "snoop dogg is a rapper");
    assertFalse(matches > 0);
}

類似地,我們可以匹配整個(gè)文本的邊界,而不僅僅是單詞的邊界:


@Test
public void givenText_whenMatchesAtWordAndTextBoundary_thenCorrect() {
    int matches = runTest("^dog$", "dog");
    assertTrue(matches > 0);
}

11. Pattern 類方法

之前,我們只用基本方式創(chuàng)建了 Pattern 對(duì)象。然而,這個(gè)類還有一個(gè)編譯方法的變體,它接受一組標(biāo)志以及正則表達(dá)式參數(shù),這會(huì)影響我們匹配模式的方式。

這些標(biāo)志只是抽象的整數(shù)值。讓我們重載測(cè)試類中的 runTest 方法,使它可以將標(biāo)志作為第三個(gè)參數(shù):

public static int runTest(String regex, String text, int flags) {
    pattern = Pattern.compile(regex, flags);
    matcher = pattern.matcher(text);
    int matches = 0;
    while (matcher.find()){
        matches++;
    }
    return matches;
}

在本節(jié)中,我們將介紹不同支持的標(biāo)志及其用法。

Pattern.CANON_EQ

此標(biāo)志啟用了規(guī)范等效性。指定時(shí),只有當(dāng)兩個(gè)字符的完整規(guī)范分解匹配時(shí),它們才被視為匹配。

考慮帶重音符號(hào)的 Unicode 字符é。其復(fù)合代碼點(diǎn)是 u00E9。然而,Unicode 也有其組件字符的獨(dú)立代碼點(diǎn):eu0065)和重音符號(hào) u0301。在這種情況下,復(fù)合字符 u00E9 與字符序列 u0065u0301 無(wú)法區(qū)分。

默認(rèn)情況下,匹配不考慮規(guī)范等效性:

@Test
public void givenRegexWithoutCanonEq_whenMatchFailsOnEquivalentUnicode_thenCorrect() {
    int matches = runTest("\u00E9", "\u0065\u0301");
    assertFalse(matches > 0);
}

但如果我們添加了標(biāo)志,則測(cè)試將通過(guò):

@Test
public void givenRegexWithCanonEq_whenMatchesOnEquivalentUnicode_thenCorrect() {
    int matches = runTest("\u00E9", "\u0065\u0301", Pattern.CANON_EQ);
    assertTrue(matches > 0);
}

Pattern.CASE_INSENSITIVE

此標(biāo)志允許忽略大小寫進(jìn)行匹配。默認(rèn)情況下,匹配時(shí)考慮大小寫:

@Test
public void givenRegexWithDefaultMatcher_whenMatchFailsOnDifferentCases_thenCorrect() {
    int matches = runTest("dog", "This is a Dog");
    assertFalse(matches > 0);
}

使用此標(biāo)志后,我們可以改變默認(rèn)行為:

@Test
public void givenRegexWithCaseInsensitiveMatcher_whenMatchesOnDifferentCases_thenCorrect() {
    int matches = runTest("dog", "This is a Dog", Pattern.CASE_INSENSITIVE);
    assertTrue(matches > 0);
}

我們還可以使用等效的嵌入標(biāo)志表達(dá)式來(lái)實(shí)現(xiàn)相同的效果:

@Test
public void givenRegexWithEmbeddedCaseInsensitiveMatcher_whenMatchesOnDifferentCases_thenCorrect() {
    int matches = runTest("(?i)dog", "This is a Dog");
    assertTrue(matches > 0);
}

Pattern.COMMENTS

Java API 允許我們使用 # 在正則表達(dá)式中包含注釋。這有助于為可能對(duì)其他程序員不太明顯的復(fù)雜正則表達(dá)式添加文檔說(shuō)明。

注釋標(biāo)志使匹配器忽略正則表達(dá)式中的任何空格或注釋,并只考慮模式。在默認(rèn)匹配模式下,以下測(cè)試將失敗:

@Test
public void givenRegexWithComments_whenMatchFailsWithoutFlag_thenCorrect() {
    int matches = runTest("dog$  #check for word dog at end of text", "This is a dog");
    assertFalse(matches > 0);
}

這是因?yàn)槠ヅ淦鲗⒃谳斎胛谋局胁檎艺麄€(gè)正則表達(dá)式,包括空格和 # 字符。但是,當(dāng)我們使用標(biāo)志時(shí),它會(huì)忽略多余的空格,并將 # 開(kāi)頭的所有文本視為注釋,忽略每行的注釋:

@Test
public void givenRegexWithComments_whenMatchesWithFlag_thenCorrect() {
    int matches = runTest("dog$  #check end of text", "This is a dog", Pattern.COMMENTS);
    assertTrue(matches > 0);
}

還有一個(gè)用于此的替代嵌入標(biāo)志表達(dá)式:

@Test
public void givenRegexWithComments_whenMatchesWithEmbeddedFlag_thenCorrect() {
    int matches = runTest("(?x)dog$  #check end of text", "This is a dog");
    assertTrue(matches > 0);
}

Pattern.DOTALL

默認(rèn)情況下,當(dāng)我們?cè)谡齽t表達(dá)式中使用點(diǎn)“.”表達(dá)式時(shí),我們會(huì)匹配輸入字符串中的每個(gè)字符,直到遇到換行符。

使用此標(biāo)志,匹配將包含行終止符。通過(guò)以下示例我們將更好地理解這一點(diǎn)。由于我們希望對(duì)匹配的字符串進(jìn)行斷言,我們將使用 matchergroup 方法,它返回上一個(gè)匹配項(xiàng)。

首先,讓我們看看默認(rèn)行為:

@Test
public void givenRegexWithLineTerminator_whenMatchFails_thenCorrect() {
    Pattern pattern = Pattern.compile("(.*)");
    Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator") + " continued on another line");
    matcher.find();
    assertEquals("this is a text", matcher.group(1));
}

如我們所見(jiàn),只有在行終止符之前的輸入部分被匹配。

現(xiàn)在在 dotall 模式中,整個(gè)文本(包括行終止符)將被匹配:

@Test
public void givenRegexWithLineTerminator_whenMatchesWithDotall_thenCorrect() {
    Pattern pattern = Pattern.compile("(.*)", Pattern.DOTALL);
    Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator") + " continued on another line");
    matcher.find();
    assertEquals("this is a text" + System.getProperty("line.separator") + " continued on another line", matcher.group(1));
}

我們還可以使用嵌入標(biāo)志表達(dá)式啟用 dotall 模式:

@Test
public void givenRegexWithLineTerminator_whenMatchesWithEmbeddedDotall_thenCorrect() {
    Pattern pattern = Pattern.compile("(?s)(.*)");
    Matcher matcher = pattern.matcher("this is a text" + System.getProperty("line.separator") + " continued on another line");
    matcher.find();
    assertEquals("this is a text" + System.getProperty("line.separator") + " continued on another line", matcher.group(1));
}

Pattern.LITERAL

在此模式下,匹配器不會(huì)給任何元字符、轉(zhuǎn)義字符或正則表達(dá)式語(yǔ)法賦予特殊含義。如果沒(méi)有此標(biāo)志,匹配器將匹配以下正則表達(dá)式與任意輸入字符串:

@Test
public void givenRegex_whenMatchesWithoutLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text");
    assertTrue(matches > 0);
}

這是我們?cè)谒惺纠锌吹降哪J(rèn)行為。但是,使用此標(biāo)志后,我們將找不到匹配,因?yàn)槠ヅ淦鲗⒉檎?(.*) 而不是解釋它:

@Test
public void givenRegex_whenMatchFailsWithLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text", Pattern.LITERAL);
    assertFalse(matches > 0);
}

現(xiàn)在如果我們添加所需的字符串,測(cè)試將通過(guò):

@Test
public void givenRegex_whenMatchesWithLiteralFlag_thenCorrect() {
    int matches = runTest("(.*)", "text(.*)", Pattern.LITERAL);
    assertTrue(matches > 0);
}

沒(méi)有用于啟用文字解析的嵌入標(biāo)志字符。

Pattern.MULTILINE

默認(rèn)情況下,^$ 元字符分別絕對(duì)匹配整個(gè)輸入字符串的開(kāi)始和結(jié)束,匹配器忽略任何行終止符:

@Test
public void givenRegex_whenMatchFailsWithoutMultilineFlag_thenCorrect() {
    int matches = runTest("dog$", "This is a dog" + System.getProperty("line.separator") + "this is a fox");
    assertFalse(matches > 0);
}

通過(guò)啟用此標(biāo)志,匹配器將匹配每一行,而不僅僅是整個(gè)輸入文本:

@Test
public void givenRegex_whenMatchesWithMultilineFlag_thenCorrect() {
    int matches = runTest("dog$", "This is a dog" + System.getProperty("line.separator") + "this is a fox", Pattern.MULTILINE);
    assertTrue(matches > 0);
}

我們還可以使用嵌入表達(dá)式來(lái)啟用多行模式:

@Test
public void givenRegex_whenMatchesWithEmbeddedMultilineFlag_thenCorrect() {
    int matches = runTest("(?m)dog$", "This is a dog" + System.getProperty("line.separator") + "this is a fox");
    assertTrue(matches > 0);
}

12. Matcher 類方法

在本節(jié)中,我們將學(xué)習(xí) Matcher 類中的一些有用方法。為了清晰起見(jiàn),我們將根據(jù)功能對(duì)其進(jìn)行分類。

12.1 索引方法

索引方法提供了有用的索引值,準(zhǔn)確顯示在輸入字符串中找到匹配的位置。在下面的測(cè)試中,我們將確認(rèn)輸入字符串中 "dog" 的匹配起始和結(jié)束索引:


@Test
public void givenMatch_whenGetsIndices_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("This dog is mine");
    matcher.find();
    assertEquals(5, matcher.start());
    assertEquals(8, matcher.end());
}

12.2 研究方法

研究方法遍歷輸入字符串,并返回一個(gè)布爾值,指示是否找到了模式。常用的方法有 matches 和 lookingAt。

matches 和 lookingAt 方法都嘗試將輸入序列與模式進(jìn)行匹配。區(qū)別在于 matches 要求整個(gè)輸入序列完全匹配,而 lookingAt 不需要。

這兩個(gè)方法都從輸入字符串的開(kāi)頭開(kāi)始匹配:

@Test
public void whenStudyMethodsWork_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("dogs are friendly");
    assertTrue(matcher.lookingAt());
    assertFalse(matcher.matches());
}

在這種情況下,matches 方法將返回 true:

@Test
public void whenMatchesStudyMethodWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher("dog");
    assertTrue(matcher.matches());
}

12.3 替換方法

替換方法用于替換輸入字符串中的文本。常見(jiàn)的方法有 replaceFirst 和 replaceAll。

replaceFirst 和 replaceAll 方法用于替換與給定正則表達(dá)式匹配的文本。顧名思義,replaceFirst 只替換第一次出現(xiàn)的匹配項(xiàng),replaceAll 替換所有出現(xiàn)的匹配項(xiàng):

@Test
public void whenReplaceFirstWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher(
      "dogs are domestic animals, dogs are friendly");
    String newStr = matcher.replaceFirst("cat");
    assertEquals(
      "cats are domestic animals, dogs are friendly", newStr);
}

替換所有出現(xiàn)的匹配項(xiàng):

@Test
public void whenReplaceAllWorks_thenCorrect() {
    Pattern pattern = Pattern.compile("dog");
    Matcher matcher = pattern.matcher(
      "dogs are domestic animals, dogs are friendly");
    String newStr = matcher.replaceAll("cat");
    assertEquals("cats are domestic animals, cats are friendly", newStr);
}

replaceAll 方法允許我們用相同的替換文本替換所有匹配項(xiàng)。如果我們想基于具體情況替換匹配項(xiàng),則需要使用一種令牌替換技術(shù)。

13. 結(jié)論

在本文中,我們學(xué)習(xí)了如何在 Java 中使用正則表達(dá)式。我們還探討了 java.util.regex 包中的最重要功能。

若你想提升Java技能,可關(guān)注我們的Java培訓(xùn)課程。