练习与测验
实践是掌握正则表达式的最佳途径。本章提供了一系列由浅入深的练习题,帮助你巩固所学知识。每道题目都配有详细的分析和参考答案。
练习使用指南
在开始练习前,请注意以下几点:
- 先独立思考:在看答案之前,尽量自己尝试解决问题
- 测试你的答案:使用调试工具验证你的正则表达式是否正确
- 考虑边界情况:一个好的正则表达式应该正确处理边界情况
- 追求简洁:在保证正确性的前提下,尽量使用简洁的表达方式
基础练习
练习 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-2552[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" → 匹配
'hello" → 不匹配(引号不配对)
"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)负向先行断言:后面不是数字(确保在正确位置)- 这个模式匹配的是"位置",然后用逗号替换
匹配过程:
- 从右向左数,每三位数字前插入逗号
- 位置 7(
1|234567890):后面有 9 位,匹配,插入逗号 - 位置 4(
1,234|567890):后面有 6 位,匹配,插入逗号 - 位置 1(
1,234,567|890):后面有 3 位,匹配,插入逗号
练习 15:匹配重复单词
题目:编写正则表达式,找出文本中连续重复的单词。
测试用例:
"the the cat" → 匹配 "the the"
"the cat cat sat" → 匹配 "cat cat"
"the cat sat" → 不匹配
点击查看答案
/\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" 200 1234
点击查看答案
const logRegex = /^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) [^"]+" (\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+) [^"]+" (?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]
}));
}
小结
通过这些练习,你应该掌握了:
- 基础匹配:字面量、字符类、量词
- 位置控制:锚点、单词边界
- 分组技术:捕获分组、非捕获分组、命名分组、反向引用
- 高级特性:零宽断言、条件匹配
- 实战应用:数据验证、格式转换、文本提取
正则表达式是一项需要大量练习的技能。建议你:
- 多用多练:在日常工作中寻找使用正则表达式的机会
- 阅读别人的正则:学习优秀正则表达式的设计思路
- 保持简洁:在满足需求的前提下,尽量使用简单的正则
- 测试边界:不要忘记测试边界情况和异常输入
参考资源
- Regex101 - 在线测试和调试
- RegexCrossword - 正则表达式游戏
- HackerRank Regex Challenges - 编程练习
- RegexOne - 交互式教程