正则表达式
正则表达式
正则表达式(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}?: 非贪婪匹配n到m次。
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: 313. 非捕获组 (?:...) 的作用
有时候,你只是想用括号来组合一部分模式(例如,为了对它使用量词 + 或 ?),但你并不关心这部分匹配到的内容,也不想让它创建一个捕获组。这时就应该使用非捕获组 (?:...)。
为什么使用非捕获组?
- 性能优化: 创建捕获组会消耗额外的内存和处理时间。如果不需要捕获,使用非捕获组可以提升匹配效率。
- 避免混淆: 当正则表达式中有很多括号时,使用非捕获组可以让你只关注真正需要提取的部分,避免产生一堆无用的捕获组编号。
示例对比:
假设你想匹配 “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+$ | 匹配带小数点的数字。 |