一、欢迎来到“正则魔法学校”
那天深夜,我盯着屏幕上这行代码已经两个小时:
// 用户输入的电话号码,格式五花八门
$userInput = "我电话是138-0013-8000,微信同号哦~";
我需要提取电话号码。用strpos?用explode?试了十几种方法,写了50行代码,结果发现用户还可能输入“13800138000”、“138 0013 8000”、“+86-13800138000”……
就在我准备砸键盘时,前辈飘过来看了一眼:“用正则啊,一行搞定。”
正则表达式——这个听起来像数学课噩梦的名词,其实是程序员世界里的文字魔法。今天,就让我带你走进这个奇妙世界,保证不说“废话文学”,只讲“实战干货”!
二、魔法入门:正则表达式到底是什么鬼?
2.1 现实版“大家来找茬”
想象一下:你面前有1000份简历,要快速找出所有留了手机号的人。你会怎么找?
“找11位数字组合”——这就是最原始的正则思维!
在PHP中,正则表达式就是一套模式匹配规则,专门用来在字符串大海里捞针。更妙的是,这根“针”可以有弹性:可以是具体字符,也可以是某种“类型”的字符。
2.2 PHP的两种“魔法杖”
PHP给了我们两根魔法杖,各有特色:
// 第一根:PCRE魔法杖(推荐使用)
$pattern = '/\d{3}-\d{4}-\d{4}/'; // 看起来像颜文字,其实是正经模式
$result = preg_match($pattern, $string);
// 第二根:POSIX魔法杖(已过时,了解一下就行)
$result = ereg($pattern, $string); // PHP 7.0+ 已经移除了
简单来说:PCRE功能更强、速度更快,是我们今天的主角。
三、基础咒语:从零开始写模式
3.1 定界符——魔法的起手式
写正则的第一条规则:用分隔符包起来,通常用/,就像魔法阵的画框:
$pattern = '/abc/'; // 寻找字符串中的"abc"
但如果你要找的内容包含/怎么办?这时候可以换其他符号,我个人的选择习惯是:
$pattern = '#https?://#i'; // 匹配http://或https://,用#号做分隔符
$pattern = '~/\d+~'; // 匹配斜杠加数字,用~号
3.2 元字符——魔法世界的特殊符号
这些是正则的“关键字”,需要背下来(别怕,就几个):
// 最常用的几个元字符
$pattern = '/^abc$/'; // ^开头,$结尾,表示完全匹配"abc"
$pattern = '/a.c/'; // .匹配任意单个字符(除了换行)
$pattern = '/ab*c/'; // *前面的b出现0次或多次
$pattern = '/ab+c/'; // +前面的b出现1次或多次
$pattern = '/ab?c/'; // ?前面的b出现0次或1次
$pattern = '/a{2,4}/'; // a出现2到4次
3.3 字符类——你的“通配符卡”
// 匹配数字
$pattern = '/\d/'; // 匹配一个数字,等同于[0-9]
$pattern = '/\D/'; // 匹配非数字
// 匹配单词字符(字母、数字、下划线)
$pattern = '/\w/'; // 等同于[a-zA-Z0-9_]
$pattern = '/\W/'; // 非单词字符
// 匹配空白字符
$pattern = '/\s/'; // 空格、制表符、换行等
$pattern = '/\S/'; // 非空白字符
// 自定义字符类
$pattern = '/[aeiou]/'; // 匹配任何一个元音字母
$pattern = '/[^aeiou]/'; // ^在[]内表示“非”,匹配非元音字母
$pattern = '/[a-z]/i'; // i修饰符表示不区分大小写
四、实战演练:7个真实场景代码示例
示例1:验证邮箱格式(经典面试题)
function validateEmail($email) {
$pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
if (preg_match($pattern, $email)) {
return "邮箱格式正确!🎉";
} else {
return "兄弟,你这邮箱有点不对劲啊~";
}
}
// 测试一下
echo validateEmail("hello@example.com"); // 正确
echo validateEmail("test@com"); // 错误(域名太短)
echo validateEmail("@example.com"); // 错误(没用户名)
注意:这个正则能覆盖90%的情况,但100%完美的邮箱正则不存在(RFC标准太复杂)。实际项目中可以考虑用filter_var($email, FILTER_VALIDATE_EMAIL)。
示例2:提取文章中的所有图片链接
$article = "
<p>看看我拍的照片:</p>
<img src='https://example.com/img1.jpg' alt='美景'>
<img src='/uploads/img2.png' width='100'>
<div>这里是文字内容</div>
<img src='img3.gif'>
";
$pattern = '/<img\s+[^>]*src=[\'"]([^\'"]+)[\'"][^>]*>/i';
preg_match_all($pattern, $article, $matches);
echo "找到" . count($matches[1]) . "张图片:\n";
print_r($matches[1]);
/*
输出:
Array
(
[0] => https://example.com/img1.jpg
[1] => /uploads/img2.png
[2] => img3.gif
)
*/
示例3:密码强度验证
function checkPasswordStrength($password) {
$errors = [];
// 至少8位
if (!preg_match('/^.{8,}$/', $password)) {
$errors[] = "密码至少8位";
}
// 包含大小写字母
if (!preg_match('/[a-z]/', $password) || !preg_match('/[A-Z]/', $password)) {
$errors[] = "需要同时包含大小写字母";
}
// 包含数字
if (!preg_match('/\d/', $password)) {
$errors[] = "至少包含一个数字";
}
// 包含特殊字符
if (!preg_match('/[!@#$%^&*(),.?":{}|<>]/', $password)) {
$errors[] = "至少一个特殊字符";
}
return empty($errors) ? "密码强度💪" : implode(",", $errors);
}
echo checkPasswordStrength("Weak"); // 一堆错误
echo checkPasswordStrength("StrongP@ss1"); // 密码强度💪
示例4:智能提取手机号(应对各种格式)
function extractPhoneNumbers($text) {
// 匹配11位手机号,允许空格、横杠分隔
$pattern = '/(?:\+86)?[-\s]?1[3-9]\d[-\s]?\d{4}[-\s]?\d{4}/';
preg_match_all($pattern, $text, $matches);
// 清理格式:移除所有非数字字符(除了开头的+86)
$phones = [];
foreach ($matches[0] as $phone) {
// 保留+86,其他只留数字
if (strpos($phone, '+86') === 0) {
$cleaned = '+86' . preg_replace('/\D/', '', substr($phone, 3));
} else {
$cleaned = preg_replace('/\D/', '', $phone);
}
$phones[] = $cleaned;
}
return array_unique($phones); // 去重
}
$testText = "
联系方式:
王经理:138-0013-8000
李总:139 1234 5678
客服:+86-156-8888-9999
诈骗电话:191-9988-7766(虚拟运营商)
";
print_r(extractPhoneNumbers($testText));
示例5:解析简单JSON(正则不是万能的,但有时很管用)
// 注意:对于完整的JSON,请用json_decode()!
// 这里只是展示正则的能力边界
$miniJson = '{"name": "张三", "age": 25, "city": "北京"}';
// 提取所有键值对
$pattern = '/"([^"]+)":\s*("[^"]*"|\d+|true|false|null)/';
preg_match_all($pattern, $miniJson, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
echo "键: {$match[1]}, 值: {$match[2]}\n";
}
示例6:Markdown标题转HTML
function markdownToHtml($markdown) {
// 转换#标题
$html = preg_replace('/^#\s+(.+)$/m', '<h1>$1</h1>', $markdown);
$html = preg_replace('/^##\s+(.+)$/m', '<h2>$1</h2>', $html);
$html = preg_replace('/^###\s+(.+)$/m', '<h3>$1</h3>', $html);
// 转换**粗体**
$html = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $html);
// 转换*斜体*
$html = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $html);
return $html;
}
$md = "# 主标题\n这是内容**加粗文字**和*斜体*。\n## 子标题";
echo markdownToHtml($md);
示例7:清理用户输入的XSS攻击
function safeInput($input) {
// 移除<script>标签及其内容
$clean = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $input);
// 移除on事件属性(如onclick、onload等)
$clean = preg_replace('/\s+on\w+\s*=\s*["\'][^"\']*["\']/i', '', $clean);
// 移除javascript:协议链接
$clean = preg_replace('/href=["\']\s*javascript:/i', 'href="#"', $clean);
return htmlspecialchars($clean, ENT_QUOTES, 'UTF-8');
}
$dangerInput = '<script>alert("哈哈")</script><a href="javascript:evil()">点我</a>';
echo safeInput($dangerInput); // 安全的内容
五、高级技巧:从“会用”到“精通”
5.1 分组捕获——魔法中的魔法
// 提取日期各部分
$date = "2023-12-25";
$pattern = '/(\d{4})-(\d{2})-(\d{2})/';
preg_match($pattern, $date, $matches);
echo "年: {$matches[1]}, 月: {$matches[2]}, 日: {$matches[3]}";
// 输出:年: 2023, 月: 12, 日: 25
// 命名分组(更清晰)
$pattern = '/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/';
preg_match($pattern, $date, $matches);
echo $matches['year']; // 2023
5.2 贪婪 vs 非贪婪匹配
这是正则最容易踩的坑!
$html = '<div>标题</div><div>内容</div>';
// 贪婪匹配(默认):尽可能多吃
preg_match('/<div>.*<\/div>/', $html, $match);
echo $match[0]; // 匹配整个字符串:<div>标题</div><div>内容</div>
// 非贪婪匹配:吃到不饿就行
preg_match('/<div>.*?<\/div>/', $html, $match);
echo $match[0]; // 只匹配第一个:<div>标题</div>
记忆口诀:*? +? ?? {n,}?——加问号变“谦让”。
5.3 零宽断言——正则里的“透视眼”
// 找后面是.com的域名
$text = "example.com example.net example.org";
$pattern = '/\w+(?=\.com)/'; // 正向肯定预查
preg_match_all($pattern, $text, $matches);
print_r($matches[0]); // Array([0] => example)
// 找后面不是.com的域名
$pattern = '/\w+(?!\.com)/'; // 正向否定预查
preg_match_all($pattern, $text, $matches);
print_r($matches[0]); // 包含example(来自.net和.org)
// 找前面是"域名:"的单词
$text = "域名:baidu.com 域名:google.com";
$pattern = '/(?<=域名:)\w+/'; // 反向肯定预查
preg_match_all($pattern, $text, $matches);
print_r($matches[0]); // Array([0] => baidu [1] => google)
六、性能优化与调试技巧
6.1 正则性能陷阱
// 灾难性的回溯(Catastrophic Backtracking)
$badPattern = '/(a+)+b/'; // 多个a后面跟b
$testString = str_repeat('a', 100) . 'c'; // 100个a加c,没有b
// 这会卡很久!因为正则引擎会尝试所有可能的a的组合
// preg_match($badPattern, $testString); // 慎运行!
// 优化方案:避免嵌套的量词
$goodPattern = '/a+b/'; // 简单直接
6.2 调试技巧
// 1. 先用简单模式测试
$pattern = '/\d+/'; // 先测试是否匹配数字
// 2. 分步构建复杂模式
$pattern = '/^'; // 开头
$pattern .= '\d{3,4}'; // 区号
$pattern .= '-\d{7,8}'; // 号码
$pattern .= '$/'; // 结尾
// 3. 使用在线工具验证(如 regex101.com)
// 4. PHP的错误信息
$result = @preg_match('/invalid (pattern/', 'test');
if (preg_last_error() !== PREG_NO_ERROR) {
echo "正则错误: " . preg_last_error_msg();
// 输出:正则错误: Compilation failed: missing ) at offset 15
}
七、常见坑点与解决方案
坑点1:中文匹配问题
// 错误做法:用\w匹配中文
$pattern = '/\w+/u'; // 加u修饰符,\w能匹配中文吗?不能!
// 正确做法:用Unicode属性或范围
$pattern = '/[\x{4e00}-\x{9fa5}]+/u'; // 匹配中文字符
$pattern = '/\p{Han}+/u'; // 匹配汉字(需要PCRE>=8.10)
$text = "Hello 世界!";
preg_match_all('/[\x{4e00}-\x{9fa5}]/u', $text, $matches);
print_r($matches[0]); // Array([0] => 世 [1] => 界)
坑点2:多行匹配模式
$text = "第一行\n第二行\n第三行";
// m修饰符:^和$匹配每行的开头结尾
preg_match_all('/^第.行$/m', $text, $matches);
print_r($matches[0]); // 匹配所有三行
// s修饰符:让.匹配换行符
$multiline = "<div>\n内容\n</div>";
preg_match('/<div>(.*)<\/div>/s', $multiline, $match);
echo $match[1]; // 包含换行符的"内容"
坑点3:特殊字符转义
// 错误:直接使用用户输入作为模式
$userSearch = $_GET['q']; // 用户可能输入"test.+*"
$badPattern = "/$userSearch/"; // 如果用户输入包含正则元字符就崩了
// 正确:用preg_quote转义
$safePattern = '/' . preg_quote($userSearch, '/') . '/';
八、综合实战:一个完整的表单验证类
class FormValidator {
public static function validateEmail($email) {
$pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';
return preg_match($pattern, $email);
}
public static function validateChineseName($name) {
// 2-4个中文字符
$pattern = '/^[\x{4e00}-\x{9fa5}]{2,4}$/u';
return preg_match($pattern, $name);
}
public static function validateIDCard($id) {
// 简单版身份证验证(15位或18位)
$pattern = '/^(\d{15}$|^\d{18}$|\d{17}(\d|X|x))$/';
return preg_match($pattern, $id);
}
public static function validateBankCard($card) {
// 银行卡号(16-19位数字)
$pattern = '/^\d{16,19}$/';
return preg_match($pattern, $card);
}
public static function extractLinks($text) {
// 提取URL链接
$pattern = '/https?:\/\/[^\s"\'<>]+/';
preg_match_all($pattern, $text, $matches);
return $matches[0] ?? [];
}
public static function removeEmoji($text) {
// 移除emoji表情
$pattern = '/[\x{1F600}-\x{1F64F}\x{1F300}-\x{1F5FF}\x{1F680}-\x{1F6FF}]/u';
return preg_replace($pattern, '', $text);
}
}
// 使用示例
$data = [
'email' => 'test@example.com',
'name' => '张三',
'content' => '欢迎访问 https://example.com 👍'
];
if (!FormValidator::validateEmail($data['email'])) {
echo "邮箱格式错误";
}
$cleanContent = FormValidator::removeEmoji($data['content']);
$links = FormValidator::extractLinks($data['content']);
九、写在最后:正则学习的三个阶段
阶段一:看正则如天书(第一周)
- 感受:“这写的什么鬼?”
- 策略:抄! 先找现成模式,理解每个部分的意思
阶段二:能写简单模式(第一个月)
- 感受:“好像有点懂了”
- 策略:练! 从验证手机号、邮箱开始,每天写3个
阶段三:游刃有余(半年后)
- 感受:“这个问题可以用正则解决”
- 策略:优化! 考虑性能、可读性、边缘情况
最后的最后
正则表达式就像编程世界里的瑞士军刀——不是每天用,但用的时候真香。关键是:
- 别怕写错——我写了5年正则,依然要测试
- 别追求完美——能解决80%问题就行,剩下20%用其他方法
- 多用工具——regex101.com是你的好朋友
- 保持幽默——当正则让你抓狂时,记得这行代码:
// 传说中的“匹配一切”
$pattern = '//'; // 其实匹配空字符串
// 真正的“匹配任何字符”是:'/.?/s'
好了,现在你已经是“正则魔法学校”的合格毕业生了。下次看到奇怪的字符串,别再手动substr、strpos组合拳了,试试正则,你会发现——真香!

被折叠的 条评论
为什么被折叠?



