算法精講–正則表達式(二):分組、引用與高級匹配技術 🚀
正則表達式的真正力量在于組合使用各種語法元素,創造出強大而精確的匹配模式!
—— 作者:無限大
推薦閱讀時間:25 分鐘
適用人群:已掌握基礎正則語法,希望提升實戰能力的開發者
引言:從基礎到組合的飛躍 🚀
在《算法精講–正則表達式(一)》中,我們學習了正則表達式的基礎知識:字符匹配、量詞和位置匹配。這些是構建正則表達式的基石,但真正讓正則表達式發揮強大威力的是組合使用這些基礎元素。
想象一下,基礎語法就像字母表中的字母,而組合技巧則是將這些字母組成單詞、句子和段落的語法規則。只有掌握了組合技巧,你才能真正用正則表達式解決復雜的文本處理問題。
本章將帶你探索正則表達式的組合藝術–分組與引用,讓你從正則表達式的初學者蛻變為匹配大師!
分組與引用:結構化匹配的藝術 🔄
1.1 什么是分組?為什么需要分組?
分組(Grouping) 是正則表達式中用于將多個字符或子表達式組合為一個邏輯單元的機制,通過圓括號 ()
實現。分組的主要作用包括:
- 邏輯組合:將多個元素視為一個整體進行操作(如應用量詞)
- 捕獲數據:提取匹配結果中的特定部分
- 模式分支:通過
|
實現多個可選模式 - 引用匹配:在表達式內部或替換操作中引用已匹配的內容
沒有分組,正則表達式將無法處理復雜的結構化文本匹配。想象一下,如果不能將 ab
組合為一個整體,我們將無法匹配重復出現的 ab
序列。
1.2 分組的類型
1.2.1 捕獲組:提取匹配的子串 🎯
捕獲組(Capturing Group) 是正則表達式中最強大的組合工具之一,它允許你將匹配的部分內容保存到臨時變量中,以便后續使用。使用圓括號 ()
可以創建捕獲組。
基本語法與工作原理
(表達式)例:ab+c,只能匹配abc、abbc、ababcb等(ab)+c,可以匹配abc、ababc、abababc等
實用示例
示例 1:提取日期中的年、月、日
// 匹配格式為 yyyy-mm-dd 的日期
const dateRegex = /(d{4})-(d{2})-(d{2})/;
const date = "2023-12-25";
const result = date.match(dateRegex);console.log(result[0]); // 輸出: 2023-12-25 (整個匹配)
console.log(result[1]); // 輸出: 2023 (第一個捕獲組: 年)
console.log(result[2]); // 輸出: 12 (第二個捕獲組: 月)
console.log(result[3]); // 輸出: 25 (第三個捕獲組: 日)
示例 2:交換姓名順序
import re# 將 "姓, 名" 格式轉換為 "名 姓"
name = "Doe, John"
# 使用 1 和 2 引用第一個和第二個捕獲組
formatted_name = re.sub(r'(w+), (w+)', r'2 1', name)print(formatted_name) # 輸出: John Doe
1.2.2 非捕獲組:提高性能的技巧 ?
當你只需要對表達式進行分組,而不需要捕獲匹配結果時,可以使用非捕獲組(Non-capturing Group)。非捕獲組不會創建編號的引用(即不參與組計數,僅用于分組匹配,不會存儲匹配結果)從而提高性能并避免不必要的內存占用。
基本語法
(?:表達式)
實用示例
區分捕獲組與非捕獲組
import java.util.regex.Matcher;
import java.util.regex.Pattern;public class GroupExample {public static void main(String[] args) {String text = "apple,banana,orange";// 使用捕獲組Pattern capturePattern = Pattern.compile("(apple),(banana)");Matcher captureMatcher = capturePattern.matcher(text);if (captureMatcher.find()) {System.out.println("捕獲組數量: " + captureMatcher.groupCount()); // 輸出: 2}// 使用非捕獲組Pattern nonCapturePattern = Pattern.compile("(?:apple),(?:banana)");Matcher nonCaptureMatcher = nonCapturePattern.matcher(text);if (nonCaptureMatcher.find()) {System.out.println("非捕獲組數量: " + nonCaptureMatcher.groupCount()); // 輸出: 0}}
}
1.2.3 命名捕獲組:提高可讀性的高級技巧 🏷?
隨著正則表達式變得復雜,僅靠數字引用捕獲組會降低代碼的可讀性。命名捕獲組(Named Capturing Group) 允許你為捕獲組分配名稱,使正則表達式更易于理解和維護。
基本語法
(?<名稱>表達式)
引用命名捕獲組的語法因語言而異:
- 在 JavaScript 中:
k<名稱>
或$<名稱>
- 在 Python 中:
(?P=名稱)
或通過group('名稱')
方法 - 在 Java 中:
k<名稱>
或group('名稱')
方法
實用示例
使用命名捕獲組解析 URL
// 定義正則表達式,用于匹配和解析URL結構。正則表達式使用命名捕獲組(如?<protocol>)標識關鍵部分。
// ^ 表示字符串起始;(?<protocol>https?) 匹配協議(http或https,s為可選)[[2]];
// :// 是固定分隔符;(?<domain>[^/]+) 匹配域名(非斜杠字符序列,直到遇到斜杠或結束);
// (?<path>/.*)? 匹配路徑(以斜杠開頭的任意字符序列,?表示路徑可選)[[5]];$ 表示字符串結束。
const urlRegex = /^(?<protocol>https?)://(?<domain>[^/]+)(?<path>/.*)?$/;// 示例URL字符串,包含協議、域名和路徑(含查詢參數)
const url = "https://www.example.com/path/to/resource?query=1";// 使用match方法執行正則匹配,返回結果對象。result.groups屬性存儲命名捕獲組的值[[8]]。
const result = url.match(urlRegex);// 輸出協議部分:result.groups.protocol訪問命名組,匹配"https"(s被捕獲)[[2]]
console.log(result.groups.protocol); // 輸出: https// 輸出域名部分:result.groups.domain訪問命名組,匹配"www.example.com"(域名不含斜杠)[[5]]
console.log(result.groups.domain); // 輸出: www.example.com// 輸出路徑部分:result.groups.path訪問命名組,匹配"/path/to/resource?query=1"(包含查詢參數)[[9]]
console.log(result.groups.path); // 輸出: /path/to/resource?query=1
1.3 分組引用:復用匹配結果 🔄
分組引用允許你在正則表達式中或替換操作中復用前面捕獲組匹配的內容,極大增強了模式匹配的靈活性和強大性。主要包括以下幾種類型:
1.3.1 反向引用:匹配重復文本
反向引用(Backreference) 允許你引用前面捕獲組匹配的內容,這對于匹配重復出現的文本模式特別有用。
基本語法:
- 數字反向引用:
\n
(n 是捕獲組編號) - 命名反向引用:
\k<名稱>
或(?P=name)
,后面的內容是 py 專屬語法
示例 1:匹配重復的單詞
// 定義待檢測文本(包含重復單詞 "is is" 和 "test test")
const text = "This is is a test test.";// 正則表達式:\b(\w+)\s+\1\b
// - \b:單詞邊界,確保匹配完整單詞
// - (\w+):捕獲組1,匹配一個或多個字母/數字/下劃線(單詞內容)
// - \s+:匹配一個或多個空白字符(如空格)
// - \1:反向引用,指向捕獲組1匹配的內容(要求后續內容與組1完全相同)
// - g:全局匹配模式
const duplicateRegex = /\b(\w+)\s+\1\b/g;// 執行匹配:返回所有符合正則的子串數組
const result = text.match(duplicateRegex);
console.log(result); // 輸出: [ 'is is', 'test test' ]
示例 2:匹配 HTML 標簽對
import re# 定義 HTML 字符串(含成對的 div 和 p 標簽)
html = "<div>這是一個 div 標簽</div><p>這是一個 p 標簽</p>"# 正則表達式:<(?P<tag>\w+)>.*?</(?P=tag)>
# - (?P<tag>\w+):命名捕獲組 "tag",匹配標簽名(如 "div")
# - .*?:非貪婪匹配標簽間任意內容
# - (?P=tag):命名反向引用,要求結束標簽名與 "tag" 組相同
pattern = r'<(?P<tag>\w+)>.*?</(?P=tag)>'# 執行匹配:返回所有標簽名的列表(僅返回捕獲組內容)
matches = re.findall(pattern, html)
print(matches) # 輸出: ['div', 'p']
1.3.2 替換引用:在替換中使用捕獲內容
在替換操作中,可以使用特殊語法引用捕獲組的內容,實現文本轉換和重組。
不同語言中的替換引用語法:
- JavaScript:
$n
或${n}
(數字引用),$<name>
(命名引用) - Python:
n
(數字引用),(?P=name)
(命名引用) - Java:
$n
(數字引用),${name}
(命名引用)
示例:格式化日期
// 將 yyyy-mm-dd 格式轉換為 mm/dd/yyyy 格式
const date = "2023-12-25";
// 使用 $2 和 $3 引用月和日,$1 引用年
const formattedDate = date.replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1");console.log(formattedDate); // 輸出: 12/25/2023
1.4 分組的高級應用 🚀
條件表達式:基于捕獲組的條件匹配 🧩
條件表達式(Conditional Expression) 允許根據前面的捕獲組是否匹配來決定應用哪個模式。
基本語法:
(?(組號或名稱)匹配成功時的模式|匹配失敗時的模式)
示例:處理可選的引號
import retext = 'name=John name="Doe"'# 正則表達式分解:
# 1. name= : 匹配字面量
# 2. (?:...) : 非捕獲組,表示兩種匹配可能
# 3. "(?P<quoted>[^"]+)" : 帶引號的情況
# - " : 匹配開頭的引號
# - (?P<quoted>[^"]+) : 命名捕獲組,匹配除"外的任意字符(至少1個)
# - " : 匹配結尾的引號
# 4. | : 或邏輯
# 5. (?P<unquoted>\w+) : 不帶引號的情況
# - \w+ : 匹配字母/數字/下劃線(至少1個)
pattern = r'name=(?:"(?P<quoted>[^"]+)"|(?P<unquoted>\w+))'# 使用finditer獲取所有匹配的迭代器對象(保持匹配順序)
matches = re.finditer(pattern, text)for match in matches:# 優先檢查帶引號的分組(因為正則中帶引號的模式在前)if match.group('quoted'):# 當quoted分組有匹配內容時,輸出帶引號的值print(f'帶引號的值: {match.group("quoted")}') # 注意使用雙引號避免沖突else:# 當unquoted分組有匹配內容時,輸出普通值print(f'不帶引號的值: {match.group("unquoted")}')# 輸出邏輯說明:
# 第一個匹配 name=John → 觸發unquoted分組
# 第二個匹配 name="Doe" → 觸發quoted分組(Doe被[^"]+捕獲)
總結:掌握分組技術,提升正則表達式能力 🚀
通過本章學習,我們深入探討了正則表達式的分組與引用技術,包括:
- 基礎分組:使用
()
創建捕獲組,提取匹配的子串 - 性能優化:使用
(?:)
非捕獲組減少內存占用 - 可讀性提升:使用命名捕獲組
(?<name>)
增強代碼可維護性 - 高級應用:掌握反向引用、條件表達式等高級技巧
正則表達式的分組技術是處理復雜文本模式的核心工具。熟練掌握這些技巧,能夠讓你輕松解決各種文本處理難題,從簡單的數據提取到復雜的嵌套結構分析。
正則表達式是程序員的瑞士軍刀,而分組技術則是這把軍刀中最鋒利的刀刃之一!🔪