一、什么是正则表达式?
正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于描述字符串匹配模式的工具。它就像是一种专门用来"找东西"的迷你语言。
正则表达式能做什么?
- 验证:检查输入是否符合特定格式(邮箱、手机号、身份证)
- 搜索:在文本中查找特定模式的内容
- 替换:批量替换符合模式的文本
- 提取:从文本中抓取需要的信息
一个简单的例子:
`文本:我的电话是 138-1234-5678,备用号码是 139-8765-4321
正则:\d{3}-\d{4}-\d{4}
结果:匹配到 "138-1234-5678" 和 "139-8765-4321"`
二、基础语法
2.1 普通字符
最简单的正则就是普通字符,它们匹配自身:
`正则:cat
文本:The cat sat on the mat.
匹配: ^^^`
2.2 元字符(特殊字符)
元字符是正则表达式的灵魂,它们有特殊含义:
| 元字符 | 含义 | 示例 | 匹配 |
|---|---|---|---|
. | 匹配任意单个字符(除换行符) | c.t | cat, cot, cut, c@t |
^ | 匹配字符串开头 | ^Hello | Hello开头的字符串 |
$ | 匹配字符串结尾 | world$ | world结尾的字符串 |
* | 前一个字符出现0次或多次 | ab*c | ac, abc, abbc, abbbc |
+ | 前一个字符出现1次或多次 | ab+c | abc, abbc, abbbc |
? | 前一个字符出现0次或1次 | colou?r | color, colour |
\ | 转义特殊字符 | \. | 匹配实际的点号 |
| ` | ` | 或(alternation) | `cat |
2.3 字符类 []
方括号定义一个字符集合,匹配其中任意一个字符:
`[abc] 匹配 a、b 或 c
[a-z] 匹配任意小写字母
[A-Z] 匹配任意大写字母
[0-9] 匹配任意数字
[a-zA-Z] 匹配任意字母
1 匹配除了 a、b、c 以外的字符(^ 在 [] 内表示取反)`
实例:
`正则:[aeiou]
文本:Hello World
匹配: ^ ^ ^ (匹配 e, o, o)`
2.4 预定义字符类
为了方便,正则提供了一些预定义的字符类:
| 符号 | 等价于 | 含义 |
|---|---|---|
\d | [0-9] | 数字 |
\D | [^0-9] | 非数字 |
\w | [a-zA-Z0-9_] | 单词字符 |
\W | [^a-zA-Z0-9_] | 非单词字符 |
\s | [ \t\n\r\f\v] | 空白字符 |
\S | [^ \t\n\r\f\v] | 非空白字符 |
实例:
`正则:\d\d\d-\d\d\d\d
文本:电话:021-1234
匹配: ^^^^^^^^`
2.5 量词
量词控制前面的元素重复多少次:
| 量词 | 含义 | 示例 |
|---|---|---|
* | 0次或多次 | a* → "", a, aa, aaa |
+ | 1次或多次 | a+ → a, aa, aaa |
? | 0次或1次 | a? → "", a |
{n} | 恰好n次 | a{3} → aaa |
{n,} | 至少n次 | a{2,} → aa, aaa, aaaa |
{n,m} | n到m次 | a{2,4} → aa, aaa, aaaa |
实例:
`正则:\d{3}-\d{4}-\d{4}
文本:手机号:138-1234-5678
匹配: ^^^^^^^^^^^^^`
2.6 贪婪与懒惰匹配
默认情况下,量词是贪婪的(尽可能多匹配):
`正则:<.*>
文本:
匹配:^^^^^^^^^^^^^^^^ (匹配整个字符串)`
在量词后加 ? 变成懒惰模式(尽可能少匹配):
`正则:<.*?>
文本:
匹配:^^^^^ ^^^^^ (分别匹配
三、分组与引用
3.1 捕获分组 ()
圆括号创建捕获组,可以:
- 将多个字符作为一个单元
- 捕获匹配的内容供后续使用
`正则:(ab)+
文本:abababc
匹配:^^^^^^ (匹配 ababab,ab作为整体重复)
正则:(\d{4})-(\d{2})-(\d{2})
文本:2024-03-15
分组1:2024
分组2:03
分组3:15`
3.2 非捕获分组 (?:)
只分组不捕获,用于纯粹的逻辑分组:
`正则:(?:ab)+c
文本:abababc
匹配:^^^^^^^ (匹配但不捕获 ab)`
3.3 反向引用
使用 \1、\2 等引用前面捕获的内容:
`正则:(\w+)\s+\1
文本:hello hello world
匹配:^^^^^^^^^^^ (匹配重复的单词 "hello hello")`
3.4 命名分组
给分组起名字,提高可读性:
`正则:(?P
文本:2024-03-15
Python中访问
match.group('year') # '2024'
match.group('month') # '03'
match.group('day') # '15'`
四、断言(零宽度匹配)
断言匹配位置,不消耗字符。
4.1 边界断言
| 断言 | 含义 | 示例 |
|---|---|---|
^ | 字符串/行开头 | ^Start |
$ | 字符串/行结尾 | End$ |
\b | 单词边界 | \bword\b |
\B | 非单词边界 | \Bword\B |
单词边界示例:
`正则:\bcat\b
文本:The cat scattered cats.
匹配: ^^^ (只匹配独立的 "cat")
正则:cat
文本:The cat scattered cats.
匹配: ^^^ ^^^ ^^^ (匹配所有包含 cat 的地方)`
4.2 环视断言(Lookaround)
环视断言检查某个位置的前后是否满足条件,但不消耗字符:
| 断言 | 名称 | 含义 |
|---|---|---|
(?=...) | 正向肯定预查 | 后面必须匹配... |
(?!...) | 正向否定预查 | 后面不能匹配... |
(?<=...) | 反向肯定预查 | 前面必须匹配... |
(?<!...) | 反向否定预查 | 前面不能匹配... |
示例:
`# 匹配后面跟着 "元" 的数字
正则:\d+(?=元)
文本:价格是100元,编号是200
匹配: ^^^ (只匹配 100)
匹配前面是 "$" 的数字
正则:(?<=$)\d+
文本:$100 and 200
匹配: ^^^ (只匹配 100)
匹配不以 "test_" 开头的函数名
正则:\b(?!test_)\w+(
文本:test_func() real_func()
匹配: ^^^^^^^^^^`
五、常用正则表达式模式
5.1 数据验证
# 邮箱地址
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
# 中国手机号
^1[3-9]\d{9}$
# 中国身份证(18位)
^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$
# URL
^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$
# IPv4地址
^((25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)$
# 强密码(至少8位,包含大小写字母和数字)
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$
# 中文字符
[\u4e00-\u9fa5]
# 日期格式 YYYY-MM-DD
^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$
# 时间格式 HH:MM:SS
^([01]\d|2[0-3]):([0-5]\d):([0-5]\d)$5.2 文本处理
# 匹配HTML标签
<[^>]+>
# 提取HTML标签内容
<(\w+)[^>]*>(.*?)<\/\1>
# 匹配空白行
^\s*$
# 去除首尾空白(用于替换)
^\s+|\s+$
# 匹配重复单词
\b(\w+)\s+\1\b
# 匹配引号内的内容
"([^"]*)"
'([^']*)'
# 驼峰转下划线
(?<!^)(?=[A-Z])
# 替换为 _,然后转小写`
### 5.3 代码处理
python
`# 单行注释 //
\/\/.*$
# 多行注释 /* */
\/\*[\s\S]*?\*\/
# Python/Shell 注释 #
#.*$
# 匹配函数定义(简化版)
def\s+(\w+)\s*\(([^)]*)\): # Python
function\s+(\w+)\s*\(([^)]*)\) # JavaScript
func\s+(\w+)\s*\(([^)]*)\) # Go
# 匹配import语句
^import\s+.+$
^from\s+\S+\s+import\s+.+$六、各编程语言中的使用
6.1 Python
import re
text = "我的邮箱是 test@example.com,电话是 138-1234-5678"
# 查找第一个匹配
match = re.search(r'\d{3}-\d{4}-\d{4}', text)
if match:
print(match.group()) # 138-1234-5678
# 查找所有匹配
phones = re.findall(r'\d{3}-\d{4}-\d{4}', text)
print(phones) # ['138-1234-5678']
# 替换
new_text = re.sub(r'\d{3}-\d{4}-\d{4}', '***-****-****', text)
print(new_text) # 我的邮箱是 test@example.com,电话是 ***-****-****
# 分割
parts = re.split(r'[,,]', text)
print(parts) # ['我的邮箱是 test@example.com', '电话是 138-1234-5678']
# 编译正则(提高重复使用效率)
pattern = re.compile(r'(?P<name>\w+)@(?P<domain>[\w.]+)')
match = pattern.search(text)
if match:
print(match.group('name')) # test
print(match.group('domain')) # example.com
# 常用标志
re.IGNORECASE # 或 re.I,忽略大小写
re.MULTILINE # 或 re.M,多行模式,^ $ 匹配每行
re.DOTALL # 或 re.S,让 . 匹配换行符6.2 JavaScript
const text = "我的邮箱是 test@example.com,电话是 138-1234-5678";
// 测试是否匹配
const phonePattern = /\d{3}-\d{4}-\d{4}/;
console.log(phonePattern.test(text)); // true
// 查找第一个匹配
const match = text.match(/\d{3}-\d{4}-\d{4}/);
console.log(match[0]); // 138-1234-5678
// 查找所有匹配(使用 g 标志)
const allMatches = text.match(/\d+/g);
console.log(allMatches); // ['138', '1234', '5678']
// 替换
const newText = text.replace(/\d{3}-\d{4}-\d{4}/, '***-****-****');
console.log(newText);
// 替换所有(使用 g 标志)
const cleaned = "a1b2c3".replace(/\d/g, '');
console.log(cleaned); // abc
// 分割
const parts = text.split(/[,,]/);
console.log(parts);
// 捕获组
const datePattern = /(\d{4})-(\d{2})-(\d{2})/;
const dateMatch = "2024-03-15".match(datePattern);
console.log(dateMatch[1], dateMatch[2], dateMatch[3]); // 2024 03 15
// 命名捕获组(ES2018+)
const namedPattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const namedMatch = "2024-03-15".match(namedPattern);
console.log(namedMatch.groups.year); // 2024
// 常用标志
// g - 全局匹配
// i - 忽略大小写
// m - 多行模式
// s - dotAll模式(. 匹配换行)
// u - Unicode模式6.3 Go
package main
import (
"fmt"
"regexp"
)
func main() {
text := "我的邮箱是 test@example.com,电话是 138-1234-5678"
// 编译正则表达式
phonePattern := regexp.MustCompile(`\d{3}-\d{4}-\d{4}`)
// 查找第一个匹配
match := phonePattern.FindString(text)
fmt.Println(match) // 138-1234-5678
// 查找所有匹配
allDigits := regexp.MustCompile(`\d+`)
matches := allDigits.FindAllString(text, -1)
fmt.Println(matches) // [138 1234 5678]
// 测试是否匹配
matched := phonePattern.MatchString(text)
fmt.Println(matched) // true
// 替换
newText := phonePattern.ReplaceAllString(text, "***-****-****")
fmt.Println(newText)
// 捕获组
datePattern := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
dateText := "日期是 2024-03-15"
groups := datePattern.FindStringSubmatch(dateText)
if groups != nil {
fmt.Printf("年: %s, 月: %s, 日: %s\n", groups[1], groups[2], groups[3])
}
// 命名捕获组
namedPattern := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
names := namedPattern.SubexpNames()
matches2 := namedPattern.FindStringSubmatch(dateText)
for i, match := range matches2 {
fmt.Printf("%s: %s\n", names[i], match)
}
// 分割
splitPattern := regexp.MustCompile(`[,,]`)
parts := splitPattern.Split(text, -1)
fmt.Println(parts)
}6.4 Java
import java.util.regex.*;
import java.util.*;
public class RegexDemo {
public static void main(String[] args) {
String text = "我的邮箱是 test@example.com,电话是 138-1234-5678";
// 编译正则
Pattern phonePattern = Pattern.compile("\\d{3}-\\d{4}-\\d{4}");
Matcher matcher = phonePattern.matcher(text);
// 查找第一个匹配
if (matcher.find()) {
System.out.println(matcher.group()); // 138-1234-5678
}
// 查找所有匹配
Pattern digitPattern = Pattern.compile("\\d+");
Matcher digitMatcher = digitPattern.matcher(text);
List<String> matches = new ArrayList<>();
while (digitMatcher.find()) {
matches.add(digitMatcher.group());
}
System.out.println(matches); // [138, 1234, 5678]
// 测试是否匹配
boolean isMatch = Pattern.matches(".*\\d{3}-\\d{4}-\\d{4}.*", text);
System.out.println(isMatch); // true
// 替换
String newText = text.replaceAll("\\d{3}-\\d{4}-\\d{4}", "***-****-****");
System.out.println(newText);
// 捕获组
Pattern datePattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher dateMatcher = datePattern.matcher("日期是 2024-03-15");
if (dateMatcher.find()) {
System.out.printf("年: %s, 月: %s, 日: %s%n",
dateMatcher.group(1), dateMatcher.group(2), dateMatcher.group(3));
}
// 命名捕获组 (Java 7+)
Pattern namedPattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher namedMatcher = namedPattern.matcher("2024-03-15");
if (namedMatcher.find()) {
System.out.println("Year: " + namedMatcher.group("year"));
}
// 分割
String[] parts = text.split("[,,]");
System.out.println(Arrays.toString(parts));
}
}七、实战案例
案例1:日志分析
从Nginx日志中提取IP地址和请求路径:
import re
log_line = '192.168.1.100 - - [15/Mar/2024:10:15:32 +0800] "GET /api/users HTTP/1.1" 200 1234'
pattern = r'(?P<ip>\d+\.\d+\.\d+\.\d+).*?"(?P<method>\w+)\s+(?P<path>\S+)'
match = re.search(pattern, log_line)
if match:
print(f"IP: {match.group('ip')}") # 192.168.1.100
print(f"Method: {match.group('method')}") # GET
print(f"Path: {match.group('path')}") # /api/users案例2:数据清洗
清理用户输入的电话号码:
import re
def clean_phone(phone):
"""将各种格式的电话号码统一为纯数字"""
# 移除所有非数字字符
digits = re.sub(r'\D', '', phone)
# 如果是11位且以1开头,认为是有效手机号
if re.match(r'^1[3-9]\d{9}$', digits):
return digits
return None
# 测试
phones = [
"138-1234-5678",
"138 1234 5678",
"(138)12345678",
"13812345678",
"+86 138-1234-5678",
"12345678", # 无效
]
for p in phones:
result = clean_phone(p)
print(f"{p:25} -> {result}")案例3:提取网页数据
从HTML中提取链接:
import re
html = '''
<html>
<body>
<a href="https://example.com/page1">Page 1</a>
<a href="https://example.com/page2" class="active">Page 2</a>
<a href="/relative/path">Relative</a>
</body>
</html>
'''
# 提取所有href属性
pattern = r'href=["\']([^"\']+)["\']'
links = re.findall(pattern, html)
print(links)
# ['https://example.com/page1', 'https://example.com/page2', '/relative/path']
# 提取链接和文本
pattern2 = r'<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>'
for match in re.finditer(pattern2, html):
print(f"Link: {match.group(1)}, Text: {match.group(2)}")案例4:密码强度验证
import re
def check_password_strength(password):
"""检查密码强度,返回强度等级和建议"""
checks = {
'length': (len(password) >= 8, "至少8个字符"),
'lowercase': (bool(re.search(r'[a-z]', password)), "包含小写字母"),
'uppercase': (bool(re.search(r'[A-Z]', password)), "包含大写字母"),
'digit': (bool(re.search(r'\d', password)), "包含数字"),
'special': (bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password)), "包含特殊字符"),
}
passed = sum(1 for check, _ in checks.values() if check)
suggestions = [msg for check, msg in checks.values() if not check]
if passed == 5:
return "强", []
elif passed >= 3:
return "中", suggestions
else:
return "弱", suggestions
# 测试
passwords = ["123456", "Password1", "P@ssw0rd!", "abcABC123!@#"]
for pwd in passwords:
strength, tips = check_password_strength(pwd)
print(f"{pwd:15} -> 强度: {strength}")
if tips:
print(f" 建议: {', '.join(tips)}")案例5:批量文件重命名
import re
import os
def batch_rename(directory, pattern, replacement):
"""
批量重命名文件
示例:将 "IMG_20240315_001.jpg" 改为 "photo_2024-03-15_001.jpg"
"""
for filename in os.listdir(directory):
new_name = re.sub(pattern, replacement, filename)
if new_name != filename:
old_path = os.path.join(directory, filename)
new_path = os.path.join(directory, new_name)
print(f"Rename: {filename} -> {new_name}")
# os.rename(old_path, new_path) # 取消注释以实际执行
# 示例:将 IMG_YYYYMMDD_NNN.jpg 转换为 photo_YYYY-MM-DD_NNN.jpg
pattern = r'IMG_(\d{4})(\d{2})(\d{2})_(\d+)'
replacement = r'photo_\1-\2-\3_\4'
# batch_rename('/path/to/photos', pattern, replacement)八、性能优化与最佳实践
8.1 编译正则表达式
如果要重复使用同一个正则,先编译它:
# 不好:每次都重新编译
for line in lines:
if re.match(r'\d{3}-\d{4}-\d{4}', line):
process(line)
# 好:预先编译
pattern = re.compile(r'\d{3}-\d{4}-\d{4}')
for line in lines:
if pattern.match(line):
process(line)8.2 避免灾难性回溯
某些正则表达式可能导致指数级的回溯,造成性能灾难:
# 危险!可能导致灾难性回溯
bad_pattern = r'(a+)+b'
# 对于 "aaaaaaaaaaaaaaaaaaaaac" 这样的输入,会非常慢
# 改进
good_pattern = r'a+b'8.3 使用具体的字符类
# 不好:.* 太宽泛
r'<.*>'
# 好:更具体
r'<[^>]*>'8.4 锚定位置
如果知道匹配位置,使用 ^ 和 $:
# 如果要验证整个字符串是否是手机号
pattern = r'^1[3-9]\d{9}$' # 好
# 而不是
pattern = r'1[3-9]\d{9}' # 这会匹配字符串中间的手机号8.5 使用非捕获组
如果不需要捕获内容,使用 (?:) 而不是 ():
# 不需要捕获
pattern = r'(?:https?|ftp)://\S+'
# 而不是
pattern = r'(https?|ftp)://\S+'九、调试工具推荐
正则表达式调试可能很痛苦,好工具能帮大忙:
在线工具
regex101 (https://regex101.com/) - 最强大的在线正则调试器
- 实时匹配高亮
- 详细的解释说明
- 支持多种语言风格
- 可以保存和分享
RegExr (https://regexr.com/) - 简洁易用
- 社区模式库
- 详细的语法参考
Debuggex (https://www.debuggex.com/) - 可视化正则
- 将正则表达式可视化为图形
IDE插件
- VS Code: Regex Previewer 扩展
- JetBrains系列: 内置正则表达式测试工具
- Sublime Text: 内置正则搜索高亮
十、速查表
`基础匹配
. 任意字符(除换行符)
\d 数字 [0-9]
\D 非数字
\w 单词字符 [a-zA-Z0-9_]
\W 非单词字符
\s 空白字符
\S 非空白字符
量词
- 0次或多次
- 1次或多次
? 0次或1次
{n} 恰好n次
{n,} 至少n次
{n,m} n到m次
*? +? ?? 懒惰模式
字符类
[abc] a、b或c
1 非a、b、c
[a-z] a到z
[A-Z] A到Z
[0-9] 0到9
锚点
^ 字符串开头
$ 字符串结尾
\b 单词边界
\B 非单词边界
分组
(...) 捕获组
(?:...) 非捕获组
(?P
\1, \2 反向引用
环视
(?=...) 正向肯定预查
(?!...) 正向否定预查
(?<=...) 反向肯定预查
(?<!...) 反向否定预查
常用标志
i 忽略大小写
g 全局匹配
m 多行模式
s 单行模式(. 匹配换行)`
结语
正则表达式就像一把双刃剑:用好了是神兵利器,用不好就是自己挖的坑。
学习正则表达式的关键在于多练习。从简单的模式开始,逐步增加复杂度,配合 regex101 等工具进行调试,很快你就能写出优雅而高效的正则表达式。
记住:能用简单方法解决的问题,不一定要用正则。正则表达式是工具箱里的一件工具,不是唯一的锤子。
- abc ↩