正则表达式

正则表达式

正则表达式(Regular Expression,简称 regex 或 regexp)是一种强大的文本处理工具,它使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。它被广泛用于字符串的搜索、替换、验证和提取。

正则表达式验证工具:

1. 基本概念

  • 模式 (Pattern): 正则表达式本身,由特殊字符(元字符)和普通字符组成。
  • 匹配 (Match): 检查一个字符串是否符合正则表达式描述的规则。
  • 引擎 (Engine): 解析和执行正则表达式的软件组件。不同语言和工具的引擎可能在细节上有所差异。

2. 核心符号与语法

2.1. 字符匹配

符号含义示例
.匹配除换行符 \n 之外的任意单个字符。a.b 匹配 “acb”, “a_b”, 但不匹配 “ab” 或 “a\nb”。
\d匹配任意一个数字,等价于 [0-9]\d{3} 匹配 “123”。
\w匹配任意一个字母、数字或下划线,等价于 [a-zA-Z0-9_]\w+ 匹配 “hello_123”。
\s匹配任意一个空白字符,包括空格、制表符、换行符等。hello\sworld 匹配 “hello world”。
\D匹配任意一个非数字字符。\D 匹配 “a”, “_”, " “。
\W匹配任意一个非字母、数字或下划线字符。\W 匹配 “@”, “#”, “!"。
\S匹配任意一个非空白字符。\S+ 匹配 “non-space”。
[...]字符集,匹配方括号中包含的任意一个字符。[aeiou] 匹配任意一个小写元音字母。
[^...]否定字符集,匹配任意一个 在方括号中的字符。[^0-9] 匹配任意一个非数字字符。
a-z在字符集中表示范围。[a-z] 匹配所有小写字母。[0-9a-fA-F] 匹配一个十六进制数。

2.2. 量词 (Quantifiers)

量词用于指定一个模式需要匹配的次数。

符号含义示例
*匹配前一个元素零次或多次。ab*c 匹配 “ac”, “abc”, “abbbc”。
+匹配前一个元素一次或多次。ab+c 匹配 “abc”, “abbc”, 但不匹配 “ac”。
?匹配前一个元素零次或一次。colou?r 匹配 “color” 和 “colour”。
{n}匹配前一个元素恰好 n 次。\d{4} 匹配一个四位数,如 “2024”。
{n,}匹配前一个元素至少 n 次。\d{2,} 匹配两位或更多位的数字。
{n,m}匹配前一个元素至少 n 次,至多 m 次。\w{3,5} 匹配长度为 3 到 5 的单词字符。

默认情况下,量词是 贪婪的 (Greedy),即尽可能多地匹配。在量词后加上 ? 可以使其变为 非贪婪 (Non-greedy)懒惰 (Lazy) 模式,即尽可能少地匹配。

  • *?: 非贪婪匹配零次或多次。
  • +?: 非贪婪匹配一次或多次。
  • ??: 非贪婪匹配零次或一次。
  • {n,}?: 非贪婪匹配至少 n 次。
  • {n,m}?: 非贪婪匹配 nm 次。

2.3. 分组与捕获

符号含义示例
(...)捕获分组。将多个字符作为一个整体,并捕获这部分内容以备后用。(ab)+ 匹配 “ab”, “abab”。 (\d{4})-(\d{2}) 可以分别捕获年份和月份。
(?:...)非捕获分组。只组合,不捕获内容,性能稍好。(?:ab)+ 匹配 “abab”,但不创建捕获组。
\1, \2反向引用。引用前面捕获组匹配到的内容。(\w+)\s+\1 匹配重复的单词,如 “hello hello”。

深入理解捕获组

捕获组是正则表达式中最强大的功能之一,它允许你从匹配的文本中提取出特定的子字符串。当你用括号 () 包围正则表达式的一部分时,你就创建了一个捕获组。

1. 捕获组的编号

每个捕获组都会被分配一个从 1 开始的编号。编号的顺序是根据开括号 ( 在正则表达式中出现的从左到右的顺序决定的。

  • 组 0: 代表整个正则表达式匹配到的完整内容。
  • 组 1, 2, 3…: 代表第一个、第二个、第三个…捕获组匹配到的内容。

示例:

对于正则表达式 (\d{4})-(\d{2})-(\d{2}) 和输入字符串 "2025-10-31":

  • 组 0: "2025-10-31" (整个匹配)
  • 组 1: "2025" (由第一个 (\d{4}) 捕获)
  • 组 2: "10" (由第二个 (\d{2}) 捕获)
  • 组 3: "31" (由第三个 (\d{2}) 捕获)

在编程语言中,你可以通过这些编号来访问提取出的数据,这在解析日期、URL、日志等结构化文本时非常有用。

2. 命名捕获组 (Named Capture Groups)

为了让正则表达式更具可读性和可维护性,现代正则引擎支持命名捕获组。语法是 (?<name>...)(?'name'...) (不同语言稍有差异)。

示例:

使用命名捕获组重写上面的日期正则: (?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})

现在,你可以通过名称(如 “year”, “month”, “day”)而不是编号来获取捕获的内容,这使得代码更加清晰。

import re
text = "Date: 2025-10-31"
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})" # Python uses ?P<name>
match = re.search(pattern, text)
if match:
    print(f"Year: {match.group('year')}")   # -> Year: 2025
    print(f"Month: {match.group('month')}") # -> Month: 10
    print(f"Day: {match.group('day')}")     # -> Day: 31

3. 非捕获组 (?:...) 的作用

有时候,你只是想用括号来组合一部分模式(例如,为了对它使用量词 +?),但你并不关心这部分匹配到的内容,也不想让它创建一个捕获组。这时就应该使用非捕获组 (?:...)

为什么使用非捕获组?

  • 性能优化: 创建捕获组会消耗额外的内存和处理时间。如果不需要捕获,使用非捕获组可以提升匹配效率。
  • 避免混淆: 当正则表达式中有很多括号时,使用非捕获组可以让你只关注真正需要提取的部分,避免产生一堆无用的捕获组编号。

示例对比:

假设你想匹配 “file1.txt”, “file2.txt” 等,但不想捕获 “file”。

  • 使用捕获组: (file)\d+\.txt
    • "file1.txt" 中,组 1 会捕获 "file"。这是一个不必要的捕获。
  • 使用非捕获组: (?:file)\d+\.txt
    • 这会同样匹配 "file1.txt",但不会创建任何捕获组,效率更高,意图也更清晰。

总结:

  • 当你需要 提取 字符串的某个部分时,使用 捕获组 (...)
  • 当你只需要 组合 模式而 不需要提取 时,使用 非捕获组 (?:...)

2.4. 断言 (Assertions)

断言也称为“零宽度断言”,它们只匹配位置,不消耗任何字符。

符号含义示例
^匹配字符串的开头。^A 匹配以 “A” 开头的字符串。
$匹配字符串的结尾。z$ 匹配以 “z” 结尾的字符串。
\b单词边界。匹配单词的开头或结尾。\bcat\b 匹配独立的单词 “cat”,但不匹配 “category”。
\B非单词边界\Bcat\B 匹配 “category” 中的 “cat”。
(?=...)正向先行断言。要求当前位置后面的内容匹配 ...,但不消耗内容。`Windows(?= 95
(?!...)负向先行断言。要求当前位置后面的内容 匹配 ...`Windows(?! 95

3. 在不同语言中使用

3.1. Python

Python 通过 re 模块提供正则表达式支持。

import re

text = "My email is example@example.com"
pattern = r"\w+@\w+\.\w+"  # 使用原始字符串 r"..." 避免反斜杠问题

# 查找第一个匹配项
match = re.search(pattern, text)
if match:
    print(f"Found: {match.group(0)}")  # 输出: Found: example@example.com

# 查找所有匹配项
emails = re.findall(pattern, text)
print(f"All emails: {emails}") # 输出: All emails: ['example@example.com']

# 替换
new_text = re.sub(pattern, "[REDACTED]", text)
print(new_text)  # 输出: My email is [REDACTED]

3.2. C++

C++11 引入了 <regex> 头文件。

#include <iostream>
#include <string>
#include <regex>

int main() {
    std::string text = "My email is example@example.com";
    std::regex pattern("(\\w+)@(\\w+)\\.(\\w+)"); // C++中反斜杠需要双写

    std::smatch match;
    if (std::regex_search(text, match, pattern)) {
        std::cout << "Full match: " << match[0] << std::endl; // Full match: example@example.com
        std::cout << "User: " << match[1] << std::endl;       // User: example
        std::cout << "Domain: " << match[2] << std::endl;     // Domain: example
    }

    // 替换
    std::string new_text = std::regex_replace(text, pattern, "[REDACTED]");
    std::cout << new_text << std::endl; // My email is [REDACTED]

    return 0;
}

3.3. TypeScript / JavaScript

TypeScript 直接使用 JavaScript 的 RegExp 对象。

const text = "My email is example@example.com";
const pattern = /\w+@\w+\.\w+/g; // g 是全局匹配标志

// 查找匹配
const match = text.match(pattern);
console.log(match); // 输出: ["example@example.com"]

// 测试是否存在匹配
const hasEmail = pattern.test(text);
console.log(hasEmail); // true (注意:由于 g 标志和 lastIndex,多次调用 test 需重置)

// 替换
const newText = text.replace(pattern, "[REDACTED]");
console.log(newText); // 输出: My email is [REDACTED]

// 捕获组
const complexPattern = /(\w+)@(\w+)\.(\w+)/;
const parts = text.match(complexPattern);
if (parts) {
    console.log(`Full: ${parts[0]}, User: ${parts[1]}, Domain: ${parts[2]}`);
    // Full: example@example.com, User: example, Domain: example
}

4. 常见正则表达式示例

用途正则表达式说明
邮箱^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$一个相对通用且实用的邮箱验证表达式。
URL^(https?ftp)://[^\s/$.?#].[^\s]*$匹配 http, https, ftp 协议的 URL。
电话号码 (中国大陆)^1[3-9]\d{9}$匹配以1开头,第二位是3到9,后面跟9个数字的手机号。
日期 (YYYY-MM-DD)^\d{4}-(0[1-9]1[0-2])-(0[1-9][12]\d3[01])$匹配 YYYY-MM-DD 格式的日期,对月份和日期做了基本范围限制。
IP 地址 (IPv4)^((25[0-5]2[0-4]\d1\d\d[1-9]?\d)\.){3}(25[0-5]2[0-4]\d1\d\d[1-9]?\d)$匹配 IPv4 地址,对每个数字段的范围 (0-255) 做了精确限制。
整数^-?\d+$匹配正整数、负整数和零。
浮点数^-?\d+\.\d+$匹配带小数点的数字。