跳到主要内容

正则表达式入门

正则表达式(Regular Expression,简称 regex 或 regexp)是一种用于匹配字符串模式的强大工具。它本质上是一门专门描述文本规则的微型编程语言,几乎所有现代编程语言都支持正则表达式。

什么是正则表达式

想象你需要在一篇文章中找出所有的邮箱地址,或者验证用户输入的手机号格式是否正确。这些任务如果用手动方式逐个检查,既繁琐又容易出错。正则表达式就是为解决这类问题而生的——它让你用简洁的语法描述文本模式,然后由程序自动完成匹配工作。

正则表达式的核心思想是:用特定的字符组合来描述一类字符串的共同特征。例如,中国手机号码 这个模式可以匹配所有以 1 开头、第二位是 3-9 的数字、总共 11 位的号码。

为什么要学习正则表达式

在实际开发中,正则表达式的应用场景非常广泛:

  • 表单验证:邮箱、手机号、身份证号、密码强度等格式校验
  • 数据提取:从 HTML、日志文件、API 响应中提取特定信息
  • 文本替换:批量替换、格式化、清理数据
  • 路由匹配:Web 框架中的 URL 路由规则
  • 语法高亮:代码编辑器中的关键字识别
  • 搜索引擎:分词、关键词匹配

掌握正则表达式能显著提升你处理文本数据的效率。

第一个正则表达式

让我们从一个简单的例子开始。假设你想判断一个字符串是否包含 "hello":

// JavaScript 示例
/hello/.test("hello world"); // true
/hello/.test("hi there"); // false
# Python 示例
import re
re.search(r"hello", "hello world") # 匹配成功
re.search(r"hello", "hi there") # None

这里的 hello 就是一个正则表达式,它匹配字符串中连续出现的 h-e-l-l-o 这五个字母。这种直接匹配字符的方式称为字面量匹配

创建正则表达式的方式

不同编程语言创建正则表达式的方式略有不同,但基本思路一致:

JavaScript

// 方式一:正则表达式字面量(推荐)
const regex1 = /abc/gi;

// 方式二:RegExp 构造函数
const regex2 = new RegExp("abc", "gi");

字面量方式在脚本加载时就完成编译,性能更好;构造函数方式适合需要动态构建模式的场景。

Python

import re

# 方式一:使用原始字符串(推荐)
pattern1 = re.compile(r"abc", re.IGNORECASE)

# 方式二:直接使用模块函数
match = re.search(r"abc", "abcdef", re.IGNORECASE)

Python 中推荐使用原始字符串(r"..."),这样可以避免反斜杠的转义问题。

正则表达式的工作原理

理解正则表达式的工作原理有助于你写出更高效的表达式。简单来说,正则引擎会:

  1. 编译:将正则表达式字符串编译成内部数据结构
  2. 匹配:从目标字符串的起始位置开始,尝试用模式进行匹配
  3. 回溯:当某条路径匹配失败时,回退到之前的状态尝试其他可能
  4. 返回:找到匹配时返回结果,或遍历完所有可能后返回失败

这个过程涉及有限自动机理论,但作为使用者,你只需要知道正则引擎会智能地尝试各种匹配可能,直到找到成功匹配或确定不存在匹配。

特殊字符与转义

正则表达式中有一些字符具有特殊含义,称为元字符

. ^ $ * + ? { } [ ] \ | ( )

如果你想匹配这些字符本身,需要使用反斜杠进行转义:

// 匹配字符串 "a*b"
/a\*b/.test("a*b"); // true

// 匹配字符串 "price: $10"
/price: \$10/.test("price: $10"); // true
# 匹配字符串 "a.b"
import re
re.search(r"a\.b", "a.b") # 匹配成功
提示

在字符类 [...] 中,大部分元字符会失去特殊含义,变成普通字符。例如 [.*+] 匹配的就是点号、星号或加号本身。

字符类

字符类用方括号 [] 表示,用于匹配方括号内的任意一个字符:

// 匹配元音字母
/[aeiou]/.test("apple"); // true,因为包含 'a'
/[aeiou]/.test("xyz"); // false,没有元音

// 匹配数字
/[0123456789]/.test("abc5def"); // true

范围表示法

连续的字符可以用范围简写:

/[a-z]/      // 所有小写字母
/[A-Z]/ // 所有大写字母
/[0-9]/ // 所有数字
/[a-zA-Z]/ // 所有字母
/[a-zA-Z0-9_]/ // 字母、数字和下划线

否定字符类

在字符类开头使用 ^ 表示否定,匹配不在方括号内的字符:

/[^aeiou]/.test("a");   // false,'a' 是元音
/[^aeiou]/.test("b"); // true,'b' 不是元音

// 匹配非数字字符
/[^0-9]/.test("123abc"); // true,因为有字母

预定义字符类

为了简化常用模式,正则表达式提供了预定义的字符类:

字符类含义等价于
.任意字符(除换行符)[^\n]
\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/.test("5");        // true
/\w/.test("_"); // true
/\s/.test(" "); // true
/\s/.test("\t"); // true(制表符)

// 实际应用:匹配电话号码
/\d{3}-\d{4}-\d{4}/.test("138-1234-5678"); // true
Unicode 注意事项

在 Unicode 模式下(JavaScript 的 u 标志,Python 的 re.UNICODE),\w 会匹配更多字符,包括中文、日文等 Unicode 字母。如果需要严格限制为 ASCII 字符,需要显式指定。

量词

量词用于指定前面元素出现的次数,是正则表达式中非常强大的特性:

基本量词

量词含义说明
*零次或多次{0,}
+一次或多次{1,}
?零次或一次{0,1}
// * 匹配零个或多个 'a'
/ca*t/.test("ct"); // true(零个 a)
/ca*t/.test("cat"); // true(一个 a)
/ca*t/.test("caaat"); // true(三个 a)

// + 匹配一个或多个 'a'
/ca+t/.test("ct"); // false(至少需要一个)
/ca+t/.test("cat"); // true

// ? 匹配零个或一个 'a'
/colou?r/.test("color"); // true(美式拼写)
/colou?r/.test("colour"); // true(英式拼写)

精确量词

量词含义
{n}恰好 n 次
{n,}至少 n 次
{n,m}n 到 m 次
/\d{4}/.test("2024");       // true,恰好 4 位数字
/\d{4}/.test("24"); // false,不够 4 位

/\d{2,}/.test("1"); // false,至少需要 2 位
/\d{2,}/.test("12345"); // true

/\d{2,4}/.test("12"); // true
/\d{2,4}/.test("1234"); // true
/\d{2,4}/.test("12345"); // true(匹配前 4 位)
/\d{2,4}/.test("1"); // false

贪婪与非贪婪匹配

默认情况下,量词是贪婪的——它们会尽可能多地匹配字符。有时这会导致意外的结果:

const html = "<div>内容1</div><div>内容2</div>";

// 贪婪匹配
html.match(/<div>.*<\/div>/);
// 结果: ["<div>内容1</div><div>内容2</div>"]
// 解释: .* 匹配了从第一个 <div> 到最后一个 </div> 的所有内容

如果你只想匹配第一个 <div> 标签内的内容,需要使用非贪婪量词,在量词后加 ?

// 非贪婪匹配
html.match(/<div>.*?<\/div>/);
// 结果: ["<div>内容1</div>"]
// 解释: .*? 匹配尽可能少的字符,遇到第一个 </div> 就停止
贪婪量词非贪婪量词
**?
++?
???
{n}{n}?
{n,}{n,}?
{n,m}{n,m}?
// 实际案例:提取引号内的内容
const text = '"hello" and "world"';

// 贪婪(错误)
/text.match(/".*"/); // [""hello" and "world""]

// 非贪婪(正确)
/text.match(/".*?"/); // [""hello""]

位置锚点

锚点用于匹配字符串中的特定位置,而不是具体字符:

锚点含义
^字符串开头
$字符串结尾
\b单词边界
\B非单词边界
// ^ 匹配开头
/^hello/.test("hello world"); // true
/^hello/.test("say hello"); // false

// $ 匹配结尾
/world$/.test("hello world"); // true
/world$/.test("world peace"); // false

// ^ 和 $ 组合进行全匹配
/^hello$/.test("hello"); // true
/^hello$/.test("hello world"); // false

单词边界

单词边界 \b 匹配单词字符(\w)和非单词字符之间的位置:

/\bcat\b/.test("the cat sat");     // true,cat 是独立单词
/\bcat\b/.test("concatenate"); // false,cat 是单词的一部分
/\bcat\b/.test("category"); // false

// 实际应用:替换单词
"the cat sat".replace(/\bcat\b/g, "dog"); // "the dog sat"
"concatenate".replace(/\bcat\b/g, "dog"); // "concatenate"(不变)
实用场景

使用 ^$ 组合可以确保整个字符串符合模式,而不仅仅是字符串中包含模式。这在表单验证时特别有用。

分组与捕获

圆括号 () 用于创建分组,分组有两个主要用途:

1. 捕获分组

分组可以捕获匹配的子字符串,供后续使用:

const date = "2024-03-15";
const regex = /(\d{4})-(\d{2})-(\d{2})/;
const match = date.match(regex);

console.log(match[0]); // "2024-03-15" - 完整匹配
console.log(match[1]); // "2024" - 第一个分组(年)
console.log(match[2]); // "03" - 第二个分组(月)
console.log(match[3]); // "15" - 第三个分组(日)

捕获的分组可以在替换中使用:

// 日期格式转换:2024-03-15 -> 15/03/2024
"2024-03-15".replace(/(\d{4})-(\d{2})-(\d{2})/, "$3/$2/$1");
// 结果: "15/03/2024"

2. 非捕获分组

如果你只需要分组功能而不需要捕获,可以使用 (?:...)

const str = "abc123";
const regex = /(?:abc)(\d+)/;
const match = str.match(regex);

console.log(match[0]); // "abc123"
console.log(match[1]); // "123" - 只有一个分组
// match[2] 不存在,因为 (?:abc) 是非捕获分组

非捕获分组在性能上略优于捕获分组,且使分组编号更简单。

3. 命名分组

现代正则表达式支持为分组命名,使代码更易读:

const date = "2024-03-15";
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = date.match(regex);

console.log(match.groups.year); // "2024"
console.log(match.groups.month); // "03"
console.log(match.groups.day); // "15"

// 命名分组也可以用于替换
"2024-03-15".replace(regex, "$<day>/$<month>/$<year>");
// 结果: "15/03/2024"
兼容性说明

命名分组在 JavaScript 2018 (ES9) 和 Python 3.6+ 中得到支持。如果需要兼容旧版本,请使用索引分组。

选择结构

竖线 | 表示"或"的关系,用于匹配多个模式中的任意一个:

/(cat|dog|bird)/.test("I have a cat");   // true
/(cat|dog|bird)/.test("I have a fish"); // false

// 结合分组使用
/(|)(|)/.test("大红猫"); // true
/(|)(|)/.test("小白猫"); // true

选择结构的优先级较低,通常需要配合分组使用:

// 错误:这会匹配 "gray" 或 "grey"
/gr|ay/.test("gray"); // true
/gr|ay/.test("grey"); // true
/gr|ay/.test("ay"); // true(不是我们想要的)

// 正确:匹配 "gray" 或 "grey"
/gr(a|e)y/.test("gray"); // true
/gr(a|e)y/.test("grey"); // true
/gr(a|e)y/.test("ay"); // false

标志(修饰符)

标志用于修改正则表达式的匹配行为:

标志含义JavaScriptPython
i忽略大小写re.I
g全局匹配-
m多行模式re.M
sdotAll 模式(. 匹配换行符)re.S
uUnicode 模式默认
y粘性匹配-
// i: 忽略大小写
/hello/i.test("HELLO"); // true

// g: 全局匹配(找到所有匹配,而不是第一个)
"hello hello".match(/hello/g); // ["hello", "hello"]

// m: 多行模式(^ 和 $ 匹配每行开始和结束)
/^hello$/m.test("world\nhello"); // true

// s: dotAll 模式(. 匹配换行符)
/hello.world/s.test("hello\nworld"); // true
import re

# 忽略大小写
re.search(r"hello", "HELLO", re.IGNORECASE)

# 多行模式
re.search(r"^hello$", "world\nhello", re.MULTILINE)

# dotAll 模式
re.search(r"hello.world", "hello\nworld", re.DOTALL)

基础练习

尝试写出匹配以下模式的正则表达式:

  1. 匹配邮箱地址:包含 @ 符号,前面是用户名,后面是域名
  2. 匹配手机号:11 位数字,以 1 开头,第二位是 3-9
  3. 匹配 IPv4 地址:四个 0-255 的数字,用点号分隔
  4. 匹配日期格式:YYYY-MM-DD 格式
  5. 匹配 HTML 标签:匹配 <tag></tag> 格式
点击查看参考答案
// 1. 简化版邮箱
/^[\w.-]+@[\w.-]+\.\w{2,}$/

// 2. 中国手机号
/^1[3-9]\d{9}$/

// 3. IPv4 地址(简化版)
/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/

// 4. 日期格式
/^\d{4}-\d{2}-\d{2}$/

// 5. HTML 标签
/<\/?[\w]+>/

小结

本章介绍了正则表达式的基础知识:

  • 字面量匹配:直接匹配字符
  • 字符类[] 匹配一组字符中的任意一个
  • 预定义字符类\d\w\s 等简写形式
  • 量词*+?{n,m} 控制匹配次数
  • 贪婪与非贪婪:默认贪婪,加 ? 变非贪婪
  • 锚点^$\b 匹配位置
  • 分组() 用于捕获和分组
  • 选择结构| 表示"或"
  • 标志igm 等修改匹配行为

掌握了这些基础概念后,你已经可以写出实用的正则表达式了。下一章我们将学习更高级的断言、反向引用等特性。

参考资源