/

如何使用JavaScript正則表達式

如何使用JavaScript正則表達式

通過這篇簡短的指南,您可以學習關於JavaScript正則表達式的所有內容,了解最重要的概念並通過示例展示。

正則表達式介紹

正則表達式(也稱為regex)是一種以非常高效的方式處理字符串的方法。

通過使用特殊的語法來定義正則表達式,您可以:

  • 在字符串中搜索文本
  • 在字符串中替換子字符串
  • 從字符串中提取信息

幾乎每一種編程語言都實現了正則表達式。每種實現之間有一些小差異,但是基本概念幾乎都是通用的。

正則表達式可以追溯到20世紀50年代,那時它被正式作為字符串處理算法的一種概念搜索模式。

它在grep、sed等UNIX工具中得到了實現,以及在流行的文本編輯器中使用,在Perl編程語言中引入了正則表達式,隨後在許多其他編程語言中引入。

JavaScript與Perl一起是具有內置的正則表達式支持的編程語言之一。

困難但有用

對於初學者來說,正則表達式可能完全看不懂,但很多時候即使對專業開發人員來說也是如此,除非他們願意投入必要的時間去理解它們。

由於寫出易於理解、易於閱讀和易於維護/修改的正則表達式非常困難,所以使用正則表達式是唯一明智的方法來執行某些字符串操作,因此它是一個非常有價值的工具。

本教程旨在以簡單的方式介紹JavaScript正則表達式,並提供閱讀和創建正則表達式所需的所有信息。

經驗法則是,簡單的正則表達式容易閱讀且易於撰寫,而複雜的正則表達式如果你不深入了解基礎知識,很快就會變得混亂不堪

正則表達式是什麼樣子

在JavaScript中,正則表達式是一個對象,可以用兩種方式定義。

第一種方式是使用構造函數通過實例化一個新的RegExp對象來定義:

1
const re1 = new RegExp('hey')

第二種方式是使用正則字面量形式:

1
const re1 = /hey/

您知道JavaScript有對象字面量數組字面量嗎?它還有正則字面量

在上面的示例中,hey被稱為模式。在字面形式中,它由斜杠分隔,而使用對象構造函數時則不是。

這是兩種形式之間的第一個重要差異,但我們稍後會看到其他差異。

工作原理

我們在上面定義的正則表達式re1非常簡單。它在字符串hey中搜索,沒有任何限制:字符串可以包含大量的文本和hey在中間,正則表達式會返回匹配。它也可以只包含hey,它也可以匹配。

這非常簡單。

您可以使用RegExp.test(String)來測試正則表達式,它將返回一個布爾值:

1
2
3
4
5
6
7
// ✅
re1.test('hey')
re1.test('blablabla hey blablabla')

// ❌
re1.test('he')
re1.test('blablabla')

在上面的示例中,我們只是檢查了是否"hey"滿足存儲在re1中的正則表達式模式。

這是最簡單的情況,但是您已經了解了許多關於正則表達式的概念。

錨定

1
/hey/

在字符串中匹配hey

如果要匹配以hey開始的字符串,請使用^操作符:

1
2
/^hey/.test('hey')      // ✅
/^hey/.test('bla hey') // ❌

如果要匹配以hey結尾的字符串,請使用$操作符:

1
2
3
/hey$/.test('hey')         // ✅
/hey$/.test('bla hey') // ✅
/hey$/.test('hey you') // ❌

結合這兩個,匹配正好與hey相同的字符串:

1
/^hey$/.test('hey')   // ✅

要匹配以一個子字符串開始並以另一個子字符串結尾的字符串,可以使用.*,它匹配任何字符的重複0次或多次:

1
2
3
4
/^hey.*joe$/.test('hey joe')                      // ✅
/^hey.*joe$/.test('heyjoe') // ✅
/^hey.*joe$/.test('hey how are you joe') // ✅
/^hey.*joe$/.test('hey joe!') // ❌

匹配範圍內的項目

與其匹配特定的字符串,您可以選擇匹配範圍內的任何字符,例如:

1
2
3
4
/[a-z]/      // a, b, c, ... , x, y, z
/[A-Z]/ // A, B, C, ... , X, Y, Z
/[a-c]/ // a, b, c
/[0-9]/ // 0, 1, 2, 3, ... , 8, 9

這些正則表達式匹配包含這些範圍內的字符的字符串:

1
2
3
4
5
6
/[a-z]/.test('a')    // ✅
/[a-z]/.test('1') // ❌
/[a-z]/.test('A') // ❌

/[a-c]/.test('d') // ❌
/[a-c]/.test('dc') // ✅

範圍可以結合使用:

1
/[A-Za-z0-9]/
1
2
3
/[A-Za-z0-9]/.test('a')    // ✅
/[A-Za-z0-9]/.test('1') // ✅
/[A-Za-z0-9]/.test('A') // ✅

多次匹配範圍項目

通過在組閉合括號之後放置正則表達式的重複字符,您可以檢查字符串是否只包含範圍內的一個字符,通過在開始使用^並以$字符結束:

1
2
3
4
5
/^[A-Z]$/.test('A')         // ✅
/^[A-Z]$/.test('AB') // ❌
/^[A-Z]$/.test('Ab') // ❌
/^[A-Za-z0-9]$/.test('1') // ✅
/^[A-Za-z0-9]$/.test('A1') // ❌

否定模式

在範圍的開始處使用^字符將其錨定到字符串的開頭。

在範圍內使用它時,這是它的否定形式,所以:

1
2
3
4
/[^A-Za-z0-9]/.test('a')     // ❌
/[^A-Za-z0-9]/.test('1') // ❌
/[^A-Za-z0-9]/.test('A') // ❌
/[^A-Za-z0-9]/.test('@') // ✅

元字符

下列字符是特殊的:

  • \
  • /
  • [ ]
  • ( )
  • { }
  • ?
  • +
  • *
  • |
  • .
  • ^
  • $

它們是特殊的,因為它們是正則表達式模式中具有意義的控制字符,因此如果要將它們用作匹配字符,則需要將它們進行轉義,即在它們之前加上反斜槓符號:

1
2
3
/^\\$/
/^\^$/ // /^\^$/.test('^') ✅
/^\$$/ // /^\$$/.test('$') ✅

字符串邊界

\b\B可讓您檢查字符串是否位於單詞的開始或結尾:

  • **\b**:匹配單詞的開頭或結尾中的一組字符
  • **\B**:匹配不在單詞的開頭或結尾中的一組字符

例如:

1
2
3
4
'I saw a bear'.match(/\bbear/)           // Array ["bear"]
'I saw a beard'.match(/\bbear/) // Array ["bear"]
'I saw a beard'.match(/\bbear\b/) // null
'cool\_bear'.match(/\bbear\b/) // null

使用正則表達式替換

我們已經看到了如何檢查字符串是否包含模式。

我們還看到了如何將字符串的部分提取到數組中,並找到匹配模式的。

現在讓我們看一下如何根據一個模式替換字符串的部分。

JavaScript中的String對象具有一個replace()方法,它可以在字符串上執行單一替換,可以在不使用正則表達式的情況下使用:

1
2
"Hello world!".replace('world', 'dog')      // Hello dog!
"My dog is a good dog!".replace('dog', 'cat') // My cat is a good dog!

此方法還接受正則表達式作為參數:

1
"Hello world!".replace(/world/, 'dog')      // Hello dog!

只有在使用g標誌時,才能在JavaScript中通過正則表達式替換字符串中的多個匹配:

1
"My dog is a good dog!".replace(/dog/g, 'cat')      // My cat is a good cat!

通過使用$1引用匹配的分組,我們可以做更有趣的事情,例如移動字符串的部分:

1
2
"Hello, world!".replace(/(\w+), (\w+)!/, '$2: $1!!!')
// "world: Hello!!!"

您可以使用函數,而不是字符串,來完成更高級的事情。它將接收與String.match(RegExp)RegExp.exec(String)返回的一樣的一系列參數,根據分組的數量而有所不同:

1
2
3
4
5
6
7
"Hello, world!".replace(/(\w+), (\w+)!/, (matchedString, first, second) => {
console.log(first);
console.log(second);

return `${second.toUpperCase()}: ${first}!!!`
})
// "WORLD: Hello!!!"

貪婪模式

預設情況下,正則表達式被認為是貪婪的

這是什麼意思?

接受此模式之後,請看此正則表達式

1
/\$(.+)\s?/

它應該從字符串中提取一個金額

1
2
/\$(.+)\s?/.exec('This costs $100')[1]
//100

但是,如果我們在數字之後有更多的單詞,它將進行匹配:

1
2
/\$(.+)\s?/.exec('This costs $100 and it is less than $200')[1]
//100 and it is less than $200

為什麼?因為正則表達式模式在$符號後面使用.+匹配任何字符,並且它在達到字符串結束時才停止。然後,它在結束的位置結束,因為\s?使結尾的空格是可選的。

要修復這個問題,我們需要告訴正則表達式是懶惰的,並且只執行可能的最小匹配數量。我們可以使用?符號在量詞後進行這樣的操作:

1
2
/\$(.+?)\s/.exec('This costs $100 and it is less than $200')[1]
//100

我刪除了\s後面的?,否則它只匹配第一個數字,因為空格是可選的

所以,?根據位置的不同意義不同,因為它既可以是一個量詞,又可以是一個懶惰模式指示符。

先行斷言:根據後面的字符串進行匹配

使用?=來匹配一個字符串後面跟著特定子字符串:

1
/Roger(?=Waters)/

?!執行相反的操作,如果字符串後跟著特定子字符串,則進行匹配:

1
/Roger(?!Waters)/

後行斷言:根據前面的字符串進行匹配

這是一個ES2018功能。

先行斷言使用?=符號。後行斷言使用?<=

1
/(?<=Roger) Waters/

/(?<=Roger) Waters/.test('Pink Waters is my dog') // false
/(?<=Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') // true

透過使用?<!進行否定操作,則匹配字符串以特定子字符串開頭:

1
/(?<!Roger) Waters/
1
2
/(?<!Roger) Waters/.test('Pink Waters is my dog') // true
/(?<!Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') // false

正則表達式和Unicode

在處理Unicode字符串時,使用u標誌是強制性的,特別是當您可能需要處理平面字符串時。它們沒有包含在頭1600個Unicode字符中。

例如表情符號,但不僅限於此。

如果不添加該標誌,則該簡單的正則表達式,應該匹配一個字符,將無法正常工作,因為對於JavaScript而言,該表情符號由2個字符(請參閱JavaScript中的Unicode)內部表示。

因此,請始終使用u標誌。

就像普通字符一樣,Unicode也可以處理範圍:

1
2
3
4
5
/[a-z]/.test('a')      // ✅
/[1-9]/.test('1') // ✅

/[🐶-🦊]/u.test('🐺') // ✅
/[🐶-🦊]/u.test('🐛') // ❌

JavaScript檢查內部代碼表示,因此🐶 < 🐺 < 🦊,因為\u1F436 < \u1F43A < \u1F98A。請查看完整的Emoji列表以獲取這些代碼,並了解其順序(提示:macOS的Emoji選擇器中的某些表情符號按順序混合排列,不要依賴它)。

Unicode屬性逃逸

正如我們上面所見,您可以使用\d來匹配任何數字,\s來匹配任何非空格字符,\w來匹配任何字母字符,等等。

Unicode屬性逃逸是一個ES2018功能,它將此概念擴展到所有Unicode字符上,引入了\p{}及其否定形式\P{}

任何Unicode字符都有一組屬性。例如,Script確定語言系列,ASCII是一個boolean,對於ASCII字符為true,等等。您可以將此屬性放在大括號相對應的括號中,並且正則表達式在檢查屬性為真時進行測試:

1
2
3
/^\p{ASCII}+$/u.test('abc')          // ✅
/^\p{ASCII}+$/u.test('ABC@') // ✅
/^\p{ASCII}+$/u.test('ABC🙃') // ❌

ASCII_Hex_Digit是另一個布爾屬性,用於檢查字符串是否僅包含有效的十六進制數字:

1
2
/^\p{ASCII\_Hex\_Digit}+$/u.test('0123456789ABCDEF')  // ✅
/^\p{ASCII\_Hex\_Digit}+$/u.test('h') // ❌

還有許多其他布爾屬性,只需在大括號內添加屬性名稱即可檢查它們,包括UppercaseLowercaseWhite_SpaceAlphabeticEmoji等等:

1
2
3
4
5
/^\p{Lowercase}$/u.test('h')                           // ✅
/^\p{Uppercase}$/u.test('H') // ✅

/^\p{Emoji}+$/u.test('H') // ❌
/^\p{Emoji}+$/u.test('🙃🙃') // ✅

除了這些二進制屬性外,您還可以檢查任何Unicode字符的任何屬性以匹配特定值。在此示例中,我檢查字符串是否是希臘字母或拉丁字母:

1
2
/^\p{Script=Greek}+$/u.test('ελληνικά')    // ✅
/^\p{Script=Latin}+$/u.test('hey') // ✅

詳細了解所有可用的屬性,您可以直接在TC39提案上了解詳細信息

示例

從字符串中提取數字

假設您需要提取一個數字,字符串中只包含數字,可以使用/\d+/

1
'Test 123123329'.match(/\d+/)        // Array [ "123123329" ]

匹配電子郵件地址

一種簡單的方法是檢查@符號之前和之後的非空格字符,使用\S

1
2
3
4
/(\S+)@(\S+)\.(\S+)/

/(\S+)@(\S+)\.(\S+)/.exec('[[email protected]](/cdn-cgi/l/email-protection)')
//["[[email protected]](/cdn-cgi/l/email-protection)", "copesc", "gmail", "com"]

然而,這只是一個簡化的示例,因為許多無效的郵件地址仍會滿足該正則表達式。

捕獲雙引號之間的文本

假設您有一個字符串包含雙引號內的內容,並且您希望提取該內容。

最好的方法是使用一個捕獲分組,因為我們知道匹配從"開始並以"結束的字符,並且我們可以輕鬆地將其定位,但我們還希望從結果中刪除這些引號。

我們將在result[1]中找到我們需要的內容:

1
2
3
const hello = 'Hello "nice flower"'
const result = /"([^']\*)"/.exec(hello)
//Array [ "\"nice flower\"", "nice flower" ]

獲取HTML標籤內的內容

例如,獲取span標籤內的內容,允許標籤內部有任意數量的參數:

1
2
3
4
5
6
7
8
/<span\b[^>]\*>(.*\?)<\/span>/

/<span\b[^>]\*>(.*\?)<\/span>/.exec('test')
// null
/<span\b[^>]\*>(.*\?)<\/span>/.exec('<span>test</span>')
// ["<span>test</span>", "test"]
/<span\b[^>]\*>(.*\?)<\/span>/.exec('<span class="x">test</span>')
// ["<span class="x">test</span>", "test"]

tags: [“正則表達式”, “JavaScript”, “字符串處理”, “編程”, “Unicode”]