跳到主要内容

练习与测验

实践是掌握正则表达式的最佳途径。本章提供了一系列由浅入深的练习题,帮助你巩固所学知识。每道题目都配有详细的分析和参考答案。

练习使用指南

在开始练习前,请注意以下几点:

  1. 先独立思考:在看答案之前,尽量自己尝试解决问题
  2. 测试你的答案:使用调试工具验证你的正则表达式是否正确
  3. 考虑边界情况:一个好的正则表达式应该正确处理边界情况
  4. 追求简洁:在保证正确性的前提下,尽量使用简洁的表达方式

基础练习

练习 1:匹配固定字符串

题目:编写正则表达式,匹配字符串 "hello"。

测试用例

hello       → 匹配
Hello → 不匹配(大小写)
hello world → 匹配(包含 hello)
hellobye → 匹配(包含 hello)
点击查看答案
/hello/

分析:最简单的正则表达式就是字面量匹配。"hello" 直接匹配字符串中的连续五个字符。注意它也会匹配包含 "hello" 的更长字符串。如果只想匹配完整的 "hello" 单词,需要添加边界:

/^hello$/    // 完全匹配
/\bhello\b/ // 匹配独立单词

练习 2:匹配任意数字

题目:编写正则表达式,匹配单个数字字符(0-9)。

测试用例

5    → 匹配
a → 不匹配
12 → 匹配两个(每个数字单独匹配)
点击查看答案
/\d/
// 或
/[0-9]/

分析\d 是预定义的数字字符类,等价于 [0-9]。两者功能相同,但 \d 更简洁。如果要匹配一个或多个连续数字,使用 \d+

练习 3:匹配单词

题目:编写正则表达式,匹配由字母组成的单词(假设单词只包含 a-z 和 A-Z)。

测试用例

hello   → 匹配
Hello → 匹配
world → 匹配
hello123 → 匹配 hello 部分
点击查看答案
/[a-zA-Z]+/
// 或
/[a-z]+/i // 使用 i 标志忽略大小写

分析

  • [a-zA-Z] 定义了一个字符类,包含所有大小写字母
  • + 量词表示匹配一个或多个
  • 使用 i 标志可以忽略大小写,简化字符类为 [a-z]

练习 4:匹配空白字符

题目:编写正则表达式,匹配一个或多个连续的空白字符(包括空格、制表符、换行符)。

测试用例

" "      → 匹配(空格)
"\t" → 匹配(制表符)
"\n" → 匹配(换行符)
" " → 匹配(多个空格)
点击查看答案
/\s+/

分析\s 是预定义的空白字符类,包括空格、制表符 \t、换行符 \n、回车符 \r、换页符 \f 和垂直制表符 \v+ 量词匹配一个或多个。

练习 5:匹配开头和结尾

题目:编写正则表达式,匹配以 "Hello" 开头、以 "World" 结尾的字符串。

测试用例

Hello World         → 匹配
Hello beautiful World → 匹配
world Hello World → 不匹配(不以 Hello 开头)
Hello World! → 不匹配(不以 World 结尾)
点击查看答案
/^Hello.*World$/

分析

  • ^ 匹配字符串开头
  • Hello 字面量匹配
  • .* 匹配任意字符零次或多次
  • World 字面量匹配
  • $ 匹配字符串结尾

如果要匹配中间部分不含换行符,使用:

/^Hello[^\n]*World$/

进阶练习

练习 6:匹配邮箱地址

题目:编写正则表达式,匹配简单的邮箱地址格式。

要求

  • 用户名部分:字母、数字、点、下划线、减号
  • 必须有 @ 符号
  • 域名部分:字母、数字、点、减号
  • 必须有顶级域名(至少 2 个字母)

测试用例

[email protected]        → 匹配
[email protected] → 匹配
[email protected] → 匹配
invalid → 不匹配
@example.com → 不匹配
[email protected] → 不匹配
点击查看答案
/^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}$/

分析

  • [\w.-]+ 用户名:\w 等价于 [a-zA-Z0-9_],加上 .-
  • @ 字面量
  • [\w.-]+ 域名
  • \. 字面量点号
  • [a-zA-Z]{2,} 顶级域名,至少 2 个字母

更严格的版本:

/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

练习 7:匹配手机号码

题目:编写正则表达式,匹配中国大陆手机号码。

要求

  • 必须是 11 位数字
  • 以 1 开头
  • 第二位是 3-9

测试用例

13812345678   → 匹配
15912345678 → 匹配
18812345678 → 匹配
12812345678 → 不匹配(第二位是 2)
12345678901 → 不匹配(第二位是 2)
1381234567 → 不匹配(只有 10 位)
点击查看答案
/^1[3-9]\d{9}$/

分析

  • ^ 字符串开头
  • 1 第一位必须是 1
  • [3-9] 第二位是 3 到 9 中的一个
  • \d{9} 后面 9 位数字
  • $ 字符串结尾

练习 8:匹配日期格式

题目:编写正则表达式,匹配 YYYY-MM-DD 格式的日期。

要求

  • 年份:4 位数字
  • 月份:01-12
  • 日期:01-31

测试用例

2024-03-15   → 匹配
1999-12-31 → 匹配
2024-1-15 → 不匹配(月份格式不对)
2024-13-15 → 不匹配(月份超范围)
2024-03-32 → 不匹配(日期超范围)
点击查看答案
/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/

分析

  • \d{4} 4 位年份
  • - 分隔符
  • (0[1-9]|1[0-2]) 月份:01-09 或 10-12
  • - 分隔符
  • (0[1-9]|[12]\d|3[01]) 日期:01-09、10-29、30-31

注意:这个正则表达式只验证格式,不验证日期的有效性(如 2 月 30 日)。完整验证需要编程逻辑。

练习 9:匹配 IP 地址

题目:编写正则表达式,匹配 IPv4 地址。

要求:每个数字段范围是 0-255

测试用例

192.168.1.1       → 匹配
0.0.0.0 → 匹配
255.255.255.255 → 匹配
256.1.1.1 → 不匹配(256 超范围)
192.168.1 → 不匹配(只有 3 段)
192.168.1.1.1 → 不匹配(5 段)
点击查看答案
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/

分析

匹配 0-255 的数字需要分段处理:

  • 25[0-5] 匹配 250-255
  • 2[0-4][0-9] 匹配 200-249
  • [01]?[0-9][0-9]? 匹配 0-199(包括一位数和两位数)

完整解析:

(?:(?:数字模式)\.){3}  前三个数字段后面跟点
(?:数字模式) 第四个数字段(没有点)

练习 10:匹配 URL

题目:编写正则表达式,匹配 HTTP/HTTPS URL。

测试用例

http://example.com           → 匹配
https://example.com → 匹配
https://www.example.com → 匹配
https://example.com/path → 匹配
https://example.com?a=1 → 匹配
ftp://example.com → 不匹配(不是 http/https)
example.com → 不匹配(缺少协议)
点击查看答案
/^https?:\/\/[\w.-]+(?:\/[\w./?%&=-]*)?$/

分析

  • ^ 开头
  • https? http 或 https(s 后的 ? 表示 s 可选)
  • :\/\/ 字面量 ://
  • [\w.-]+ 域名
  • (?:\/[\w./?%&=-]*)? 可选的路径部分
  • $ 结尾

更完整的版本:

/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/

高级练习

练习 11:提取 HTML 标签内容

题目:编写正则表达式,提取 HTML 标签内的文本内容。

要求:提取 <div></div> 之间的内容

测试用例

<div>内容</div>       → 提取 "内容"
<div>多行
内容</div> → 提取 "多行\n内容"
<div><span>嵌套</span></div> → 提取 "<span>嵌套</span>"
点击查看答案
// 简单版本(不处理嵌套)
/<div>([^<]*)<\/div>/

// 支持 . 匹配换行
/<div>([\s\S]*?)<\/div>/

// 使用 s 标志(ES2018)
/<div>(.*?)<\/div>/s

分析

  • [^<]* 匹配非 < 的字符,但无法匹配嵌套标签
  • [\s\S]*? 匹配任意字符(包括换行),非贪婪
  • s 标志让 . 也能匹配换行符
注意

正则表达式不适合解析复杂的 HTML 结构。对于复杂的 HTML 处理,请使用专门的解析器(如 DOMParser、BeautifulSoup)。

练习 12:匹配成对引号

题目:编写正则表达式,匹配单引号或双引号包围的内容,要求开始和结束的引号类型一致。

测试用例

'hello'     → 匹配
"hello&quot; → 匹配
'hello&quot; → 不匹配(引号不配对)
"hello' → 不匹配(引号不配对)
点击查看答案
// 方法1:使用反向引用
/(["'])(.*?)\1/

// 方法2:使用分组
/'[^']*'|"[^"]*"/

分析

方法1

  • (["']) 捕获第一个引号(第1组)
  • .*? 匹配内容(非贪婪)
  • \1 反向引用,匹配与第1组相同的引号

方法2

  • '[^']*' 匹配单引号包围的内容
  • "[^"]*" 匹配双引号包围的内容
  • | 表示或

练习 13:密码强度验证

题目:编写正则表达式,验证密码强度。

要求

  • 至少 8 个字符
  • 包含大写字母
  • 包含小写字母
  • 包含数字

测试用例

Password123     → 匹配
password123 → 不匹配(无大写)
PASSWORD123 → 不匹配(无小写)
Password → 不匹配(无数字)
Pass1 → 不匹配(太短)
点击查看答案
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/

分析

  • (?=.*[a-z]) 正向先行断言:后面某处有小写字母
  • (?=.*[A-Z]) 正向先行断言:后面某处有大写字母
  • (?=.*\d) 正向先行断言:后面某处有数字
  • .{8,} 实际匹配:至少 8 个任意字符

如果要求包含特殊字符:

/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/

练习 14:千位分隔符

题目:编写正则表达式,给数字添加千位分隔符。

要求:将 1234567890 转换为 1,234,567,890

测试用例

1234567890  → 1,234,567,890
12345 → 12,345
123 → 123
点击查看答案
function addCommas(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

分析

  • \B 非单词边界(不在数字开头)
  • (?=(\d{3})+) 正向先行断言:后面是 3 的倍数个数字
  • (?!\d) 负向先行断言:后面不是数字(确保在正确位置)
  • 这个模式匹配的是"位置",然后用逗号替换

匹配过程:

  1. 从右向左数,每三位数字前插入逗号
  2. 位置 7(1|234567890):后面有 9 位,匹配,插入逗号
  3. 位置 4(1,234|567890):后面有 6 位,匹配,插入逗号
  4. 位置 1(1,234,567|890):后面有 3 位,匹配,插入逗号

练习 15:匹配重复单词

题目:编写正则表达式,找出文本中连续重复的单词。

测试用例

"the the cat&quot;        → 匹配 "the the"
"the cat cat sat&quot; → 匹配 "cat cat"
"the cat sat&quot; → 不匹配
点击查看答案
/\b(\w+)\s+\1\b/gi

分析

  • \b 单词边界
  • (\w+) 捕获一个单词(第1组)
  • \s+ 一个或多个空白
  • \1 反向引用,匹配与第1组相同的内容
  • \b 单词边界
  • g 全局匹配
  • i 忽略大小写

练习 16:提取 URL 参数

题目:编写正则表达式,从 URL 中提取查询参数。

测试用例

"https://example.com?name=张三&age=25"
→ 提取 {name: "张三", age: "25"}

"https://example.com?page=1&size=10&sort=desc"
→ 提取 {page: "1", size: "10", sort: "desc"}
点击查看答案
function parseUrlParams(url) {
const params = {};
const regex = /[?&]([^=]+)=([^&]*)/g;
let match;

while ((match = regex.exec(url)) !== null) {
params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
}

return params;
}

分析

  • [?&] 匹配 ?& 开头
  • ([^=]+) 捕获参数名(非 = 的字符)
  • = 字面量
  • ([^&]*) 捕获参数值(非 & 的字符)
  • decodeURIComponent 解码 URL 编码的字符

练习 17:驼峰与下划线互转

题目:编写正则表达式,实现驼峰命名和下划线命名的互转。

测试用例

驼峰转下划线:
userName → user_name
getUserById → get_user_by_id
XMLParser → x_m_l_parser

下划线转驼峰:
user_name → userName
get_user_by_id → getUserById
点击查看答案
// 驼峰转下划线
function camelToSnake(str) {
return str.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
}

// 下划线转驼峰
function snakeToCamel(str) {
return str.replace(/_([a-z])/g, (_, char) => char.toUpperCase());
}

分析

驼峰转下划线

  • ([a-z])([A-Z]) 匹配小写字母后跟大写字母
  • $1_$2 在两个字母之间插入下划线
  • toLowerCase() 转为小写

下划线转驼峰

  • _([a-z]) 匹配下划线后跟小写字母
  • 替换函数将字母转为大写并返回

练习 18:敏感信息脱敏

题目:编写正则表达式,对手机号和身份证号进行脱敏处理。

测试用例

手机号:
13812345678 → 138****5678

身份证号:
110105199001011234 → 110105********1234
点击查看答案
// 手机号脱敏
function maskPhone(phone) {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}

// 身份证号脱敏
function maskIdCard(idCard) {
return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2');
}

分析

  • (\d{3}) 捕获前 3 位
  • \d{4} 中间 4 位(不捕获,用于替换)
  • (\d{4}) 捕获后 4 位
  • $1****$2 组合:前3位 + **** + 后4位

练习 19:匹配嵌套括号

题目:编写正则表达式,匹配最多 2 层嵌套的括号内容。

测试用例

(a)           → 匹配
(a(b)) → 匹配
(a(b(c))) → 不匹配(超过 2 层)
点击查看答案
// 匹配最多 2 层嵌套
/\((?:[^()]|\([^()]*\))*\)/

分析

  • \( 开始括号
  • (?:...) 非捕获分组
  • [^()] 非括号字符
  • |
  • \([^()]*\) 一层嵌套的括号
  • * 重复前面的内容
  • \) 结束括号

对于更多层的嵌套,正则表达式会变得非常复杂。对于任意深度的嵌套,应该使用解析器或专门的库(如 Python 的 regex 模块支持递归)。

练习 20:验证版本号

题目:编写正则表达式,验证语义化版本号(SemVer)。

要求:格式为 MAJOR.MINOR.PATCH,每个部分都是非负整数

测试用例

1.0.0         → 匹配
0.0.1 → 匹配
10.20.30 → 匹配
1.0 → 不匹配(缺少 PATCH)
1.0.0.0 → 不匹配(太多部分)
v1.0.0 → 不匹配(有前缀)
点击查看答案
// 基本版本
/^\d+\.\d+\.\d+$/

// 完整语义化版本(包含预发布和构建信息)
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/

分析

基本版本

  • \d+ 一个或多个数字
  • \. 字面量点号
  • 三部分用点号连接

完整版本

  • v? 可选的 v 前缀
  • (0|[1-9]\d*) 不允许前导零(除非是 0 本身)
  • (?:-...)? 可选的预发布标识
  • (?:\+...)? 可选的构建元数据

综合挑战

挑战 1:日志解析器

题目:解析 Apache/Nginx 访问日志,提取 IP、时间、请求方法、路径、状态码。

日志格式

192.168.1.1 - - [10/Oct/2023:13:55:36 +0800] "GET /index.html HTTP/1.1&quot; 200 1234
点击查看答案
const logRegex = /^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) [^"]+&quot; (\d+) (\d+)$/;

function parseLog(log) {
const match = log.match(logRegex);
if (!match) return null;

return {
ip: match[1],
time: match[2],
method: match[3],
path: match[4],
status: parseInt(match[5]),
size: parseInt(match[6])
};
}

// 使用命名分组(更清晰)
const logRegexNamed = /^(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) [^"]+&quot; (?P<status>\d+) (?P<size>\d+)$/;

挑战 2:CSV 解析器

题目:解析 CSV 格式的数据行,处理引号包围的字段。

测试用例

name,age,city
"John Doe",25,"New York"
"Smith, Jr.",30,"Los Angeles"
点击查看答案
function parseCSV(line) {
const result = [];
let current = '';
let inQuotes = false;

for (let i = 0; i < line.length; i++) {
const char = line[i];

if (inQuotes) {
if (char === '"') {
if (line[i + 1] === '"') {
current += '"';
i++;
} else {
inQuotes = false;
}
} else {
current += char;
}
} else {
if (char === '"') {
inQuotes = true;
} else if (char === ',') {
result.push(current);
current = '';
} else {
current += char;
}
}
}

result.push(current);
return result;
}

// 正则表达式版本(简化,不处理转义引号)
function parseCSVRegex(line) {
const regex = /(?:"([^"]*)"|([^,]*))/g;
const result = [];
let match;

while ((match = regex.exec(line)) !== null) {
result.push(match[1] !== undefined ? match[1] : match[2]);
}

return result;
}

挑战 3:Markdown 链接提取

题目:从 Markdown 文本中提取所有链接的文本和 URL。

格式[链接文本](URL)

测试用例

"查看 [官方文档](https://example.com/docs) 和 [教程](https://tutorial.com)"
→ 提取 [{text: "官方文档", url: "https://example.com/docs"}, {text: "教程", url: "https://tutorial.com"}]
点击查看答案
function extractMarkdownLinks(text) {
const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
const links = [];
let match;

while ((match = regex.exec(text)) !== null) {
links.push({
text: match[1],
url: match[2]
});
}

return links;
}

// 使用 matchAll(ES2020)
function extractMarkdownLinksV2(text) {
return [...text.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)].map(match => ({
text: match[1],
url: match[2]
}));
}

小结

通过这些练习,你应该掌握了:

  • 基础匹配:字面量、字符类、量词
  • 位置控制:锚点、单词边界
  • 分组技术:捕获分组、非捕获分组、命名分组、反向引用
  • 高级特性:零宽断言、条件匹配
  • 实战应用:数据验证、格式转换、文本提取

正则表达式是一项需要大量练习的技能。建议你:

  1. 多用多练:在日常工作中寻找使用正则表达式的机会
  2. 阅读别人的正则:学习优秀正则表达式的设计思路
  3. 保持简洁:在满足需求的前提下,尽量使用简单的正则
  4. 测试边界:不要忘记测试边界情况和异常输入

参考资源