正则表达式(Regular Expression,简称regex或regexp),是一种用于匹配字符串的强大工具。它通过一系列特殊字符和语法,描述字符串的模式,可以用来查找、替换、验证、分割字符串。
在PHP开发中,正则表达式是非常常用的工具。验证邮箱、手机号、身份证号,提取网页中的链接、图片、数据,替换文本中的特定内容,分割字符串……这些都可以用正则表达式来完成。
PHP提供了两套正则表达式函数:POSIX扩展正则(ereg系列,PHP 5.3已废弃,PHP 7已移除)和PCRE(Perl兼容正则表达式,preg系列)。现在我们都用PCRE,也就是preg_*系列函数。
今天,我们来详细学习PHP正则表达式,从基础语法到常用函数,再到实际应用场景,帮你掌握这个强大的工具。
正则表达式基础
什么是正则表达式
正则表达式,是用一系列特殊字符描述的字符串模式。它可以匹配符合这个模式的字符串。
比如,/abc/ 可以匹配包含"abc"的字符串;/[0-9]+/ 可以匹配一个或多个数字;/^\w+@\w+\.\w+$/ 可以匹配邮箱格式。
定界符
在PHP中,正则表达式需要用定界符包裹。定界符可以是任意非字母数字、非反斜杠、非空白的字符,常用的是斜杠/,也可以用#、~、!等。
$pattern = '/abc/'; // 用/作为定界符
$pattern = '#abc#'; // 用#作为定界符
$pattern = '~abc~'; // 用~作为定界符如果正则表达式中包含定界符,需要转义,或者换一个定界符。比如匹配URL时,用#更方便:
$pattern = '/https?:\/\/.+/'; // 需要转义/
$pattern = '#https?://.+#'; // 用#就不用转义/修饰符
定界符后面可以加修饰符,改变正则的行为。
常用修饰符:
i:不区分大小写(case-insensitive)m:多行模式,^和$匹配每行的开头和结尾s:点号.匹配包括换行符在内的所有字符(dotall模式)x:忽略空白和注释u:UTF-8模式,处理多字节字符g:全局匹配(PHP中不需要,pregmatchall默认全局)
$pattern = '/abc/i'; // 不区分大小写,匹配ABC、Abc、abc等
$pattern = '/^abc$/m'; // 多行模式
$pattern = '/.+/s'; // 点号匹配换行符正则表达式语法
普通字符
普通字符(字母、数字、汉字等)匹配自身。
preg_match('/abc/', 'abcdef'); // 匹配,因为包含abc元字符
元字符是有特殊含义的字符,需要转义才能匹配自身。
常用元字符:. \ + * ? [ ] ^ $ { } ( ) | /
转义用反斜杠\:
preg_match('/a\.b/', 'a.b'); // 匹配a.b,\.匹配点号本身
preg_match('/a\+b/', 'a+b'); // 匹配a+b,\+匹配加号本身
preg_match('/a\?b/', 'a?b'); // 匹配a?b,\?匹配问号本身字符类
字符类用[]表示,匹配方括号中的任意一个字符。
preg_match('/[abc]/', 'a'); // 匹配a、b、c中的任意一个
preg_match('/[0-9]/', '5'); // 匹配0-9中的任意一个数字
preg_match('/[a-z]/', 'm'); // 匹配a-z中的任意一个小写字母
preg_match('/[A-Z]/', 'M'); // 匹配A-Z中的任意一个大写字母
preg_match('/[a-zA-Z0-9]/', 'x'); // 匹配字母和数字字符类中的特殊字符:
^:在字符类开头表示取反,匹配不在字符类中的字符-:表示范围,如a-z、0-9\:转义字符
preg_match('/[^0-9]/', 'a'); // 匹配非数字字符
preg_match('/[^abc]/', 'd'); // 匹配不是a、b、c的字符预定义字符类
PHP提供了一些预定义的字符类,用反斜杠加字母表示:
\d:匹配数字,等价于[0-9]\D:匹配非数字,等价于[^0-9]\w:匹配单词字符(字母、数字、下划线),等价于[a-zA-Z0-9_]\W:匹配非单词字符,等价于[^a-zA-Z0-9_]\s:匹配空白字符(空格、制表符、换行符等)\S:匹配非空白字符
preg_match('/\d+/', 'abc123'); // 匹配123
preg_match('/\w+/', 'hello_world'); // 匹配hello_world
preg_match('/\s+/', 'a b c'); // 匹配空格量词
量词指定前面的字符或分组重复的次数。
常用量词:
*:0次或多次(等价于{0,})+:1次或多次(等价于{1,})?:0次或1次(等价于{0,1}){n}:恰好n次{n,}:至少n次{n,m}:n到m次
preg_match('/ab*c/', 'ac'); // 匹配,b出现0次
preg_match('/ab*c/', 'abc'); // 匹配,b出现1次
preg_match('/ab*c/', 'abbbc'); // 匹配,b出现3次
preg_match('/ab+c/', 'ac'); // 不匹配,b至少出现1次
preg_match('/ab+c/', 'abc'); // 匹配
preg_match('/ab?c/', 'ac'); // 匹配,b出现0次
preg_match('/ab?c/', 'abc'); // 匹配,b出现1次
preg_match('/ab?c/', 'abbc'); // 不匹配,b最多出现1次
preg_match('/a{3}/', 'aaa'); // 匹配,a恰好出现3次
preg_match('/a{2,}/', 'aaaa'); // 匹配,a至少出现2次
preg_match('/a{2,4}/', 'aaa'); // 匹配,a出现2-4次贪婪与懒惰
量词默认是贪婪的,会尽可能多地匹配。在量词后面加?,变成懒惰模式,尽可能少地匹配。
$str = '<div>hello</div><div>world</div>';
// 贪婪模式,匹配最长的
preg_match('/<div>.*<\/div>/', $str, $matches);
echo $matches[0]; // 输出<div>hello</div><div>world</div>
// 懒惰模式,匹配最短的
preg_match('/<div>.*?<\/div>/', $str, $matches);
echo $matches[0]; // 输出<div>hello</div>锚点
锚点不匹配任何字符,只匹配位置。
常用锚点:
^:匹配字符串开头(多行模式下匹配行开头)$:匹配字符串结尾(多行模式下匹配行结尾)\b:匹配单词边界\B:匹配非单词边界
preg_match('/^abc/', 'abcdef'); // 匹配,abc在开头
preg_match('/^abc/', 'xabc'); // 不匹配,abc不在开头
preg_match('/abc$/', 'xabc'); // 匹配,abc在结尾
preg_match('/abc$/', 'abcx'); // 不匹配,abc不在结尾
preg_match('/\bword\b/', 'a word b'); // 匹配,word是独立单词
preg_match('/\bword\b/', 'sword'); // 不匹配,word不是独立单词分组
分组用()表示,把多个字符当作一个整体,可以对分组使用量词,也可以捕获分组匹配的内容。
preg_match('/(ab)+/', 'ababab'); // 匹配ababab,ab重复3次
preg_match('/(ab){2}/', 'abab'); // 匹配abab,ab恰好重复2次捕获分组:用$1、$2等引用分组匹配的内容。
preg_match('/(\d{4})-(\d{2})-(\d{2})/', '2015-08-04', $matches);
echo $matches[0]; // 整个匹配:2015-08-04
echo $matches[1]; // 第1个分组:2015
echo $matches[2]; // 第2个分组:08
echo $matches[3]; // 第3个分组:04非捕获分组:用(?:)表示,只分组不捕获。
preg_match('/(?:ab)+/', 'abab', $matches);
echo $matches[0]; // 整个匹配:abab
// 没有$matches[1],因为是非捕获分组或运算
用|表示或,匹配左边或右边的表达式。
preg_match('/cat|dog/', 'I have a cat'); // 匹配cat
preg_match('/cat|dog/', 'I have a dog'); // 匹配dog
preg_match('/gr(a|e)y/', 'gray'); // 匹配gray或grey反向引用
用\1、\2等在正则内部引用前面分组匹配的内容。
preg_match('/(\w+)\s+\1/', 'hello hello'); // 匹配,两个相同的单词
preg_match('/(\w+)\s+\1/', 'hello world'); // 不匹配断言(零宽断言)
断言不匹配字符,只匹配位置,用来判断某个位置前面或后面是什么。
(?=pattern):正向先行断言,后面是pattern(?!pattern):负向先行断言,后面不是pattern(?<=pattern):正向后行断言,前面是pattern(?<!pattern):负向后行断言,前面不是pattern
preg_match('/windows(?=95|98|NT|2000)/', 'windows2000'); // 匹配windows
preg_match('/windows(?!95|98|NT|2000)/', 'windowsXP'); // 匹配windows
preg_match('/(?<=95|98|NT|2000)windows/', '2000windows'); // 匹配windows
preg_match('/(?<!95|98|NT|2000)windows/', 'XPwindows'); // 匹配windowsPHP正则表达式函数
preg_match
执行一个正则匹配,只匹配第一次。
$pattern = '/\d+/';
$subject = 'abc123def456';
preg_match($pattern, $subject, $matches);
print_r($matches); // Array ( [0] => 123 )返回值:匹配次数(0或1),错误返回false。
pregmatchall
执行全局正则匹配,匹配所有结果。
$pattern = '/\d+/';
$subject = 'abc123def456';
preg_match_all($pattern, $subject, $matches);
print_r($matches); // Array ( [0] => Array ( [0] => 123 [1] => 456 ) )返回值:完整匹配次数,错误返回false。
preg_replace
执行正则替换。
$pattern = '/\d+/';
$replacement = 'NUM';
$subject = 'abc123def456';
$result = preg_replace($pattern, $replacement, $subject);
echo $result; // abcNUMdefNUM可以用数组批量替换:
$patterns = ['/\d+/', '/[a-z]+/'];
$replacements = ['NUM', 'LETTER'];
$subject = 'abc123def456';
$result = preg_replace($patterns, $replacements, $subject);
echo $result; // LETTERNUMLETTERNUM反向引用替换:
$pattern = '/(\d{4})-(\d{2})-(\d{2})/';
$replacement = '$3/$2/$1';
$subject = '2015-08-04';
$result = preg_replace($pattern, $replacement, $subject);
echo $result; // 04/08/2015preg_filter
和preg_replace类似,但只返回有匹配的结果。
$subject = ['abc123', 'def', 'ghi456'];
$result = preg_filter('/\d+/', 'NUM', $subject);
print_r($result); // Array ( [0] => abcNUM [2] => ghiNUM )preg_split
用正则分割字符串。
$pattern = '/[\s,]+/';
$subject = 'a b, c d';
$result = preg_split($pattern, $subject);
print_r($result); // Array ( [0] => a [1] => b [2] => c [3] => d )preg_grep
返回数组中匹配正则的元素。
$pattern = '/\d+/';
$array = ['abc', '123', 'def', '456'];
$result = preg_grep($pattern, $array);
print_r($result); // Array ( [1] => 123 [3] => 456 )preg_quote
转义正则表达式中的特殊字符。
$str = '$100.00 (USD)';
$pattern = '/' . preg_quote($str, '/') . '/';
echo $pattern; // /\$100\.00 \(USD\)/常用正则表达式
验证邮箱
$pattern = '/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/';
// 更严格的邮箱验证
$pattern = '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/';验证手机号
// 中国大陆手机号
$pattern = '/^1[3-9]\d{9}$/';验证身份证号
// 18位身份证号
$pattern = '/^\d{17}[\dXx]$/';
// 15位或18位
$pattern = '/^\d{15}$|^\d{17}[\dXx]$/';验证URL
$pattern = '/^https?:\/\/[\w\-]+(\.[\w\-]+)+([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?$/';验证IP地址
// IPv4
$pattern = '/^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/';验证日期
// YYYY-MM-DD
$pattern = '/^\d{4}-\d{2}-\d{2}$/';
// 更严格的日期验证(需要结合checkdate)验证中文字符
$pattern = '/^[\x{4e00}-\x{9fa5}]+$/u'; // UTF-8中文提取HTML标签内容
// 提取所有链接
$pattern = '/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/is';
preg_match_all($pattern, $html, $matches);
// 提取所有图片
$pattern = '/<img[^>]*src="([^"]*)"[^>]*>/is';
preg_match_all($pattern, $html, $matches);去除HTML标签
$text = preg_replace('/<[^>]+>/', '', $html);
// 或者用strip_tags()正则表达式性能优化
- 避免过度使用正则:能用字符串函数(strpos、str_replace、substr等)解决的,就不用正则。字符串函数比正则快得多。
- 使用锚点:如果匹配开头或结尾,用
^和$,避免不必要的回溯。 - 避免贪婪匹配:需要匹配到某个位置时,用懒惰模式
.?,或者用排除型字符类[^"]。 - 避免嵌套量词:如
(a+)*,会导致灾难性回溯。 - 使用非捕获分组:不需要捕获的分组,用
(?:),减少内存开销。 - 预编译正则:多次使用的正则,先赋值给变量,避免重复编译。
- 使用pregmatchall代替循环pregmatch:全局匹配用pregmatch_all,比循环高效。
- 限制匹配长度:对长字符串,先用substr截取需要的部分,再用正则。
常见坑
- 定界符冲突:正则中包含定界符时忘记转义,导致正则错误。解决:换一个不冲突的定界符,或转义。
- 修饰符遗漏:处理多行文本时忘记加
s修饰符,导致.不匹配换行符。 - UTF-8处理:处理中文时忘记加
u修饰符,导致匹配错误。 - 贪婪匹配:默认贪婪模式导致匹配过多内容,需要用懒惰模式。
- 字符类中的元字符:字符类中的
.、*、+等不需要转义,但^、-、]、\需要注意。 - pregreplace的/e修饰符:PHP 5.5已废弃,PHP 7已移除,用pregreplace_callback代替。
- 正则错误:正则语法错误时,preg函数返回false,用preglasterror()查看错误。
总结
正则表达式,是PHP开发中非常强大的工具。
核心要点:
- 基础语法:定界符、修饰符、普通字符、元字符、字符类、预定义字符类、量词、锚点、分组、或运算、反向引用、断言
- PHP函数:pregmatch(单次匹配)、pregmatchall(全局匹配)、pregreplace(替换)、pregsplit(分割)、preggrep(过滤数组)、preg_quote(转义)
- 常用正则:邮箱、手机号、身份证、URL、IP、日期、中文、HTML提取
- 性能优化:避免过度使用、用锚点、避免贪婪、避免嵌套量词、用非捕获分组、预编译
- 常见坑:定界符冲突、修饰符遗漏、UTF-8处理、贪婪匹配、字符类元字符
正则表达式虽然强大,但也不要滥用。能用简单字符串函数解决的,就不用正则。正则表达式适合处理复杂的模式匹配,简单的字符串操作用字符串函数更高效。
掌握正则表达式,能让你的PHP开发更加高效。希望这篇文章能帮你更好地理解和使用正则表达式。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录