正则表达式(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-z0-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');    // 匹配windows

PHP正则表达式函数

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/2015

preg_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()

正则表达式性能优化

  1. 避免过度使用正则:能用字符串函数(strpos、str_replace、substr等)解决的,就不用正则。字符串函数比正则快得多。
  2. 使用锚点:如果匹配开头或结尾,用^$,避免不必要的回溯。
  3. 避免贪婪匹配:需要匹配到某个位置时,用懒惰模式.?,或者用排除型字符类[^"]
  4. 避免嵌套量词:如(a+)*,会导致灾难性回溯。
  5. 使用非捕获分组:不需要捕获的分组,用(?:),减少内存开销。
  6. 预编译正则:多次使用的正则,先赋值给变量,避免重复编译。
  7. 使用pregmatchall代替循环pregmatch:全局匹配用pregmatch_all,比循环高效。
  8. 限制匹配长度:对长字符串,先用substr截取需要的部分,再用正则。

常见坑

  1. 定界符冲突:正则中包含定界符时忘记转义,导致正则错误。解决:换一个不冲突的定界符,或转义。
  2. 修饰符遗漏:处理多行文本时忘记加s修饰符,导致.不匹配换行符。
  3. UTF-8处理:处理中文时忘记加u修饰符,导致匹配错误。
  4. 贪婪匹配:默认贪婪模式导致匹配过多内容,需要用懒惰模式。
  5. 字符类中的元字符:字符类中的.*+等不需要转义,但^-]\需要注意。
  6. pregreplace的/e修饰符:PHP 5.5已废弃,PHP 7已移除,用pregreplace_callback代替。
  7. 正则错误:正则语法错误时,preg函数返回false,用preglasterror()查看错误。

总结

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

核心要点:

  1. 基础语法:定界符、修饰符、普通字符、元字符、字符类、预定义字符类、量词、锚点、分组、或运算、反向引用、断言
  2. PHP函数:pregmatch(单次匹配)、pregmatchall(全局匹配)、pregreplace(替换)、pregsplit(分割)、preggrep(过滤数组)、preg_quote(转义)
  3. 常用正则:邮箱、手机号、身份证、URL、IP、日期、中文、HTML提取
  4. 性能优化:避免过度使用、用锚点、避免贪婪、避免嵌套量词、用非捕获分组、预编译
  5. 常见坑:定界符冲突、修饰符遗漏、UTF-8处理、贪婪匹配、字符类元字符

正则表达式虽然强大,但也不要滥用。能用简单字符串函数解决的,就不用正则。正则表达式适合处理复杂的模式匹配,简单的字符串操作用字符串函数更高效。

掌握正则表达式,能让你的PHP开发更加高效。希望这篇文章能帮你更好地理解和使用正则表达式。