搜 索

正则表达式

  • 218阅读
  • 2022年12月25日
  • 0评论
首页 / Java/Go/Python / 正文

一、什么是正则表达式?

正则表达式(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.tcat, cot, cut, c@t
^匹配字符串开头^HelloHello开头的字符串
$匹配字符串结尾world$world结尾的字符串
*前一个字符出现0次或多次ab*cac, abc, abbc, abbbc
+前一个字符出现1次或多次ab+cabc, abbc, abbbc
?前一个字符出现0次或1次colou?rcolor, 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 贪婪与懒惰匹配

默认情况下,量词是贪婪的(尽可能多匹配):

`正则:<.*>
文本:

Hello

匹配:^^^^^^^^^^^^^^^^ (匹配整个字符串)`

在量词后加 ? 变成懒惰模式(尽可能少匹配):

`正则:<.*?>
文本:

Hello

匹配:^^^^^ ^^^^^ (分别匹配
)`


三、分组与引用

3.1 捕获分组 ()

圆括号创建捕获组,可以:

  1. 将多个字符作为一个单元
  2. 捕获匹配的内容供后续使用

`正则:(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\d{4})-(?P\d{2})-(?P\d{2})
文本: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+'

九、调试工具推荐

正则表达式调试可能很痛苦,好工具能帮大忙:

在线工具

  1. regex101 (https://regex101.com/) - 最强大的在线正则调试器

    • 实时匹配高亮
    • 详细的解释说明
    • 支持多种语言风格
    • 可以保存和分享
  2. RegExr (https://regexr.com/) - 简洁易用

    • 社区模式库
    • 详细的语法参考
  3. 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 等工具进行调试,很快你就能写出优雅而高效的正则表达式。

记住:能用简单方法解决的问题,不一定要用正则。正则表达式是工具箱里的一件工具,不是唯一的锤子。


  1. abc
评论区
暂无评论
avatar