正则表达式,是PHP开发中非常强大的工具。它可以用来匹配、查找、替换、分割字符串,在表单验证、数据提取、文本处理、URL路由等场景中,都有广泛的应用。
在之前的文章中,我们学习了正则表达式的基础语法和进阶技巧。今天,我们来通过10个真实项目中的实战案例,看看正则表达式在实际开发中是怎么用的,帮你把正则表达式真正用起来。
每个案例,都包含需求分析、正则表达式、代码实现、注意事项,让你不仅知其然,更知其所以然。
案例1:邮箱验证
需求:验证用户输入的邮箱地址是否合法。
正则表达式:
/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/代码实现:
function validateEmail($email) {
$pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
return preg_match($pattern, $email) === 1;
}
// 使用
var_dump(validateEmail('user@example.com')); // true
var_dump(validateEmail('user.name+tag@example.co.uk')); // true
var_dump(validateEmail('invalid-email')); // false
var_dump(validateEmail('user@.com')); // false正则解析:
^:字符串开始[a-zA-Z0-9._%+-]+:用户名部分,允许字母、数字、点、下划线、百分号、加号、减号,至少一个字符@:@符号[a-zA-Z0-9.-]+:域名部分,允许字母、数字、点、减号\.:点号(转义)[a-zA-Z]{2,}:顶级域名,至少2个字母$:字符串结束
注意事项:
- 这个正则能验证大多数邮箱,但不是100%符合RFC标准。RFC 5322的邮箱规则非常复杂,完全实现需要更复杂的正则。
- 实际项目中,这个正则已经足够用了。更严格的验证,可以用filter_var():
filter_var($email, FILTER_VALIDATE_EMAIL) !== false;- 邮箱验证后,最好发送验证邮件,确认邮箱真实存在。
案例2:手机号验证
需求:验证中国大陆手机号是否合法。
正则表达式:
/^1[3-9]\d{9}$/代码实现:
function validatePhone($phone) {
$pattern = '/^1[3-9]\d{9}$/';
return preg_match($pattern, $phone) === 1;
}
// 使用
var_dump(validatePhone('13812345678')); // true
var_dump(validatePhone('19912345678')); // true(199号段)
var_dump(validatePhone('12345678901')); // false(12开头)
var_dump(validatePhone('1381234567')); // false(10位)正则解析:
^1:以1开头[3-9]:第二位是3-9(覆盖13、14、15、16、17、18、19号段)\d{9}:后面9位数字$:字符串结束
注意事项:
- 2015年的号段主要是13、14、15、17、18,后来增加了16、19等号段。用
[3-9]可以覆盖现有和未来的号段。 - 如果需要更严格,可以列出具体号段:
/^1(3[0-9]|4[579]|5[0-35-9]|6[6]|7[0135678]|8[0-9]|9[89])\d{8}$/ - 手机号验证后,最好发送短信验证码,确认手机号真实。
案例3:URL验证和提取
需求:验证URL是否合法,并提取URL的各个部分(协议、域名、路径、查询参数)。
正则表达式:
/^(https?):\/\/([^\/:]+)(:\d+)?(\/[^\?]*)?(\?.*)?$/代码实现:
function parseUrl($url) {
$pattern = '/^(https?):\/\/([^\/:]+)(:\d+)?(\/[^\?]*)?(\?.*)?$/';
if (preg_match($pattern, $url, $matches)) {
return [
'scheme' => $matches[1],
'host' => $matches[2],
'port' => isset($matches[3]) ? ltrim($matches[3], ':') : null,
'path' => isset($matches[4]) ? $matches[4] : '/',
'query' => isset($matches[5]) ? ltrim($matches[5], '?') : null,
];
}
return false;
}
// 使用
$result = parseUrl('https://www.example.com:8080/article/123?page=1&sort=desc');
print_r($result);
// Array
// (
// [scheme] => https
// [host] => www.example.com
// [port] => 8080
// [path] => /article/123
// [query] => page=1&sort=desc
// )正则解析:
(https?):协议,http或https:\/\/:://(转义斜杠)([^\/:]+):域名,不包含斜杠和冒号(:\d+)?:端口,可选,冒号加数字(\/[^\?]*)?:路径,可选,以斜杠开头,不包含问号(\?.*)?:查询字符串,可选,问号加任意字符
注意事项:
- 这个正则能处理大多数URL,但不是100%符合RFC 3986。
- 实际项目中,更推荐用parse_url()函数:
$parts = parse_url($url);
// $parts包含scheme、host、port、path、query、fragment等- parse_url()更可靠,能处理更复杂的URL。
案例4:IP地址验证
需求:验证IPv4地址是否合法。
正则表达式:
/^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/代码实现:
function validateIpv4($ip) {
$pattern = '/^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/';
return preg_match($pattern, $ip) === 1;
}
// 使用
var_dump(validateIpv4('192.168.1.1')); // true
var_dump(validateIpv4('255.255.255.255')); // true
var_dump(validateIpv4('256.1.1.1')); // false(256超过255)
var_dump(validateIpv4('192.168.1')); // false(只有3段)
var_dump(validateIpv4('192.168.1.1.1')); // false(5段)正则解析: 每一段IP的规则:25[0-5]|2[0-4]\d|[01]?\d\d?
25[0-5]:250-2552[0-4]\d:200-249[01]?\d\d?:0-199(0-9、10-99、100-199)
然后{3}重复3次(带点号),最后一段不带点号。
注意事项:
- 这个正则能正确验证0-255的范围,比简单的
\d{1,3}更严格。 - 实际项目中,更推荐用filter_var():
filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;- 如果需要验证IPv6,用
FILTERFLAGIPV6。
案例5:身份证号验证
需求:验证中国大陆18位身份证号是否合法(包括校验位验证)。
正则表达式:
/^[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) {
// 基本格式验证
$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 (!preg_match($pattern, $idCard)) {
return false;
}
// 校验位验证(ISO 7064:1983.MOD 11-2)
$weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
$checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
$sum = 0;
for ($i = 0; $i < 17; $i++) {
$sum += intval($idCard[$i]) * $weights[$i];
}
$checkCode = $checkCodes[$sum % 11];
return strtoupper($idCard[17]) === $checkCode;
}
// 使用
var_dump(validateIdCard('110101199003071234')); // 需要真实校验位正则解析:
[1-9]\d{5}:地区码(6位,第一位非0)(18|19|20)\d{2}:出生年份(1800-2099)(0[1-9]|1[0-2]):出生月份(01-12)(0[1-9]|[12]\d|3[01]):出生日期(01-31)\d{3}:顺序码(3位)[\dXx]:校验位(数字或X)
注意事项:
- 正则只能验证格式,校验位需要额外计算。
- 这个正则没有验证日期的合法性(比如2月30日),需要额外检查。
- 实际项目中,身份证验证后,最好通过官方接口验证真实性。
案例6:提取HTML中的所有链接
需求:从HTML内容中提取所有的超链接(a标签的href属性)。
正则表达式:
/<a\s+[^>]*href=["']([^"']+)["'][^>]*>(.*?)<\/a>/is代码实现:
function extractLinks($html) {
$pattern = '/<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>(.*?)<\/a>/is';
preg_match_all($pattern, $html, $matches, PREG_SET_ORDER);
$links = [];
foreach ($matches as $match) {
$links[] = [
'href' => $match[1],
'text' => strip_tags($match[2]),
];
}
return $links;
}
// 使用
$html = '<div>
<a href="https://example.com" class="link">示例网站</a>
<a href="/article/123" title="文章">文章标题</a>
<a href="mailto:user@example.com">联系我们</a>
</div>';
$links = extractLinks($html);
print_r($links);
// Array
// (
// [0] => Array ([href] => https://example.com, [text] => 示例网站)
// [1] => Array ([href] => /article/123, [text] => 文章标题)
// [2] => Array ([href] => mailto:user@example.com, [text] => 联系我们)
// )正则解析:
<a\s+:a标签开始,后面至少一个空白字符[^>]*:a标签中href之前的其他属性href=["']:href属性,用单引号或双引号([^"']+):链接地址(捕获组1),不包含引号["']:结束引号[^>]*:href之后的其他属性>:a标签开始标签结束(.*?):链接文本(捕获组2),非贪婪匹配<\/a>:a标签结束- 修饰符:
i不区分大小写,s点号匹配换行符
注意事项:
- 正则处理HTML不是100%可靠,因为HTML结构可能不规范、有嵌套、有注释等。
- 更可靠的方法是用DOMDocument:
$dom = new DOMDocument();
@$dom->loadHTML($html);
$anchors = $dom->getElementsByTagName('a');
foreach ($anchors as $anchor) {
$href = $anchor->getAttribute('href');
$text = $anchor->nodeValue;
}- 简单场景用正则足够,复杂场景推荐用DOMDocument。
案例7:敏感词过滤
需求:过滤文本中的敏感词,替换为*号。
正则表达式:动态构建,将敏感词数组用|连接。
代码实现:
function filterSensitiveWords($text, $sensitiveWords) {
// 转义正则特殊字符
$escapedWords = array_map(function($word) {
return preg_quote($word, '/');
}, $sensitiveWords);
// 构建正则,按长度降序排列(先匹配长的)
usort($escapedWords, function($a, $b) {
return strlen($b) - strlen($a);
});
$pattern = '/' . implode('|', $escapedWords) . '/u';
// 替换为等长的*号
return preg_replace_callback($pattern, function($matches) {
return str_repeat('*', mb_strlen($matches[0]));
}, $text);
}
// 使用
$sensitiveWords = ['敏感词', '脏话', '广告'];
$text = '这是一个包含敏感词和脏话的文本,还有广告内容。';
$result = filterSensitiveWords($text, $sensitiveWords);
echo $result;
// 这是一个包含***和**的文本,还有**内容。注意事项:
- 用preg_quote()转义敏感词中的正则特殊字符,避免正则错误。
- 按长度降序排列,先匹配长的敏感词,避免短词先匹配导致长词被拆分。
- 用pregreplacecallback()实现等长替换,比简单的str_replace更友好。
- 修饰符
u支持UTF-8中文。 - 敏感词过滤不能100%防绕过(比如用谐音、拆字、特殊符号),需要结合其他方法。
案例8:Markdown图片提取和替换
需求:从Markdown内容中提取所有图片,并将图片URL替换为本地路径。
正则表达式:
/!\[([^\]]*)\]\(([^)]+)\)/代码实现:
function processMarkdownImages($markdown) {
$pattern = '/!\[([^\]]*)\]\(([^)]+)\)/';
// 提取所有图片
preg_match_all($pattern, $markdown, $matches, PREG_SET_ORDER);
$images = [];
foreach ($matches as $match) {
$images[] = [
'alt' => $match[1],
'url' => $match[2],
'raw' => $match[0],
];
}
// 替换图片URL为本地路径
$processed = preg_replace_callback($pattern, function($matches) {
$alt = $matches[1];
$url = $matches[2];
// 模拟下载并生成本地路径
$localPath = '/uploads/images/' . md5($url) . '.jpg';
return "";
}, $markdown);
return [
'images' => $images,
'processed' => $processed,
];
}
// 使用
$markdown = '这是一篇文章,包含图片:和。';
$result = processMarkdownImages($markdown);
print_r($result['images']);
echo $result['processed'];正则解析:
!:Markdown图片的感叹号\[:左方括号(转义)([^\]]*):alt文本(捕获组1),不包含右方括号\]:右方括号(转义)\(:左括号(转义)([^)]+):图片URL(捕获组2),不包含右括号\):右括号(转义)
注意事项:
- 这个正则能处理标准的Markdown图片语法
。 - Markdown图片还有其他写法,如引用式
![alt][ref]、带标题,需要额外处理。 - 实际项目中,可以用Markdown解析库(如Parsedown)来处理,更可靠。
案例9:驼峰命名和下划线命名互转
需求:将驼峰命名(camelCase)转为下划线命名(snake_case),或反向转换。
代码实现:
// 驼峰转下划线
function camelToSnake($camel) {
$pattern = '/([a-z])([A-Z])/';
$snake = preg_replace($pattern, '$1_$2', $camel);
return strtolower($snake);
}
// 下划线转驼峰
function snakeToCamel($snake, $ucfirst = false) {
$camel = preg_replace_callback('/_([a-z])/', function($matches) {
return strtoupper($matches[1]);
}, $snake);
return $ucfirst ? ucfirst($camel) : $camel;
}
// 使用
echo camelToSnake('userName'); // user_name
echo camelToSnake('getUserInfo'); // get_user_info
echo camelToSnake('APIResponse'); // a_p_i_response(注意:连续大写有问题)
echo snakeToCamel('user_name'); // userName
echo snakeToCamel('get_user_info'); // getUserInfo
echo snakeToCamel('user_name', true); // UserName正则解析(驼峰转下划线):
([a-z]):小写字母(捕获组1)([A-Z]):大写字母(捕获组2)- 替换为
$1_$2:在小写字母和大写字母之间加下划线
注意事项:
- 简单的驼峰转下划线,连续大写字母(如APIResponse)会有问题,会变成api_response。
- 更完善的转换:
function camelToSnake($camel) {
return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([A-Z]+)([A-Z][a-z])/'], '$1_$2', $camel));
}
// APIResponse -> api_response- 这种转换在数据库字段名和PHP变量名之间转换时很常用。
案例10:URL路由匹配
需求:实现简单的URL路由,将友好的URL匹配到对应的处理函数,并提取参数。
代码实现:
class Router {
private $routes = [];
// 添加路由
public function add($pattern, $handler) {
// 将路由模式中的{param}转换为正则捕获组
$regex = preg_replace('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', '([^/]+)', $pattern);
$regex = '#^' . $regex . '$#';
$this->routes[] = [
'pattern' => $pattern,
'regex' => $regex,
'handler' => $handler,
];
}
// 匹配路由
public function match($url) {
foreach ($this->routes as $route) {
if (preg_match($route['regex'], $url, $matches)) {
// 提取参数名
preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', $route['pattern'], $paramNames);
$params = [];
foreach ($paramNames[1] as $i => $name) {
$params[$name] = $matches[$i + 1];
}
return [
'handler' => $route['handler'],
'params' => $params,
];
}
}
return null;
}
}
// 使用
$router = new Router();
$router->add('/article/{id}', 'ArticleController@show');
$router->add('/user/{username}/post/{postId}', 'PostController@show');
$router->add('/page/{slug}', 'PageController@show');
$result = $router->match('/article/123');
print_r($result);
// Array
// (
// [handler] => ArticleController@show
// [params] => Array ([id] => 123)
// )
$result = $router->match('/user/zhangsan/post/456');
print_r($result);
// Array
// (
// [handler] => PostController@show
// [params] => Array ([username] => zhangsan, [postId] => 456)
// )正则解析:
\{([a-zA-Z][a-zA-Z0-9]*)\}:匹配{param}格式的参数占位符
- \{:左花括号(转义) - ([a-zA-Z][a-zA-Z0-9]*):参数名(字母或下划线开头,后面跟字母、数字、下划线) - \}:右花括号(转义)
- 替换为
([^/]+):匹配不包含斜杠的任意字符(捕获组)
注意事项:
- 这是一个简单的路由实现,实际框架(如Laravel、Symfony)的路由更复杂。
- 可以扩展支持可选参数
{param?}、正则约束{param:[0-9]+}、HTTP方法匹配等。 - 路由匹配的性能,在路由数量很多时需要注意,可以用缓存优化。
正则表达式最佳实践
- 先写正则,再测试:写完正则后,用各种测试用例验证,包括正常情况、边界情况、异常情况。
- 使用在线工具:推荐用regex101.com、regexr.com等在线工具测试正则,实时查看匹配结果。
- 注释复杂正则:用
x修饰符(PCRE_EXTENDED)可以在正则中加注释和空白,提高可读性。
$pattern = '/
^ # 开始
[a-z]+ # 用户名
@ # @符号
[a-z.]+ # 域名
\.[a-z]{2,} # 顶级域名
$ # 结束
/x';- 避免过度使用正则:能用字符串函数(strpos、substr、explode等)解决的,就不要用正则,性能更好。
- 注意贪婪匹配:
.是贪婪匹配,会尽可能多地匹配。需要非贪婪时用.?。 - 使用分隔符:选择正则中不常出现的字符作为分隔符(如
#、~),避免转义斜杠。 - 性能优化:避免嵌套量词、避免回溯过多、用锚点
^$限定位置。 - 中文处理:处理中文时用
u修饰符(UTF-8),用\x{4e00}-\x{9fa5}匹配中文。 - 安全考虑:正则表达式可能被ReDoS(正则表达式拒绝服务)攻击,避免用户输入直接作为正则。
- 参考已有方案:常见的验证(邮箱、URL、IP等),优先用filter_var()等内置函数,不要自己造轮子。
总结
正则表达式,是PHP开发中非常强大的工具。
10个实战案例:
- 邮箱验证:
/^[a-zA-Z0-9.%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,或用filtervar - 手机号验证:
/^1[3-9]\d{9}$/,覆盖13-19号段 - URL验证和提取:协议、域名、端口、路径、查询参数,或用parse_url
- IP地址验证:
/^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/,或用filter_var - 身份证号验证:格式验证+校验位计算(ISO 7064:1983.MOD 11-2)
- 提取HTML链接:
/<a\s+[^>]href="'["'][^>]>(.*?)<\/a>/is,或用DOMDocument - 敏感词过滤:动态构建正则,preg_quote转义,按长度降序,等长替换
- Markdown图片处理:
/!\[([^\]]*)\]\(([^)]+)\)/,提取和替换图片URL - 命名转换:驼峰转下划线
/([a-z])([A-Z])/,下划线转驼峰用pregreplacecallback - URL路由:
{param}转换为([^/]+),实现简单的路由匹配和参数提取
最佳实践:先写再测、用在线工具、注释复杂正则、避免过度使用、注意贪婪匹配、选择合适分隔符、性能优化、中文处理、安全考虑、参考已有方案。
正则表达式,虽然语法复杂,学习曲线陡峭,但掌握之后,能极大提升字符串处理的效率。希望这10个实战案例,能帮你把正则表达式真正用起来,在实际项目中游刃有余。
"工欲善其事,必先利其器。"正则表达式,就是PHP开发者的利器之一。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录