正则表达式,是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-255
  • 2[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 "![{$alt}]({$localPath})";
    }, $markdown);
    
    return [
        'images' => $images,
        'processed' => $processed,
    ];
}

// 使用
$markdown = '这是一篇文章,包含图片:![示例图片](https://example.com/image.jpg)和![另一张](https://example.com/photo.png)。';
$result = processMarkdownImages($markdown);
print_r($result['images']);
echo $result['processed'];

正则解析

  • !:Markdown图片的感叹号
  • \[:左方括号(转义)
  • ([^\]]*):alt文本(捕获组1),不包含右方括号
  • \]:右方括号(转义)
  • \(:左括号(转义)
  • ([^)]+):图片URL(捕获组2),不包含右括号
  • \):右括号(转义)

注意事项

  • 这个正则能处理标准的Markdown图片语法alt
  • Markdown图片还有其他写法,如引用式![alt][ref]、带标题alt,需要额外处理。
  • 实际项目中,可以用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方法匹配等。
  • 路由匹配的性能,在路由数量很多时需要注意,可以用缓存优化。

正则表达式最佳实践

  1. 先写正则,再测试:写完正则后,用各种测试用例验证,包括正常情况、边界情况、异常情况。
  2. 使用在线工具:推荐用regex101.com、regexr.com等在线工具测试正则,实时查看匹配结果。
  3. 注释复杂正则:用x修饰符(PCRE_EXTENDED)可以在正则中加注释和空白,提高可读性。
$pattern = '/
    ^           # 开始
    [a-z]+      # 用户名
    @           # @符号
    [a-z.]+     # 域名
    \.[a-z]{2,} # 顶级域名
    $           # 结束
/x';
  1. 避免过度使用正则:能用字符串函数(strpos、substr、explode等)解决的,就不要用正则,性能更好。
  2. 注意贪婪匹配.是贪婪匹配,会尽可能多地匹配。需要非贪婪时用.?
  3. 使用分隔符:选择正则中不常出现的字符作为分隔符(如#~),避免转义斜杠。
  4. 性能优化:避免嵌套量词、避免回溯过多、用锚点^$限定位置。
  5. 中文处理:处理中文时用u修饰符(UTF-8),用\x{4e00}-\x{9fa5}匹配中文。
  6. 安全考虑:正则表达式可能被ReDoS(正则表达式拒绝服务)攻击,避免用户输入直接作为正则。
  7. 参考已有方案:常见的验证(邮箱、URL、IP等),优先用filter_var()等内置函数,不要自己造轮子。

总结

正则表达式,是PHP开发中非常强大的工具。

10个实战案例:

  1. 邮箱验证/^[a-zA-Z0-9.%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,或用filtervar
  2. 手机号验证/^1[3-9]\d{9}$/,覆盖13-19号段
  3. URL验证和提取:协议、域名、端口、路径、查询参数,或用parse_url
  4. IP地址验证/^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/,或用filter_var
  5. 身份证号验证:格式验证+校验位计算(ISO 7064:1983.MOD 11-2)
  6. 提取HTML链接/<a\s+[^>]href="&#039;["'][^>]>(.*?)<\/a>/is,或用DOMDocument
  7. 敏感词过滤:动态构建正则,preg_quote转义,按长度降序,等长替换
  8. Markdown图片处理/!\[([^\]]*)\]\(([^)]+)\)/,提取和替换图片URL
  9. 命名转换:驼峰转下划线/([a-z])([A-Z])/,下划线转驼峰用pregreplacecallback
  10. URL路由{param}转换为([^/]+),实现简单的路由匹配和参数提取

最佳实践:先写再测、用在线工具、注释复杂正则、避免过度使用、注意贪婪匹配、选择合适分隔符、性能优化、中文处理、安全考虑、参考已有方案。

正则表达式,虽然语法复杂,学习曲线陡峭,但掌握之后,能极大提升字符串处理的效率。希望这10个实战案例,能帮你把正则表达式真正用起来,在实际项目中游刃有余。

"工欲善其事,必先利其器。"正则表达式,就是PHP开发者的利器之一。