如何在 PHP 中处理文本

(再次)使用括号

在大多数情况下,使用一组括号可以定义子模式和捕捉匹配子模式的文本。但是,括号不需要捕捉子模式。正如在复杂的数学公式中,您可以简单地使用括号来给术语分组。

下面是一个示例。您能否说出它匹配哪类数据?

/[-a-z0-9]+(?:\.[-a-z0-9]+)*\.(?:com|edu|info)/i

您可能已经预料到此 regex 将匹配主机名(虽然只在 .com、.edu 和 .info 这几个域中)。差别是添加了 ?:。子模式限定符 ?: 将禁用捕捉,留下括号来阐明操作的优先次序。例如,在这里,短句 (?:\.[-a-z0-9]+)* 将匹配零个或多个字符串实例(例如 “.ibm”)。类似地,短句 \.(?:com|edu|info) 表示句点,后接字符串 com、edu 或 info 中的任意一个。

禁用捕捉可能看似毫无意义,直至您意识到捕捉需要额外的处理。如果代码将处理大量数据,则忽略捕捉可能是有意义的。此外,如果 regex 特别复杂,禁用某些子模式中的捕捉可以更轻松地提取真正感兴趣的子模式。

注:使用 regex 末尾的 i 修饰语可以使模式内的所有匹配都不区分大小写。因此,子集 a-z 将匹配所有字母,而不区分大小写。

PHP 将提供其他子模式修饰词。使用第 1 部分中提供的 regex 测试 jig(如 清单 1 所示),将针对候选字符串 “EDU”、“edu” 和 “Edu” 匹配 regex ((?i)edu)。如果子模式以修饰词 (?i) 为开头,则在子模式中进行匹配不区分大小写。只要子模式结束,区分大小写将被重新启用(将此修饰词与上面的 / ... /i 修饰词相比较,后者应用于整个模式)。


清单 1. 简单的 regex 测试实用程序
<?php
    //
    // divide the comma-separated list into individual words
    //   the third parameter, -1, permits a limitless number of matches
    //   the fourth parameter, PREG_SPLIT_NO_EMPTY, ignores empty matches
    //
    $words = preg_split( '/,/',  $_REQUEST[ 'words' ], -1, PREG_SPLIT_NO_EMPTY );

    //
    // remove the leading and trailing spaces from each element
    //
    foreach ( $words as $key => $value ) { 
        $words[ $key ] = trim( $value ); 
    }

    //
    // find the words that match the regular expression
    //
    $matches = preg_grep( "/${_REQUEST[ 'regex' ]}/", $words );

    print_r( $_REQUEST['regex' ] ); 
    echo( '<br /><br />' );
    
    print_r( $words ); 
    echo( '<br /><br />' );
    
    print_r( $matches );
    
    exit;
?>

另一个有用的子模式修饰词是 (?x)。它允许您在子模式中嵌入空白,使 regex 更易读。因而,子模式 ((?x) edu | com | info)(请注意备用操作符之间的空格,这些空格是为了易读性而添加的)与 (edu|com|info) 相同。您可以使用全局修饰词 / ... /x 在整个 regex 中嵌入空白和注释,如下所示:


清单 2. 嵌入空白和注释
$matches = preg_grep( 
            "/
              [- a-z 0-9]+            # machine name
              (?: \. [- a-z 0-9]+)*   # subdomains
              \. (?: com | edu | info)# domain
             /xi", $words );

正如您所见,还可以根据需要组合修饰词。另外,如果需要在使用 (?x) 时匹配空格,那么,使用元字符 \s 来匹配所有空格字符或使用 \ (反斜杠后接空格)来匹配单个空格,如 ((?x) hello \ there)。

其他应用

regex 的大量应用都是验证或分解存储为存储库中的数据或由应用程序立即执行的各个小块的输入。处理表单中的字段、解析 XML 代码以及解释协议都是典型应用。

regex 的另一个应用是格式化、规范化或提高数据的可读性。格式化不是使用 regex 查找和提取文本,而是使用 regex 查找并在正确位置插入文本。

下面是一个有用的格式化应用程序。假定 Web 表单把按照美元计算的薪金提交给应用程序。由于把薪金存储为整数,因此应用程序必须先去掉所粘贴数据中的标点符号,然后再保存。但是,在从存储库中检索出数据时,则需要使用逗号重新设定数据的格式使其具有可读性。下面显示了一个用于把美元金额转换为数字的简单 PHP 调用。


清单 3. 把美元金额转换为数字
$salary = preg_replace( "/[\$\s,]/", '', $_REQUEST[ 'salary' ] );

if ( is_numeric( $salary ) ) {
    // persist the data
}
else {
    // error
}

调用 preg_replace() 函数将用空字符串替换美元符号、所有空格和每个逗号,生成认为是整数的内容。如果调用 is_numeric() 对输入进行了验证,则可以存储数据。

接下来,让我们反向操作输出带有货币符号和用于分隔百、千、百万的逗号的数字。您可以编写代码来查找这些数字单元,也可以使用向前查找 和向后查找 在正确位置上插入逗号。子模式修饰词 ?<= 指示从当前位置开始向后查找(即向左查找)。修饰词 ?= 表示从当前位置开始向前查找(向右查找)。

那么,正确位置在哪里?字符串中左侧至少有一位数并且右侧有一组或多组三位数的任意位置,不包括小数点和美分数。给定该规则和两个查找修饰词(两者都是零宽度断言),这条语句将可成功执行:

$pretty_print = preg_replace( "/(?<=\d)(?=\d\d\d)+$)/", ',', $salary );

后面的 regex 如何工作?从字符串的开头开始并继续通过每个位置,regex 将断言 “左侧是否至少有一位数并且右侧是否有一组或多组三位数”?如果是这样,逗号将 “替换” 零宽度断言。

使用类似于上面的策略可以轻松地免除许多复杂匹配。例如,下面是另一种可以轻松解决一般困难的向前查找。


清单 4. 向前查找示例
$tab_data = preg_replace( '/
    ,                               # look for a comma
    (?=                             # then look ahead for
        (?:[^"]*$)                  # a string with no quotes and eol
        |                           #  -or-
        (?:[^"]*"[^"]*"[^"]*)*$     # a string with balanced quotes
    )                               # 
    /x', "\t", $csv_data );

这条 preg_replace() 指令将把一行用逗号分隔的数据转换为一行用制表符分隔的数据。它很聪明,不会替换在引号括起的字符串中找到的逗号。

regex 将在所有出现逗号(这是位于 regex 开头的逗号)的位置做出断言:“前面是不是没有引号或者前面的引号个数是否为偶数”?如果断言为真,则可以用制表符 (\t) 替换逗号。

如果不希望使用查找操作符,或者使用的是不提供查找操作符的语言,则可以使用传统 regex 把逗号嵌入到数字中,尽管这样做要求完成多次迭代。下面是一种可能的解决方案。


清单 5. 嵌入逗号
$pretty_print = preg_replace( "/[\$\s,]/", '', $_REQUEST[ 'salary' ] );

do {
    $old = $pretty_print;
    $pretty_print = preg_replace( "/(\d)(\d\d\d\b)/", "$1,$2", $pretty_print );
} while ( $old != $pretty_print );

让我们仔细研究一下代码。首先,移除 salary 参数的标点来模拟从数据库中读取整数。接下来,循环将重复执行,查找这样一个位置:一位数 ((\d) 后接三位数 ((\d\d\d\) 并在 \b 所指定的词界(word boundary)立即终止的位置。词界 是另一个零宽度断言并被定义为:

  • 如果第一个字符为单词字符,则在字符串中的第一个字符之前。
  • 如果最后一个字符为单词字符,则在字符串中的最后一个字符之后。
  • 在单词字符和非单词字符之间,紧跟在单词字符之后。
  • 在非单词字符和单词字符之间,紧跟在非单词字符之后。

因而,空格、句点和逗号都是有效的词界。

由于是外部循环,因此 regex 实质上将从右向左前进查找后接三位数和词界的一位数。如果找到匹配,则在两个子模式之间插入一个逗号。只要 preg_replace() 找到匹配,循环就必须继续,这解释了 $old != $pretty_print 条件。

贪婪和懒惰

Regex 十分强大。甚至有时候过于强大。例如,考虑当 regex ".*" 被应用到字符串 “The author of 'Wicked' also wrote 'Mirror, Mirror.'” 上时发生的情况。虽然预期 preg_match() 可能返回两个匹配,但是您可能会惊讶地发现只有一个结果:'Wicked' also wrote 'Mirror, Mirror.'

原因是什么?除非进行指定,否则诸如 *(无或多个)和 +(一个或多个)之类的操作符都很贪婪。如果模式可以继续匹配,那么它可能将生成最多的结果。要使匹配最少,则必须强制使某些操作符变得懒惰。懒惰操作将查找最短的匹配,然后就停止。要使操作符变得懒惰,请添加问号后缀。清单 6 显示了一个示例。


清单 6. 添加问号后缀
$text = 'The author of "Wicked" also wrote "Mirror, Mirror."';
    if ( preg_match_all( '/".*?"/', $text, $matches ) ) {
        print_r( $matches[0] );
    }

上面的代码片段将生成:

Array ( [0] => "Wicked" [1] => "Mirror, Mirror." )

regex ".*?" 变为匹配一个引号,后接刚好足够的 字符,后接一个引号。

但是,使用 * 操作符有时可能太懒惰。例如,采用以下代码片段。它将生成什么输出?


清单 7. 简单的 regex 测试实用程序
if (preg_match( "/([0-9]*)/", "-123", $matches  ) ) {
    print_r( $matches );
}

猜测输出是什么?“123”?“1”?没有输出?实际上,输出是 Array ( [0] => [1] => ),表示找到一个匹配,但是未捕捉到任何内容。为什么?回想一下操作符 * 可以匹配零次或多次。在这里,表达式 [0-9]* 针对字符串开头匹配零次,随后停止处理。

要解决此问题,请添加零宽度断言来锚定匹配,这将强制 regex 引擎继续进行匹配;/([0-9]*\b/ 就可解决问题。

更多提示和技巧

regex 可以解决简单或复杂的文本处理问题。首先掌握一些操作符,随着经验逐渐丰富,您可以进一步扩展词汇表。要立即开始使用,请参考下面这些提示和技巧。

用字符类实现可移植的 regex

您已经看到过匹配所有空格字符的元字符,例如 \s。此外,许多 regex 实现都支持更易于跨多种编写语言使用和移植的预定义字符类。例如,字符类 [:punct:] 表示当前语言环境中的所有标点字符。您可以使用 [:digit:] 代替 [0-9],并且 [:alpha:] 是比 [-a-zA-Z0-9_] 更具有可移植性的替代者。例如,您可以使用以下语句移除字符串中的所有标点符号:

$clean = preg_replace( "/[[:punct:]]/", '', $string );

使用字符类比清楚说明所有标点符号更简洁。要获得字符类的完整列表,请参阅适用于您的 PHP 版本的文档。

排除不需要查找的内容

与将逗号分隔的值 (CSV) 转换为用制表符分隔的数据一样,列出 需要匹配的内容有时更容易也更精确。以脱字符号 (^) 为开头的集合将匹配集合中不包括的所有字符。例如,您可以使用正则表达式 /[2-9][0-9]{2}[2-9][0-9]{2}[0-9]{4}/ 来验证美国电话号码。使用排除集合,可以把 regex 编写为更显式的 /[^01][0-9]{2}[^01][0-9]{2}[0-9]{4}/。两个 regex 都可以正常运行,但是显然后者意图更加明显。

跳过换行符

如果输入跨度多行,则使用典型的 regex 是不够的,因为扫描将在 $ 所指示的换行符处终止。但是,如果使用 s 或 m 修饰词,regex 引擎将按照不同的方式处理输入。前者将把字符串处理为单行,强制用点匹配换行符(它通常不这样做)。后者将把字符串处理为多行,其中 ^ 和 $ 将分别匹配每行的开头和结尾。下面是一个示例:如果设置 $string = "Hello,\nthere";,则语句 preg_match( "/.*/s", $string, $matches) 将把 $matches[0] 设为 Hello,\nthere(删除 s 将生成 Hello)。

正则表达式几乎无所不能,也许惟一的限制因素就是您的想象力和创造力了

转载于:https://my.oschina.net/harvard/blog/182127

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值