跳到主要内容

正则表达式实战案例

本章通过一系列实际案例,展示正则表达式在真实场景中的应用。每个案例都包含需求分析、正则表达式设计思路、完整代码实现以及注意事项。

数据验证

数据验证是正则表达式最常见的应用场景之一。无论是用户注册、表单提交还是数据导入,都需要验证输入数据的格式是否正确。

邮箱地址验证

邮箱地址的格式看似简单,实则复杂。根据 RFC 5322 标准,邮箱地址可以有非常多的变体。在实际应用中,我们通常采用简化版的验证规则。

需求分析

一个标准的邮箱地址由三部分组成:

  • 用户名:字母、数字、点号、下划线、横线等
  • @ 符号
  • 域名:由多个标签组成,用点号分隔,最后一个标签是顶级域名

正则表达式设计

// 基础版本 - 覆盖绝大多数常见邮箱
const emailBasic = /^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,}$/;

// 较严格版本 - 对域名部分有更多限制
const emailStrict = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

// 完整验证函数
function validateEmail(email) {
const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return pattern.test(email);
}

// 测试用例
console.log(validateEmail("[email protected]")); // true
console.log(validateEmail("[email protected]")); // true
console.log(validateEmail("[email protected]")); // true
console.log(validateEmail("[email protected]")); // true
console.log(validateEmail("user@example")); // false(无顶级域名)
console.log(validateEmail("@example.com")); // false(无用户名)
console.log(validateEmail("user @example.com")); // false(含空格)

Python 实现

import re

def validate_email(email: str) -> bool:
"""验证邮箱地址格式"""
pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
return bool(pattern.match(email))

# 测试
test_emails = [
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"invalid.email",
"@example.com"
]

for email in test_emails:
print(f"{email}: {validate_email(email)}")

重要说明

正则表达式只能验证邮箱的格式是否正确,无法验证邮箱是否真实存在。如果需要验证邮箱的真实性,需要发送验证邮件。

关于邮箱验证

不要过度依赖正则表达式验证邮箱。实际上,唯一可靠的方式是发送验证邮件。复杂的正则表达式可能拒绝合法的邮箱地址(如带引号的用户名、国际化域名等),而简单的正则又可能通过一些无效地址。在生产环境中,建议使用简单验证配合邮件确认。

手机号码验证

不同国家和地区的手机号码格式差异很大,这里以中国大陆手机号为例。

需求分析

中国大陆手机号码的特点:

  • 共 11 位数字
  • 第一位固定为 1
  • 第二位为 3-9(运营商号段)
  • 其余 9 位为任意数字

正则表达式设计

// 基础版本
const phoneBasic = /^1[3-9]\d{9}$/;

// 带格式的版本(支持空格、横线分隔)
const phoneFormatted = /^1[3-9][\s-]?\d{4}[\s-]?\d{4}$/;

// 完整验证函数
function validatePhone(phone) {
// 先去除所有非数字字符
const cleaned = phone.replace(/\D/g, '');

// 验证格式
const pattern = /^1[3-9]\d{9}$/;
return pattern.test(cleaned);
}

// 测试用例
console.log(validatePhone("13812345678")); // true
console.log(validatePhone("138-1234-5678")); // true
console.log(validatePhone("138 1234 5678")); // true
console.log(validatePhone("12812345678")); // false(第二位不在3-9)
console.log(validatePhone("1381234567")); // false(只有10位)
console.log(validatePhone("138123456789")); // false(12位)

Python 实现

import re

def validate_phone(phone: str) -> bool:
"""验证中国大陆手机号"""
# 去除非数字字符
cleaned = re.sub(r'\D', '', phone)

# 验证格式
pattern = re.compile(r'^1[3-9]\d{9}$')
return bool(pattern.match(cleaned))

def format_phone(phone: str) -> str:
"""格式化手机号为标准格式:138-1234-5678"""
cleaned = re.sub(r'\D', '', phone)
if len(cleaned) == 11:
return f"{cleaned[:3]}-{cleaned[3:7]}-{cleaned[7:]}"
return phone

# 测试
print(validate_phone("13812345678")) # True
print(validate_phone("138-1234-5678")) # True
print(format_phone("13812345678")) # 138-1234-5678

身份证号码验证

中国大陆身份证号码为 18 位,包含地区码、出生日期、顺序码和校验码。

需求分析

身份证号码结构:

  • 前 6 位:地区码
  • 7-14 位:出生日期(YYYYMMDD)
  • 15-17 位:顺序码(奇数为男性,偶数为女性)
  • 第 18 位:校验码(0-9 或 X)

正则表达式设计

// 基础格式验证
const idCardBasic = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;

// 完整验证函数(包含校验码验证)
function validateIdCard(idCard) {
// 基础格式验证
const pattern = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/;
if (!pattern.test(idCard)) {
return false;
}

// 校验码验证
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];

let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(idCard[i]) * weights[i];
}

const checkCode = checkCodes[sum % 11];
return idCard[17].toUpperCase() === checkCode;
}

// 提取身份证信息
function parseIdCard(idCard) {
if (!validateIdCard(idCard)) {
return null;
}

return {
province: idCard.substring(0, 2),
city: idCard.substring(0, 4),
district: idCard.substring(0, 6),
birthDate: `${idCard.substring(6, 10)}-${idCard.substring(10, 12)}-${idCard.substring(12, 14)}`,
gender: parseInt(idCard[16]) % 2 === 1 ? '男' : '女'
};
}

// 测试
console.log(validateIdCard("11010519900307888X")); // true(示例)
console.log(parseIdCard("11010519900307888X"));
// { province: '11', city: '1101', district: '110105', birthDate: '1990-03-07', gender: '男' }

密码强度验证

密码强度验证通常需要检查多个条件:长度、是否包含大小写字母、数字、特殊字符等。

需求分析

常见密码强度要求:

  • 至少 8 个字符
  • 包含大写字母
  • 包含小写字母
  • 包含数字
  • 包含特殊字符(可选)

正则表达式设计

// 中等强度:至少8位,包含大小写字母和数字
const passwordMedium = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;

// 高强度:至少8位,包含大小写字母、数字和特殊字符
const passwordStrong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;

// 灵活的验证函数
function validatePassword(password, options = {}) {
const {
minLength = 8,
requireLower = true,
requireUpper = true,
requireDigit = true,
requireSpecial = false,
specialChars = '@$!%*?&'
} = options;

const checks = [];

if (requireLower) checks.push({ pattern: /[a-z]/, message: '需要包含小写字母' });
if (requireUpper) checks.push({ pattern: /[A-Z]/, message: '需要包含大写字母' });
if (requireDigit) checks.push({ pattern: /\d/, message: '需要包含数字' });
if (requireSpecial) {
checks.push({
pattern: new RegExp(`[${specialChars}]`),
message: `需要包含特殊字符:${specialChars}`
});
}

const errors = [];

if (password.length < minLength) {
errors.push(`密码长度至少${minLength}`);
}

for (const check of checks) {
if (!check.pattern.test(password)) {
errors.push(check.message);
}
}

return {
valid: errors.length === 0,
errors
};
}

// 测试
console.log(validatePassword("Password123"));
// { valid: true, errors: [] }

console.log(validatePassword("pass123"));
// { valid: false, errors: ['需要包含大写字母', '密码长度至少8位'] }

console.log(validatePassword("Password123", { requireSpecial: true }));
// { valid: false, errors: ['需要包含特殊字符:@$!%*?&'] }

IP 地址验证

IP 地址分为 IPv4 和 IPv6 两种格式,验证规则各不相同。

IPv4 验证

// IPv4 地址:四个 0-255 的数字,用点号分隔

// 方法一:简化版(可能匹配超出范围的数字)
const ipv4Simple = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;

// 方法二:精确版(每个段都在 0-255 范围内)
const ipv4Strict = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/;

// 解析正则表达式:
// 25[0-5] 匹配 250-255
// 2[0-4]\d 匹配 200-249
// [01]?\d\d? 匹配 0-199(包括 0-9, 00-99, 100-199)

// 完整验证函数
function validateIPv4(ip) {
const pattern = /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/;

if (!pattern.test(ip)) {
return false;
}

// 额外检查:确保每段不超过 255
const parts = ip.split('.').map(Number);
return parts.every(part => part >= 0 && part <= 255);
}

// 测试
console.log(validateIPv4("192.168.1.1")); // true
console.log(validateIPv4("255.255.255.255")); // true
console.log(validateIPv4("0.0.0.0")); // true
console.log(validateIPv4("256.1.1.1")); // false
console.log(validateIPv4("192.168.1")); // false
console.log(validateIPv4("192.168.1.1.1")); // false

IPv6 验证

// IPv6 地址:8 组 4 位十六进制数,用冒号分隔
// 支持缩写形式(如 :: 表示连续的零)

// 完整形式
const ipv6Full = /^([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}$/;

// 支持缩写形式(复杂但完整)
const ipv6Compressed = /^(([\da-fA-F]{1,4}:){7}[\da-fA-F]{1,4}|([\da-fA-F]{1,4}:){1,7}:|([\da-fA-F]{1,4}:){1,6}:[\da-fA-F]{1,4}|([\da-fA-F]{1,4}:){1,5}(:[\da-fA-F]{1,4}){1,2}|([\da-fA-F]{1,4}:){1,4}(:[\da-fA-F]{1,4}){1,3}|([\da-fA-F]{1,4}:){1,3}(:[\da-fA-F]{1,4}){1,4}|([\da-fA-F]{1,4}:){1,2}(:[\da-fA-F]{1,4}){1,5}|[\da-fA-F]{1,4}:((:[\da-fA-F]{1,4}){1,6})|:((:[\da-fA-F]{1,4}){1,7}|:)|fe80:(:[\da-fA-F]{0,4}){0,4}%[\da-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[\da-fA-F]){0,1}[\da-fA-F])\.){3}(25[0-5]|(2[0-4]|1{0,1}[\da-fA-F]){0,1}[\da-fA-F])|([\da-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[\da-fA-F]){0,1}[\da-fA-F])\.){3}(25[0-5]|(2[0-4]|1{0,1}[\da-fA-F]){0,1}[\da-fA-F]))$/;

// 实际应用中,可以使用更简单的验证
function validateIPv6(ip) {
// 检查是否包含 ::
const doubleColon = ip.indexOf('::');

if (doubleColon !== -1) {
// 只能有一个 ::
if (ip.indexOf('::', doubleColon + 1) !== -1) {
return false;
}
}

// 分割并验证每组
const groups = ip.split(':');

for (const group of groups) {
if (group === '') continue; // 空组(由 :: 产生)
if (!/^[0-9a-fA-F]{1,4}$/.test(group)) {
return false;
}
}

return true;
}

// 测试
console.log(validateIPv6("2001:0db8:85a3:0000:0000:8a2e:0370:7334")); // true
console.log(validateIPv6("2001:db8:85a3::8a2e:370:7334")); // true
console.log(validateIPv6("::1")); // true
console.log(validateIPv6("::")); // true

数据提取

正则表达式非常适合从文本中提取特定格式的数据。

提取网页链接

从 HTML 或文本中提取 URL 是常见的爬虫任务。

// 提取 HTTP/HTTPS URL
function extractUrls(text) {
const pattern = /https?:\/\/[^\s<>"{}|\\^`\[\]]+/gi;
return text.match(pattern) || [];
}

// 提取 HTML 中的链接
function extractHtmlLinks(html) {
const pattern = /<a\s+(?:[^>]*?\s+)?href=["']([^"']+)["'][^>]*>([^<]*)<\/a>/gi;
const links = [];
let match;

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

return links;
}

// 测试
const html = `
<a href="https://example.com">Example</a>
<a href="https://google.com" target="_blank">Google</a>
<a class="link" href="https://github.com">GitHub</a>
`;

console.log(extractHtmlLinks(html));
// [
// { url: 'https://example.com', text: 'Example' },
// { url: 'https://google.com', text: 'Google' },
// { url: 'https://github.com', text: 'GitHub' }
// ]

提取 Markdown 链接和图片

// 提取 Markdown 链接 [text](url)
function extractMarkdownLinks(text) {
const pattern = /\[([^\]]+)\]\(([^)]+)\)/g;
const links = [];
let match;

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

return links;
}

// 提取 Markdown 图片 ![alt](src)
function extractMarkdownImages(text) {
const pattern = /!\[([^\]]*)\]\(([^)]+)\)/g;
const images = [];
let match;

while ((match = pattern.exec(text)) !== null) {
images.push({
alt: match[1],
src: match[2]
});
}

return images;
}

// 测试
const markdown = `
这是一个[链接](https://example.com)和一张图片:
![示例图片](https://example.com/image.png)
另一个[GitHub链接](https://github.com)
`;

console.log(extractMarkdownLinks(markdown));
// [{ text: '链接', url: 'https://example.com' }, ...]

console.log(extractMarkdownImages(markdown));
// [{ alt: '示例图片', src: 'https://example.com/image.png' }]

提取 JSON 数据

从文本中提取 JSON 字符串:

// 提取 JSON 对象或数组
function extractJson(text) {
// 匹配 {...} 或 [...]
const pattern = /[\[{](?:[^[\]{}]|[\[{](?:[^[\]{}]|[\[{][^[\]{}]*[\]}])*[\]}])*[\]}]/g;
const results = [];
let match;

while ((match = pattern.exec(text)) !== null) {
try {
const parsed = JSON.parse(match[0]);
results.push(parsed);
} catch (e) {
// 不是有效的 JSON,跳过
}
}

return results;
}

// 更简单的方法:查找 JSON 开始和结束标记
function extractJsonSimple(text) {
const results = [];
let start = -1;
let depth = 0;

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

if (char === '{' || char === '[') {
if (depth === 0) start = i;
depth++;
} else if (char === '}' || char === ']') {
depth--;
if (depth === 0 && start !== -1) {
try {
const jsonStr = text.slice(start, i + 1);
results.push(JSON.parse(jsonStr));
} catch (e) {
// 解析失败,继续
}
start = -1;
}
}
}

return results;
}

// 测试
const text = '响应数据:{"name": "张三", "age": 25} 和数组:[1, 2, 3]';
console.log(extractJsonSimple(text));
// [{ name: '张三', age: 25 }, [1, 2, 3]]

提取代码中的函数定义

// 提取 JavaScript 函数定义
function extractJsFunctions(code) {
// 匹配 function name(...) 或 const name = (...) =>
const pattern = /(?:function\s+(\w+)\s*\([^)]*\)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>)/g;
const functions = [];
let match;

while ((match = pattern.exec(code)) !== null) {
functions.push({
name: match[1] || match[2],
type: match[1] ? 'function' : 'arrow'
});
}

return functions;
}

// 测试
const code = `
function add(a, b) {
return a + b;
}

const multiply = (a, b) => a * b;

async function fetchData(url) {
return await fetch(url);
}
`;

console.log(extractJsFunctions(code));
// [
// { name: 'add', type: 'function' },
// { name: 'multiply', type: 'arrow' },
// { name: 'fetchData', type: 'function' }
// ]

文本处理

文本清洗

清洗和规范化文本数据:

// 去除 HTML 标签
function stripHtmlTags(html) {
return html.replace(/<[^>]+>/g, '');
}

// 去除多余空白
function normalizeWhitespace(text) {
return text
.replace(/\s+/g, ' ') // 多个空白变一个
.replace(/^\s+|\s+$/g, ''); // 去除首尾空白
}

// 去除特殊字符(保留字母、数字、中文)
function removeSpecialChars(text) {
return text.replace(/[^\w\u4e00-\u9fa5]/g, '');
}

// 测试
const dirtyText = " <p>Hello</p> 世界!@#$%^&*() ";
console.log(stripHtmlTags(dirtyText)); // " Hello 世界!@#$%^&*() "
console.log(normalizeWhitespace(dirtyText)); // "<p>Hello</p> 世界!@#$%^&*()"
console.log(removeSpecialChars(dirtyText)); // "pHellop世界"

命名转换

在不同命名风格之间转换:

// 驼峰转下划线 (camelCase -> snake_case)
function camelToSnake(str) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase();
}

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

// 驼峰转短横线 (camelCase -> kebab-case)
function camelToKebab(str) {
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}

// 短横线转驼峰 (kebab-case -> camelCase)
function kebabToCamel(str) {
return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
}

// 测试
console.log(camelToSnake("userName")); // "user_name"
console.log(snakeToCamel("user_name")); // "userName"
console.log(camelToKebab("backgroundColor")); // "background-color"
console.log(kebabToCamel("background-color")); // "backgroundColor"

敏感信息脱敏

// 手机号脱敏:138****5678
function maskPhone(phone) {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}

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

// 银行卡号脱敏:6222 **** **** 7890
function maskBankCard(card) {
return card.replace(/(\d{4})\d+(\d{4})/, '$1 **** **** $2');
}

// 邮箱脱敏:u***@example.com
function maskEmail(email) {
return email.replace(/(.{1}).*(@.*)/, '$1***$2');
}

// 通用脱敏函数
function maskSensitive(text, type) {
const patterns = {
phone: { pattern: /(\d{3})\d{4}(\d{4})/g, replacement: '$1****$2' },
idCard: { pattern: /(\d{6})\d{8}(\d{4})/g, replacement: '$1********$2' },
bankCard: { pattern: /(\d{4})\d+(\d{4})/g, replacement: '$1 **** **** $2' },
email: { pattern: /(.{1}).*(@.*)/g, replacement: '$1***$2' }
};

const config = patterns[type];
if (!config) return text;

return text.replace(config.pattern, config.replacement);
}

// 测试
console.log(maskPhone("13812345678")); // "138****5678"
console.log(maskIdCard("110105199001011234")); // "110105********1234"
console.log(maskBankCard("6222021234567890")); // "6222 **** **** 7890"
console.log(maskEmail("[email protected]")); // "u***@example.com"

数字格式化

// 添加千位分隔符:1234567 -> 1,234,567
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

// 保留小数位数
function formatDecimal(num, decimals = 2) {
const fixed = num.toFixed(decimals);
return fixed.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}

// 格式化货币
function formatCurrency(amount, currency = '¥') {
const formatted = amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return `${currency}${formatted}`;
}

// 测试
console.log(formatNumber(1234567)); // "1,234,567"
console.log(formatDecimal(1234567.89123, 2)); // "1,234,567.89"
console.log(formatCurrency(1234567.89)); // "¥1,234,567.89"

日志分析

解析 Apache/Nginx 访问日志

import re
from collections import Counter

# Apache/Nginx 通用日志格式
log_pattern = re.compile(
r'(?P<ip>\S+)\s+' # IP 地址
r'\S+\s+' # 身份(通常为 -)
r'\S+\s+' # 用户(通常为 -)
r'\[(?P<time>[^\]]+)\]\s+' # 时间
r'"(?P<method>\S+)\s+' # 请求方法
r'(?P<path>\S+)\s+' # 请求路径
r'(?P<protocol>[^"]+)"\s+' # 协议
r'(?P<status>\d+)\s+' # 状态码
r'(?P<size>\d+|-)' # 响应大小
)

def parse_log_line(line):
"""解析单行日志"""
match = log_pattern.match(line)
if match:
return match.groupdict()
return None

def analyze_logs(log_file):
"""分析日志文件"""
ip_counter = Counter()
path_counter = Counter()
status_counter = Counter()

with open(log_file, 'r') as f:
for line in f:
parsed = parse_log_line(line)
if parsed:
ip_counter[parsed['ip']] += 1
path_counter[parsed['path']] += 1
status_counter[parsed['status']] += 1

return {
'top_ips': ip_counter.most_common(10),
'top_paths': path_counter.most_common(10),
'status_codes': dict(status_counter)
}

# 示例日志行
sample_log = '192.168.1.1 - - [10/Oct/2023:13:55:36 +0800] "GET /index.html HTTP/1.1" 200 1234'
parsed = parse_log_line(sample_log)
print(parsed)
# {'ip': '192.168.1.1', 'time': '10/Oct/2023:13:55:36 +0800', ...}

解析应用日志

import re
from datetime import datetime

# 通用应用日志格式:[时间] [级别] [模块] 消息
log_pattern = re.compile(
r'\[(?P<time>[^\]]+)\]\s+'
r'\[(?P<level>\w+)\]\s+'
r'\[(?P<module>[^\]]+)\]\s+'
r'(?P<message>.+)'
)

def parse_app_log(line):
"""解析应用日志"""
match = log_pattern.match(line)
if match:
data = match.groupdict()
# 转换时间格式
try:
data['datetime'] = datetime.strptime(
data['time'], '%Y-%m-%d %H:%M:%S'
)
except ValueError:
pass
return data
return None

# 提取错误日志
def extract_errors(log_file):
"""提取所有错误日志"""
errors = []
with open(log_file, 'r') as f:
for line in f:
parsed = parse_app_log(line)
if parsed and parsed['level'] in ('ERROR', 'CRITICAL'):
errors.append(parsed)
return errors

# 示例
sample = '[2024-03-15 10:30:45] [ERROR] [Database] Connection failed: timeout'
print(parse_app_log(sample))
# {'time': '2024-03-15 10:30:45', 'level': 'ERROR', 'module': 'Database', 'message': 'Connection failed: timeout'}

文件处理

批量重命名文件

import re
import os

def batch_rename(directory, pattern, replacement, dry_run=True):
"""
批量重命名文件

参数:
directory: 目录路径
pattern: 正则表达式模式
replacement: 替换字符串
dry_run: 如果为 True,只显示将要执行的操作,不实际重命名
"""
regex = re.compile(pattern)
results = []

for filename in os.listdir(directory):
new_name = regex.sub(replacement, filename)
if new_name != filename:
results.append({
'old': filename,
'new': new_name
})
if not dry_run:
os.rename(
os.path.join(directory, filename),
os.path.join(directory, new_name)
)

return results

# 示例:将 photo_001.jpg 重命名为 image_001.jpg
# batch_rename('./photos', r'^photo_(\d+\.jpg)$', r'image_\1')

搜索文件内容

import re
import os

def search_in_files(directory, pattern, file_pattern='*'):
"""
在文件中搜索匹配的内容

参数:
directory: 搜索目录
pattern: 正则表达式模式
file_pattern: 文件名模式(如 '*.py')
"""
import fnmatch

regex = re.compile(pattern)
results = []

for root, dirs, files in os.walk(directory):
for filename in fnmatch.filter(files, file_pattern):
filepath = os.path.join(root, filename)
try:
with open(filepath, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
if regex.search(line):
results.append({
'file': filepath,
'line': line_num,
'content': line.strip()
})
except (UnicodeDecodeError, PermissionError):
continue

return results

# 示例:搜索所有 Python 文件中的 TODO 注释
# results = search_in_files('./src', r'TODO|FIXME', '*.py')

实用工具函数

高亮关键词

function highlightKeywords(text, keywords, tag = 'mark') {
// 转义关键词中的特殊字符
const escaped = keywords.map(k =>
k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
);

const pattern = new RegExp(`(${escaped.join('|')})`, 'gi');
return text.replace(pattern, `<${tag}>$1</${tag}>`);
}

// 测试
const text = "JavaScript is awesome. I love JavaScript!";
console.log(highlightKeywords(text, ['JavaScript', 'awesome']));
// "<mark>JavaScript</mark> is <mark>awesome</mark>. I love <mark>JavaScript</mark>!"

生成随机字符串

import re
import random
import string

def generate_password(length=12, options=None):
"""
生成符合特定规则的密码

参数:
length: 密码长度
options: 字符类型配置
"""
defaults = {
'lower': True,
'upper': True,
'digits': True,
'special': True,
'special_chars': '!@#$%^&*'
}
options = {**defaults, **(options or {})}

chars = ''
if options['lower']:
chars += string.ascii_lowercase
if options['upper']:
chars += string.ascii_uppercase
if options['digits']:
chars += string.digits
if options['special']:
chars += options['special_chars']

# 确保至少包含每种类型的字符
password = []
if options['lower']:
password.append(random.choice(string.ascii_lowercase))
if options['upper']:
password.append(random.choice(string.ascii_uppercase))
if options['digits']:
password.append(random.choice(string.digits))
if options['special']:
password.append(random.choice(options['special_chars']))

# 填充剩余长度
remaining = length - len(password)
password.extend(random.choices(chars, k=remaining))

# 打乱顺序
random.shuffle(password)
return ''.join(password)

# 测试
print(generate_password(16))
# 示例输出: "xY3!mN7@kL9#pQ2$"

小结

本章通过多个实际案例展示了正则表达式的应用场景:

  • 数据验证:邮箱、手机号、身份证、密码、IP 地址等
  • 数据提取:URL、Markdown 链接、JSON、代码结构等
  • 文本处理:清洗、转换、脱敏、格式化等
  • 日志分析:解析服务器日志、应用日志等
  • 文件处理:批量重命名、内容搜索等

关键要点:

  1. 先分析需求,再设计正则表达式
  2. 从简单版本开始,逐步完善
  3. 充分测试边界情况
  4. 注意性能和安全问题
  5. 有时简单字符串操作比正则表达式更好
实践建议
  • 使用在线工具(如 regex101.com)测试正则表达式
  • 为常用的正则表达式编写测试用例
  • 在生产环境中注意 ReDoS 攻击风险
  • 复杂场景考虑使用专门的解析器而非正则表达式