掌握 PHP 中的正则表达式.docx
《掌握 PHP 中的正则表达式.docx》由会员分享,可在线阅读,更多相关《掌握 PHP 中的正则表达式.docx(14页珍藏版)》请在冰豆网上搜索。
掌握PHP中的正则表达式
模式匹配是软件中最常见的内容,因此正则表达式这种特殊的简写方式得到了不断演变,从而实现任务简化。
在“掌握PHP中的正则表达式”系列的第1部分中了解如何在代码中使用这种简写方式。
所有机器都会消耗输入,执行某种工作,然后生成输出。
例如,电话把声能转换为电信号并重新转换回声频来启动对话。
发动机吸收燃料(蒸汽、裂变、汽油或者做大量的功)并将其转换为功。
又或者将朗姆酒、冰块、酸橙和柑桂酒倒入调酒壶中,并且用力搅拌制作麦泰(或者,如果您希望调制出更具有大都会特色的饮品,请尝试使用一点香槟酒和带果肉的梨汁饮料来享用贝利尼。
调酒壶真是一个灵活而又非凡的工具)。
由于软件将转换数据,因此每个应用程序也是一台机器——但是是一台“虚拟”机器,因为没有使用物理部件。
例如,编译器期望获得源代码作为输入并将其转化为适于执行的二进制代码。
气象建模工具将根据历史测量数据来生成预报。
而图像编辑器将消耗并生成像素,对每个像素或每组像素应用规则从而锐化图像或形成某种风格。
就像任何其他机器一样,软件应用程序期望获得某些“原料”,例如数字列表、XML模式中封装的数据或者协议。
如果给程序提供错误的“原料”——类型或形式不匹配——则很可能得到无法预测的结果,甚至导致灾难。
有句格言说得好,“错误的输入必然导致错误的输出”。
事实上,所有比较重要的问题都要求从错误数据中过滤出正确数据和/或拒绝错误数据以防止得到错误输出。
对于PHPWeb应用程序来说也是如此。
无论输入是来自手动形式还是来自编程式的AsynchronousJavaScript+XML(Ajax)请求,程序都必须在执行任何计算之前检查传入信息。
可能要求数值属于某个范围或者被限定为整数。
值可能需要匹配一种特定格式,如邮政编码。
例如,美国的邮政编码是五位数字加上可选的“4个”限定号码,后者由连字符和四位附加数字组成。
其他字符串可能是特定数目的字符,例如两个字母表示的美国各州的缩写。
字符串最为棘手:
PHP应用程序必须对嵌入SQL查询、JavaScript代码或者其他能够改变应用程序行为或有碍安全性的恶意操作程序保持警惕。
但是程序如何告知输入是数字还是遵循某个约定(例如邮政编码)?
基本上,执行匹配需要使用一个小型解析器——创建一个状态机、读取输入、处理标记、监视状态并生成结果。
但是,即使是一个简单的解析器,也难于进行创建和维护。
幸运的是,由于模式匹配分析是最常见的计算需求,因此,随着时间的推移,一种特殊的简写方式(引擎)应运而生(大约从UNIX®出现之后),它可以减轻事务的工作量。
正则表达式(regex)使用简明、易读的符号描述模式。
给定一个正则表达式和数据,正则表达式引擎将得到数据是否匹配模式及匹配内容(如果找到匹配)等结果。
下面是应用从UNIX命令行实用程序grep中提取的正则表达式的简单示例,该实用程序将在一个或多个UNIX文本文件的内容中搜索指定模式。
命令 grep-i-E'^Bat' 将搜索序列 beginning-of-line(用脱字符号[^]来表示),后面紧接着大写或小写字母 b、a 和 t(使用 -i 选项将在模式匹配时忽略大小写,举例来说,也就是 B 和 b 是等效的)。
因此,给出文件heroes.txt:
清单1.heroes.txt
Catwoman
Batman
TheTick
BlackCat
Batgirl
DangerGirl
WonderWoman
LukeCage
ThePunisher
AntMan
DeadGirl
Aquaman
SCUD
Blackbolt
MartianManhunter
上述grep命令将生成两个匹配:
Batman
Batgirl
正则表达式
PHP将提供两个regex编程接口,一个用于可移植操作系统接口(PortableOperatingSystemInterface,POSIX),另一个接口用于PerlCompatibleRegularExpressions(PCRE)。
基本上,推荐使用第二个接口,因为PCRE比POSIX实现更加强大,可以提供能在Perl中找到的所有操作符。
要了解关于POSIXregex函数调用的更多信息,请阅读PHP文档(请参阅 参考资料)。
在这里,我主要介绍PCRE功能。
PHPPCREregex包含针对特定字符和其他操作符、针对特定位置(例如字符串的开头或结尾)或者针对单词的开头或结尾的匹配操作符。
regex还可以描述替代词,即在其他技术中可能描述为“this”或“that”的单词;定长、变长或不确定长度的副本;字符集(例如,“a 到 m 之间的任意字母”);以及类 或各类字符(可打印字符或标点符号)。
regex中的特殊操作符也允许分组——一种将某个操作符应用到所有其他操作符的方法。
表1显示了一些常用的regex操作符。
您可以连接和结合表1中的基本操作符(以及其他操作符)并进行组合来构建(非常)复杂的regex。
表1.常见regex操作符
操作符
用途
.(句点)
匹配所有单个字符
^(脱字符号)
匹配出现在行或字符串开头的空字符串
$(美元符号)
匹配出现在行尾的空字符串
A
匹配大写字母 A
a
匹配小写字母 a
/d
匹配所有一位数字
/D
匹配所有单个非数字字符
/w
匹配所有单个字母或数字字符;同义词是 [:
alnum:
]
[A-E]
匹配所有大写的 A、B、C、D 或 E
[^A-E]
匹配除大写 A、B、C、D 或 E 之外的任何字符
X?
匹配出现零次或一次的大写字母 X
X*
匹配零个或多个大写字母 X
X+
匹配一个或多个大写字母 X
X{n}
精确匹配 n 个大写字母 X
X{n,m}
至少匹配 n 个且不多于 m 个大写字母 X;如果忽略 m,则表达式将尝试匹配至少 n 个 X
(abc|def)+
匹配一连串的(最少一个)abc 和 def;abc 和 def 将匹配
下面是regex的常见用法示例。
假定Web站点要求每个用户创建一个登录名。
每个用户名至少要包含3个但不多于10个字母数字字符,并且必须以字母为开头。
要强制遵守这些规范,可以使用以下regex在提交给应用程序时验证用户名:
^[A-Za-z][A-Za-z0-9_]{2,9}$。
脱字符号将匹配字符串的开头。
第一个集合 [A-Za-z] 表示所有字母。
第二个集合 [A-Za-z0-9_]{2,9} 表示由至少2个至多9个任意字母、数字和划线组成的序列。
并且使用美元符号($)匹配字符串末尾。
乍看之下,美元符号可能看似不必要,但是它是至关重要的。
如果忽略掉它,regex将匹配开头为字母、包含2至9个字母数字字符以及任意数目的任何其他字符的所有字符串。
换言之,没有美元符号锚定字符串的结尾,带有匹配前缀(例如“martin1234-cruft”)的非常长的字符串将生成误判(falsepositive)。
回页首
用PHP和regex编程
PHP提供了用于在文本中查找匹配、将每个匹配替换为其他文本(la搜索和替换)以及在列表的元素之中查找匹配的函数。
函数包括:
∙preg_match()
∙preg_match_all()
∙preg_replace()
∙preg_replace_callback()
∙preg_grep()
∙preg_split()
∙preg_last_error()
∙preg_quote()
为了演示函数,让我们编写一个小型PHP应用程序,该应用程序将搜索单词列表以查找特定模式,遵循这种模式的单词和regex都是由传统的Web表单来提供的,并且结果都使用 simpleprint_r() 函数返回给浏览器。
如果需要测试或改进regex,则这种小型程序非常有用。
清单2显示了PHP代码。
所有输入都是通过简单的HTML表单来提供的(为了简洁起见,不显示相应的表单,并且已经省略了用于捕捉PHP代码错误的代码)。
清单2.比较文本与模式
php
//
//dividethecomma-separatedlistintoindividualwords
//thethirdparameter,-1,permitsalimitlessnumberofmatches
//thefourthparameter,PREG_SPLIT_NO_EMPTY,ignoresemptymatches
//
$words=preg_split('/,/',$_REQUEST['words'],-1,PREG_SPLIT_NO_EMPTY);
//
//removetheleadingandtrailingspacesfromeachelement
//
foreach($wordsas$key=>$value){
$words[$key]=trim($value);
}
//
//findthewordsthatmatchtheregularexpression
//
$matches=preg_grep("/${_REQUEST['regex']}/",$words);
print_r($_REQUEST['regex']);
echo('
');
print_r($words);
echo('
');
print_r($matches);
exit;
?
>
首先,使用 preg_split() 函数把用逗号分隔的单词字符串分隔为单个元素。
此函数将在匹配给定regex的每个点上划分字符串。
在这里,regex只是 ,(一个逗号,以逗号分隔的列表中的分隔符)。
代码中的首尾斜杠只表示regex的开头和结尾。
preg_split() 的第三个参数和第四个参数都是可选的,但是每个参数都十分有用。
给第三个参数提供整数 n 将只返回前 n 个匹配;或者提供 -1 返回所有匹配。
如果指定第四个参数,标志 PREG_SPLIT_NO_EMPTY、preg_split() 将处理所有空结果。
接下来,用逗号分隔的单词列表中的每个元素都是通过 trim() 函数整理的(省略了开始和结束部分的空白),然后与提供的regex进行比较。
使用函数 preg_grep() 可以非常轻松地处理列表:
只需提供模式作为第一个参数,并提供要匹配的单词数组作为第二个参数。
函数将返回匹配数组。
例如,如果键入regex ^[A-Za-z][A-Za-z0-9_]{2,9}$ 作为模式和变长单词列表,则可能获得类似清单3的内容。
清单3.简单regex的结果
^[A-Za-z][A-Za-z0-9_]{2,9}$
Array([0]=>martin[1]=>1happy[2]=>hermanmunster)
Array([0]=>martin)
顺便说一句,您可以转化 preg_grep() 操作并查找与具有 PREG_GREP_INVERT 可选标志的模式(与命令行中的grep-v 相同)不 匹配的元素。
用$matches=preg_grep("/${_REQUEST['regex']}/",$words,PREG_GREP_INVERT) 替换第22行并重用清单3的输入,生成 Array([1]=>1happy[2]=>hermanmunster)。
回页首
分解字符串
函数 preg_split() 和 preg_grep() 是优秀的小函数。
如果使用可预测的模式分隔子字符串,则前者可以将字符串分解成几个子字符串。
函数 preg_grep() 还可以快速地过滤列表。
但是如果必须使用一个或多个复杂规则分解字符串会发生什么情况?
例如,美国的电话号码通常显示为“(305)555-1212”、“305-555-1212”或“305.555.1212”。
如果删除标点符号,所有电话号码都减少到10位,这样在使用regex /d{10} 时十分易于识别。
但是,美国的三位区号和三位电话号码前缀不能以零或一为开头(因为零和一是非本地呼叫的前缀)。
regex不会把数字序列分隔为单个数字并编写复杂的代码,而是会测试其有效性。
清单4显示了执行此任务的代码片段。
清单4.确定电话号码是否是有效的美国电话号码
php
$punctuation=preg_quote("().-");
$number=preg_replace("/[$punctuation]/",'',$_REQUEST['number']);
$valid="/[2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4}/";
if(preg_match($valid,$number)==1){
echo("${_REQUEST['number']}isvalid
");
}
exit;
?
>
让我们详细查看这段代码:
∙如 表1 所示,regex使用一小组操作符,例如方括号([]),对一个集合命名。
如果需要匹配对象文本中的这样一个操作符,则必须用前导反斜杠(/)来对regex中的操作符进行“转义”。
在转义了操作符后,它将像所有其他文字一样进行匹配。
例如,如果需要匹配一个句点字符,比方说,在完全限定主机名中查找句点,则编写 /.。
您可以随意将字符串传递给 preg_quote() 来自动转义它找到的所有regex操作符,如第1行所示。
如果在第1行后使用echo()$punctuation,则应当会看到 /(/)/.-。
∙第2行将删除电话号码中的所有标点符号。
preg_replace() 函数将把 $punctuation 中出现的所有某个字符替换为空字符串(因此,使用集合操作符 []),从而有效地省略字符。
新字符串将被返回并分配给 $number。
∙第4行定义有效的美国电话号码模式。
∙第5行执行匹配,把现在仅含数字的电话号码与模式进行比较。
如果有匹配,则函数 preg_match() 返回1。
如果未找到匹配,则 preg_match() 返回零。
如果在处理过程中出错,则函数将返回False。
因此,要检查是否成功,请查看返回值是否为1。
否则,查看 preg_last_error() 的结果(如果使用PHPV5.2.0或更高版本)。
如果不为零,则可能已经超出计算范围,例如超出regex可以递归的深度。
您可以在PCRERegularExpressionFunctions页面中查找关于PHPregex使用惯例和限制的讨论(请参阅 参考资料)。
回页首
捕捉
如果在数据验证中只需要进行“是否匹配?
”的测试,则有许多实例可用。
但是,regex更常用于检验匹配和提取关于匹配的信息。
返回到电话号码示例,如果找到匹配,您可能希望将区号、前缀和行号保存到数据库的独立字段中。
Regex可以记住与 capture 匹配的内容。
capture 操作符是一些括号,并且操作符可以出现在regex中的任意位置。
您还可以对捕捉进行嵌套,以查找较大捕捉的子分段。
例如,要捕捉10位电话号码中的区号、前缀和行号,您可以使用:
/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/
如果找到匹配,则把前三个数字捕捉到第一组括号中,把接下来的三个数字捕捉到第二组括号中;并且把最后四个数字捕捉到剩余操作符中。
preg_match() 调用的变体将检索捕捉。
清单5. preg_match() 如何检索捕捉
$valid="/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/";
if(preg_match($valid,$number,$matches)==1){
echo("${_REQUEST['number']}isvalid
");
echo("Entirematch:
${matches[0]}
");
echo("Areacode:
${matches[1]}
");
echo("Prefix:
${matches[2]}
");
echo("Number:
${matches[3]}
");
}
如果提供一个变量作为 preg_match() 的第三个参数,例如这里的 $matches,则它被设为捕捉结果列表。
第0个元素(索引编号为 0)是整个匹配;第1个元素是分别与第一组括号相关联的匹配,以此类推。
嵌套捕捉几乎可以捕捉任意深度的分段和子分段。
嵌套捕捉的诀窍在于预测每个匹配出现在匹配数组(例如$matches)中的位置。
下面是需要遵循的规则:
从regex的开头开始计算左括号的数目——该计数是匹配数组的索引。
清单6提供了用于提取街道地址片段(人为设计)的示例。
清单6.用于提取街道地址的代码
$address="123Main,Warsaw,NC,29876";
$valid="/((/d+)/s+(/w+)),/s+(/w+),/s+([A-Z]{2}),/s+(/d{5})/";
if(preg_match($valid,$address,$matches)==1){
echo("Street:
${matches[1]}
");
echo("Streetnumber:
${matches[2]}
");
echo("Streetname:
${matches[3]}
");
echo("City:
${matches[4]}
");
echo("State:
${matches[5]}
");
echo("Zip:
${matches[6]}
");
}
同样,整个匹配是在索引 0 处找到的。
在哪里找到街道编号?
从左侧开始计算,街道编号是由 /d+ 匹配的。
左括号是从左侧算起第二个;因此,$matches[2] 是 123。
$matches[4] 保存城市名称,而 $matches[6] 捕捉ZIP编码。
回页首
强大的技术
处理文本非常普通,并且PHP提供了一些功能使大量操作可以更轻松地完成。
下面是需要牢记的一些简写:
∙preg_replace() 函数可以对单个字符串或字符串数组进行操作。
如果对一个字符串数组而不是一个字符串调用preg_replace(),则数组中的所有元素都将进行替换。
在本例中,preg_replace() 将返回修改后的字符串数组。
∙正如其他PCRE实现一样,您可以从替换中引用子模式匹配,允许操作进行自引用。
为进行演示,考虑统一电话号码的格式。
所有标点符号都被删除,而使用点来代替。
清单7中显示了一种解决方案。
清单7.用点替代标点符号
$punctuation=preg_quote("().-");
$number=preg_replace("/[$]/",'',$_REQUEST['number']);
$valid="/([2-9][0-9]{2})([2-9][0-9]{2})([0-9]{4})/";
$standard=preg_replace($valid,"//1.//2.//3",$number);
if(strcmp($standard,$number)){
echo("Thestandardnumberis$standard
");
}
如果模式匹配,则模式测试和标准电话号码的转换将在一个步骤中完成。