PHP-MySQL-入门教程-四-

53 阅读30分钟

PHP MySQL 入门教程(四)

原文:Beginning PHP and MySQL

协议:CC BY-NC-SA 4.0

九、字符串和正则表达式

程序员基于关于信息的分类、解析、存储和显示的既定规则来构建应用,无论这些信息是由美食菜谱、商店销售收据、诗歌还是其他任何内容组成。本章介绍了许多 PHP 函数,在执行这类任务时,您肯定会经常用到这些函数。

本章涵盖以下主题:

  • 正则表达式: PHP 支持使用正则表达式在字符串中搜索模式,或者根据模式用另一个值替换字符串中的元素。有几种类型的正则表达式,PHP 支持的一种称为 Pearl style regex 或 PCRE。

  • 字符串操作: PHP 是字符串操作的“瑞士军刀”,允许您以几乎所有可以想象的方式切割文本。提供了近 100 个原生字符串操作函数,并且能够将函数链接在一起以产生更复杂的行为,在耗尽 PHP 在这方面的能力之前,您将耗尽编程思路。在这一章中,我将向你介绍 PHP 提供的几个最常用的操作函数。

正则表达式

正则表达式根据定义的语法规则提供描述或匹配数据的基础。正则表达式只不过是字符本身的一种模式,与某个文本包相匹配。这个序列可能是您已经熟悉的模式,例如单词 dog、,或者它可能是在模式匹配环境中具有特定含义的模式,例如<(?)>.*<\ /.?>

如果您还不熟悉通用表达式的机制,请花些时间通读构成本节剩余部分的简短教程。然而,因为已经有无数的在线和印刷教程是关于这个问题的,所以我将重点为您提供这个主题的基本介绍。如果您已经非常熟悉正则表达式语法,可以跳过本教程,直接阅读“PHP 的正则表达式函数(Perl 兼容)”一节。

正则表达式语法(Perl)

Perl 一直被认为是有史以来最强大的解析语言之一。它提供了一种全面的正则表达式语言,甚至可以用来搜索、修改和替换最复杂的字符串模式。PHP 开发人员认为,他们应该让 PHP 用户可以使用著名的 Perl 正则表达式语法,而不是重新发明正则表达式。

Perl 的正则表达式语法实际上是 POSIX 实现的派生,这导致了两者之间相当大的相似性。本节的剩余部分将致力于对 Perl 正则表达式语法的简要介绍。让我们从一个基于 Perl 的正则表达式的简单例子开始:

/food/

请注意,字符串food被括在两个正斜杠之间,也称为分隔符。除了斜线(/)之外,还可以使用散列符号(#)、加号(+)、百分比(%)等。如果在模式中使用,用作分隔符的字符必须用反斜杠()转义。使用不同的分隔符将有可能消除转义的需要。如果要匹配包含许多斜线的 URL 模式,使用散列符号作为分隔符可能更方便,如下所示:

/http:\/\/somedomain.com\//
#http://somedomain.com/#

除了匹配精确的单词,还可以使用量词来匹配多个单词:

/fo+/

使用+限定符表示任何包含 f 后跟一个或多个 o 的字符串都将匹配该模式。一些潜在的匹配包括foodfoolfo4。或者,使用*限定符来匹配 0 个或多个前面的字符。例如

/fo*/

将匹配字符串中 f 后面跟 0 或多个 0 的任何部分。这将匹配前面示例中的食物傻瓜fo4 以及快速精细等。这两个限定词对字符的重复次数都没有上限。如下例所示,可以添加这样的上限:

/fo{2,4}/

这与f匹配,后跟两到四次o。一些潜在的匹配包括foolfoooolfoosball

上面的三个例子定义了一个以 f 开头的模式,后面是 1 个或多个 o,0 个或多个 o,或者 2 到 4 个 o。模式之前或之后的任何字符都不是匹配的一部分。

修饰语

通常你会想要调整正则表达式的解释;例如,您可能希望告诉正则表达式执行不区分大小写的搜索,或者忽略嵌入其语法中的注释。这些调整被称为修饰语,它们对帮助你写出简洁明了的表达大有帮助。表 9-1 中列出了一些更有趣的修饰语。有效修饰符的完整列表和详细描述可以在这里找到: http://php.net/manual/en/reference.pcre.pattern.modifiers.php

表 9-1

五种样品改性剂

|

修改

|

描述

| | --- | --- | | i | 执行不区分大小写的搜索。 | | m | 将一个字符串视为几行(m表示多个)。默认情况下,^$字符匹配在字符串的最开始和最末尾。使用 m 修饰符将允许^$在字符串中任何一行的开头匹配。 | | s | 将字符串视为单行,忽略其中的换行符。 | | x | 忽略正则表达式中的空格和注释,除非空格被转义或在字符块中。 | | U | 停在第一场比赛。很多量词都是“贪心”的;他们尽可能多地匹配模式,而不是只在第一次匹配时停止。你可以用这个修饰符使它们变得“不优雅”。 |

这些修饰符直接放在正则表达式之后,例如,

/string/i。让我们考虑一个例子:

  • /wmd/i:匹配WMDwMDWMdwmd以及字符串wmd的任何其他大小写变化。

其他语言支持全局修饰符(g)。然而在 PHP 中,这是通过使用不同的函数preg_match()preg_match_all()来实现的。

元字符

Perl 正则表达式也使用元字符来进一步过滤它们的搜索。元字符只是一个字符或字符序列,它象征着特殊的含义。有用的元字符列表如下:

  • \A:仅匹配字符串的开头。

  • \b:匹配单词边界。

  • \B:匹配除单词边界以外的任何内容。

  • \d:匹配一个数字字符。这个和[0-9]一样。

  • \D:匹配非数字字符。

  • \s:匹配一个空白字符。

  • \S:匹配一个非白色空格字符。

  • []:括起一个字符类。

  • ():包含一个字符组或定义一个反向引用或子模式的开始和结束。

  • $:匹配行尾。

  • ^:在多行模式下匹配字符串的开头或每一行的开头。

  • .:匹配除换行符以外的任何字符。

  • \:引用下一个元字符。

  • \w:匹配任何只包含下划线和字母数字字符的字符串。这取决于语言环境。对于美国英语,这与[a-zA-Z0-9_]相同。

  • \W:匹配字符串,省略下划线和字母数字字符。

让我们考虑几个例子。第一个正则表达式将匹配字符串,如pisalisa,但不匹配sand:

/sa\b/

下一个匹配第一个不区分大小写的单词linux:

/\blinux\b/i

单词边界元字符的反义词是\B,匹配除单词边界之外的任何内容。因此,本例将匹配字符串,如sandSally,但不匹配Melissa:

/sa\B/i

最后一个示例返回与美元符号后跟一个或多个数字匹配的字符串的所有实例:

/\$\d+/

PHP 的正则表达式函数(Perl 兼容)

PHP 提供了九个使用 Perl 兼容的正则表达式搜索和修改字符串的函数:preg_filter(), preg_grep(), preg_match(), preg_match_all(), preg_quote(), preg_replace(), preg_replace_callback(),preg_replace_callback_array(),preg_split()。除此之外,preg_last_error()函数还提供了获取上次执行的错误代码的方法。这些功能将在以下章节中介绍。

搜索模式

preg_match()函数在字符串中搜索特定的模式,如果存在则返回TRUE,否则返回FALSE。其原型如下:

int preg_match(string pattern, string string [, array matches] [, int flags [, int offset]]])

可选的输入参数 matches 通过引用传递,并且将包含搜索模式中包含的子模式的各个部分,如果适用的话。下面是一个使用preg_match()执行不区分大小写的搜索的例子:

<?php
    $line = "vim is the greatest word processor ever created! Oh vim, how I love thee!";
    if (preg_match("/\bVim\b/i", $line, $match)) print "Match found!";
?>

例如,如果找到了单词Vimvim,这个脚本将确认匹配,但不会确认simplevimvimsevim

您可以使用可选的 flags 参数来修改返回的 matches 参数的行为,通过返回每个匹配的字符串及其由匹配位置确定的相应偏移量来改变数组的填充方式。

最后,可选的 offset 参数会将字符串内的搜索起点调整到指定位置。

匹配模式的所有出现

preg_match_all()函数匹配一个字符串中一个模式的所有出现,按照您通过可选输入参数指定的顺序将每个出现分配给一个数组。其原型如下:

int preg_match_all(string pattern, string string, array matches [, int flags] [, int offset]))

标志参数接受三个值之一:

  • 如果可选的标志参数未定义,则PREG_PATTERN_ORDER为默认值。PREG_PATTERN_ORDER以您认为最符合逻辑的方式指定顺序:$pattern_array[0]是所有完全模式匹配的数组,$pattern_array[1]是匹配第一个带括号正则表达式的所有字符串的数组,依此类推。

  • PREG_SET_ORDER对数组的排序与默认设置稍有不同。$pattern_array[0]包含与第一个带括号的正则表达式匹配的元素,$pattern_array[1]包含与第二个带括号的正则表达式匹配的元素,依此类推。

  • PREG_OFFSET_CAPTURE修改返回的matches参数的行为,通过返回每个匹配的字符串及其由匹配位置确定的相应偏移量来改变数组的填充方式。

下面是如何使用preg_match_all()来查找包含在粗体 HTML 标签中的所有字符串:

<?php
    $userinfo = "Name: <b>Zeev Suraski</b> <br> Title: <b>PHP Guru</b>";
    preg_match_all("/<b>(.*)<\/b>/U", $userinfo, $pat_array);
    printf("%s <br /> %s", $pat_array[0][0], $pat_array[0][1]);
?>

这将返回以下内容:

Zeev Suraski
PHP Guru

搜索数组

preg_grep()函数搜索一个数组的所有元素,返回一个由匹配特定模式的所有元素组成的数组。其原型如下:

  • 数组 preg_grep(字符串模式,数组输入 [,int 标志)

考虑一个使用这个函数在数组中搜索以p开头的食物的例子:

<?php
    $foods = array("pasta", "steak", "fish", "potatoes");
    $food = preg_grep("/^p/", $foods);
    print_r($food);
?>

这将返回以下内容:

Array ( [0] => pasta [3] => potatoes )

请注意,该数组对应于输入数组的索引顺序。如果该索引位置的值匹配,它将包含在输出数组的相应位置。否则,该位置是空的。如果你想删除那些空白的数组实例,通过第五章中介绍的函数array_values()过滤输出数组。

可选输入参数标志接受一个值PREG_GREP_INVERT。传递这个标志将导致检索那些匹配模式的数组元素。

分隔特殊正则表达式字符

函数preg_quote()在每个对正则表达式语法有特殊意义的字符前插入一个反斜杠分隔符。这些特殊字符包括$ ^ * ( ) + = { } [ ] | \\ : < >。其原型如下:

string preg_quote(string str [, string delimiter])

可选参数 delimiter 指定正则表达式使用什么分隔符,使其也被反斜杠转义。考虑一个例子:

<?php
    $text = "Tickets for the fight are going for $500.";
    echo preg_quote($text);
?>

这将返回以下内容:

Tickets for the fight are going for \$500\.

替换模式的所有出现

preg_replace()函数用replacement替换所有出现的pattern,并返回修改后的结果。其原型如下:

mixed preg_replace(mixed pattern, mixed replacement, mixed str [, int limit [, int count]])

注意图案替换参数都定义为mixed。这是因为您可以为这两者提供字符串或数组。可选输入参数 limit 指定应该发生多少次匹配。未设置limit或将其设置为-1将导致所有事件的替换(无限制)。最后,通过引用传递的可选计数参数将被设置为替换的总数。考虑一个例子:

<?php
    $text = "This is a link to http://www.wjgilmore.com/.";
    echo preg_replace("/http:\/\/(.*)\//", "<a href=\"\${0}\">\${0}</a>", $text);
?>

这将返回以下内容:

This is a link to
<a href="http://www.wjgilmore.com/">http://www.wjgilmore.com/</a>.

如果您将数组作为模式替换参数传递,该函数将遍历每个数组的每个元素,在发现替换时进行替换。考虑这个例子,它可以作为公司报告过滤器销售:

<?php
    $draft = "In 2010 the company faced plummeting revenues and scandal.";
    $keywords = array("/faced/", "/plummeting/", "/scandal/");
    $replacements = array("celebrated", "skyrocketing", "expansion");
    echo preg_replace($keywords, $replacements, $draft);
?>

这将返回以下内容:

In 2010 the company celebrated skyrocketing revenues and expansion.

preg_filter()函数的运行方式与preg_replace() ,相同,除了不返回修改后的结果,只返回匹配的结果。

创建自定义替换函数

在某些情况下,您可能希望根据 PHP 默认功能之外的一组更复杂的标准来替换字符串。例如,考虑这样一种情况,您想扫描一些文本中的首字母缩略词,如 IRS ,并在首字母缩略词后直接插入完整的名称。为此,您需要创建一个自定义函数,然后使用函数preg_replace_callback()将它临时绑定到语言中。其原型如下:

mixed preg_replace_callback(mixed pattern, callback callback, mixed str
                            [, int limit [, int count]])

模式参数确定您要查找的内容,而字符串参数定义您要搜索的字符串。回调参数定义用于替换任务的函数名。可选参数 limit 指定应该进行多少次匹配。未设置限值或将其设置为-1将导致所有事件被替换。最后,可选的计数参数将被设置为更换次数。在下面的例子中,名为acronym()的函数被传递到preg_replace_callback()中,用于将各种缩写的长形式插入到目标字符串中:

<?php

    // This function will add the acronym's long form
    // directly after any acronyms found in $matches
    function acronym($matches) {
        $acronyms = array(
            'WWW' => 'World Wide Web',
            'IRS' => 'Internal Revenue Service',
            'PDF' => 'Portable Document Format');

        if (isset($acronyms[$matches[1]]))
            return $acronyms[$matches[1]] . " (" . $matches[1] . ")";
        else
            return $matches[1];
    }

    // The target text
    $text = "The <acronym>IRS</acronym> offers tax forms in
             <acronym>PDF</acronym> format on the <acronym>WWW</acronym>.";

    // Add the acronyms' long forms to the target text
    $newtext = preg_replace_callback("/<acronym>(.*)<\/acronym>/U", 'acronym',
                                      $text);

    print_r($newtext);

?>

这将返回以下内容:

The Internal Revenue Service  (IRS) offers tax forms
in Portable Document Format (PDF) on the World Wide Web (WWW).

PHP 7.0 引入了 preg_replace_callback()的一个变种,叫做 preg_replace_callback_array()。这些函数以相似的方式工作,除了新函数将模式和回调组合成一个模式和回调数组。这使得用一个函数调用进行多次替换成为可能。

还要注意,随着匿名函数(也称为闭包)的引入(参见第四章),不再需要以函数名字符串的形式提供回调参数。可以写成匿名函数。上面的例子应该是这样的:

<?php

    // The target text
    $text = "The <acronym>IRS</acronym> offers tax forms in

             <acronym>PDF</acronym> format on the <acronym>WWW</acronym>.";

    // Add the acronyms' long forms to the target text
    $newtext = preg_replace_callback("/<acronym>(.*)<\/acronym>/U",
      function($matches) {
        $acronyms = array(
            'WWW' => 'World Wide Web',
            'IRS' => 'Internal Revenue Service',
            'PDF' => 'Portable Document Format');

        if (isset($acronyms[$matches[1]]))
            return $acronyms[$matches[1]] . " (" . $matches[1] . ")";
        else
            return $matches[1];
      },
       $text);
    print_r($newtext);

?>

基于不区分大小写的模式将字符串拆分为不同的元素

除了pattern也可以用正则表达式来定义之外,preg_split()函数的操作与explode(),完全一样。其原型如下:

array preg_split(string pattern, string string [, int limit [, int flags]])

如果指定了可选输入参数限制,则只返回该limit个数的子字符串。考虑一个例子:

<?php
    $delimitedText = "Jason+++Gilmore+++++++++++Columbus+++OH";
    $fields = preg_split("/\++/", $delimitedText);
    foreach($fields as $field) echo $field."<br />";
?>

这将返回以下内容:

Jason
Gilmore
Columbus
OH

注意

在本章的后面,“正则表达式函数的替代方法”一节提供了几个标准函数,它们可以代替正则表达式用于某些任务。在许多情况下,这些替代函数实际上比它们的正则表达式对应物执行得快得多。

其他字符串特定的函数

除了本章前半部分讨论的基于正则表达式的函数之外,PHP 还提供了大约 100 个函数,它们能够处理字符串的几乎所有方面。介绍每个函数超出了本书的范围,只会重复 PHP 文档中的许多信息。这一部分专门讨论各种类别的常见问题,重点关注社区论坛中似乎最常出现的与字符串相关的问题。该部分分为以下主题:

  • 确定字符串长度

  • 比较两个字符串

  • 操纵字符串大小写

  • 将字符串与 HTML 相互转换

  • 正则表达式函数的替代方法

  • 填充和剥离字符串

  • 计算字符和单词

注意

本节描述的函数假设字符串由单字节字符组成。这意味着字符串中的字符数等于字节数。有些字符集使用多个字节来表示每个字符。当用于多字节字符串时,标准的 PHP 函数通常无法提供正确的值。有一个名为 mb_string 的扩展可用于操作多字节字符串。

确定字符串的长度

确定字符串长度是无数应用中的重复操作。PHP 函数strlen()很好地完成了这项任务。此函数返回字符串的长度,其中字符串中的每个字符相当于一个单位(字节)。其原型如下:

int strlen(string str)

以下示例验证用户密码的长度是否可接受:

<?php
    $pswd = "secretpswd";
    if (strlen($pswd) < 10)
        echo "Password is too short!";
    else
        echo "Password is valid!";
?>

在这种情况下,不会出现错误消息,因为选择的密码由 10 个字符组成,而条件表达式验证目标字符串是否少于 10 个字符。

比较两个字符串

字符串比较可以说是任何语言的字符串处理功能中最重要的特性之一。虽然有很多方法可以比较两个字符串是否相等,但是 PHP 提供了四个函数来执行这个任务:strcmp(), strcasecmp(), strspn()strcspn()

敏感地比较两个字符串的大小写

strcmp()函数对两个字符串进行区分大小写的比较。其原型如下:

int strcmp(string str1, string str2)

它将根据比较结果返回三个可能值之一:

  • 0如果str1str2相等

  • -1如果str1小于str2

  • 1如果str2小于str1

网站通常要求注册用户输入并确认密码,从而降低了因打字错误而输入错误密码的可能性。strcmp()是比较两个密码条目的一个很好的功能,因为密码通常区分大小写:

<?php
    $pswd = "supersecret";
    $pswd2 = "supersecret2";

    if (strcmp($pswd, $pswd2) != 0) {
        echo "Passwords do not match!";
    } else {
        echo "Passwords match!";
    }
?>

注意,字符串必须完全匹配,strcmp()才会认为它们相等。例如,Supe rsecret 不同于 supersecret。如果你想比较两个字符串的大小写,考虑下一个介绍的strcasecmp()

关于这个函数的另一个常见混淆点是,如果两个字符串相等,它返回0的行为。这不同于使用==运算符执行字符串比较,如下所示:

if ($str1 == $str2)

虽然两者都完成了相同的目标,即比较两个字符串,但请记住,它们这样做返回的值是不同的。

不区分大小写地比较两个字符串

除了比较不区分大小写之外,strcasecmp()函数的操作与strcmp(),完全一样。其原型如下:

int strcasecmp(string str1, string str2)

以下示例比较了两个电子邮件地址,这是对strcasecmp()的理想使用,因为大小写不决定电子邮件地址的唯一性:

<?php
    $email1 = "admin@example.com";
    $email2 = "ADMIN@example.com";

    if (! strcasecmp($email1, $email2))
        echo "The email addresses are identical!";
?>

在本例中,输出消息是因为strcasecmp()对 email1 和 email2 执行不区分大小写的比较,并确定它们确实相同。

计算两个字符串之间的相似度

函数的作用是:返回一个字符串中第一段的长度,该字符串中包含的字符也可以在另一个字符串中找到。其原型如下:

int strspn(string str1, string str2 [, int start [, int length]])

以下是如何使用strspn()来确保密码不仅仅由数字组成:

<?php
    $password = "3312345";
    if (strspn($password, "1234567890") == strlen($password))
        echo "The password cannot consist solely of numbers!";
?>

在这种情况下,会返回错误消息,因为$password确实只由数字组成。

您可以使用可选的 start 参数来定义字符串中的起始位置,而不是默认的 0 偏移量。可选的长度参数可用于定义将用于比较的str1字符串的长度。

计算两个字符串之间的差

strcspn()函数返回一个字符串的第一段长度,该字符串包含在另一个字符串中找不到的字符。可选的起始长度参数的行为方式与之前介绍的strspn()功能中使用的方式相同。其原型如下:

int strcspn(string str1, string str2 [, int start [, int length]])

这里有一个使用strcspn():进行密码验证的例子

<?php
    $password = "a12345";
    if (strcspn($password, "1234567890") == 0) {
        echo "Password cannot consist solely of numbers!";
    }
?>

在这种情况下,不会显示错误消息,因为$password不仅仅由数字组成。

操纵字符串大小写

有五个函数可以帮助你处理字符串中字符的大小写:strtolower(), strtoupper(), ucfirst()lcfirst()ucwords()

将字符串转换为全部小写

strtolower()函数将一个字符串转换成全部小写字母,返回修改后的字符串。非字母字符不受影响。其原型如下:

string strtolower(string str)

以下示例使用strtolower()将 URL 转换为全部小写字母:

<?php
    $url = "http://WWW.EXAMPLE.COM/";
    echo strtolower($url);
?>

这将返回以下内容:

http://www.example.com/

将字符串转换为全大写

正如您可以将字符串转换为小写一样,您也可以将其转换为大写。这是通过函数strtoupper() .完成的,其原型如下:

string strtoupper(string str)

非字母字符不受影响。此示例使用strtoupper()将字符串转换为全大写字母:

<?php
    $msg = "I annoy people by capitalizing e-mail text.";
    echo strtoupper($msg);
?>

这将返回以下内容:

I ANNOY PEOPLE BY CAPITALIZING E-MAIL TEXT.

将字符串的第一个字母大写

ucfirst()函数将字符串str的第一个字母大写,如果它是字母的话。其原型如下:

string ucfirst(string str)

非字母字符不会受到影响。此外,字符串中的任何大写字符都将保持不变。考虑这个例子:

<?php
    $sentence = "the newest version of PHP was released today!";
    echo ucfirst($sentence);
?>

这将返回以下内容:

The newest version of PHP was released today!

注意,虽然第一个字母确实是大写的,但是大写的单词 PHP 没有被改动。函数lcfirst()执行相反的操作,将字符串的第一个字符转换成小写。

将字符串中的每个单词大写

函数的作用是:将字符串中每个单词的首字母大写。其原型如下:

string ucwords(string str)

非字母字符不受影响。此示例使用ucwords()将字符串中的每个单词大写:

<?php
    $title = "O'Malley wins the heavyweight championship!";
    echo ucwords($title);
?>

这将返回以下内容:

O'Malley Wins The Heavyweight Championship!

请注意,如果欧玛利不小心被写成奥马利ucwords()不会捕捉到错误,因为它认为一个单词被定义为一串字符,通过两边的空格与字符串中的其他实体分开。

将字符串与 HTML 相互转换

将一个字符串或整个文件转换成适合在 Web 上查看的形式(反之亦然)比您想象的要容易,而且会带来一些安全风险。如果输入字符串是由正在浏览网站的用户提供的,则有可能注入将由浏览器执行的脚本代码,因为现在看起来该代码来自服务器。不要相信用户的输入。以下函数适用于此类任务。

将换行符转换为 HTML Break 标记

nl2br()函数将字符串中的所有换行符(\n)转换为符合 XHTML 标准的对应字符<br />。其原型如下:

string nl2br(string str)

换行符可以通过回车创建,或者显式写入字符串。以下示例将文本字符串转换为 HTML 格式:

<?php
    $recipe = "3 tablespoons Dijon mustard
    1/3 cup Caesar salad dressing
    8 ounces grilled chicken breast
    3 cups romaine lettuce";

    // convert the newlines to <br />'s.
    echo nl2br($recipe);
?>

执行此示例会产生以下输出:

3 tablespoons Dijon mustard<br />
1/3 cup Caesar salad dressing<br />
8 ounces grilled chicken breast<br />
3 cups romaine lettuce

将特殊字符转换为它们的 HTML 等效字符

在一般的交流过程中,您可能会遇到许多不包含在文档的文本编码中的字符,或者在键盘上不容易找到的字符。这种字符的例子包括版权符号(©)、分符号( )和重音符(è)。为了弥补这些缺点,人们设计了一套通用键码,称为字符实体引用。当浏览器解析这些实体时,它们将被转换成可识别的对应实体。例如,前面提到的三个角色将分别表示为©&cent;&Egrave;

要执行这些转换,您可以使用htmlentities()功能。其原型如下:

string htmlentities(string str [, int flags [, int charset [, boolean double_encode]]])

由于标记中引号的特殊性质,可选的 quote_style 参数提供了选择如何处理它们的机会。接受三个值:

  • ENT_COMPAT:转换双引号,忽略单引号。这是默认设置。

  • ENT_NOQUOTES:忽略双引号和单引号。

  • ENT_QUOTES:转换双引号和单引号。

第二个可选参数 charset ,决定用于转换的字符集。表 9-2 提供了支持的字符集列表。如果省略了charset,它将默认使用 php.ini 设置 default_charset 定义的默认字符集。

表 9-2

htmlentities()支持的字符集

|

字符集

|

描述

| | --- | --- | | BIG5 | 繁体中文 | | BIG5-HKSCS | BIG5 附加香港扩展,繁体中文 | | cp866 | 特定于 DOS 的西里尔字符集 | | cp1251 | 特定于 Windows 的西里尔字符集 | | cp1252 | 西欧的 Windows 专用字符集 | | EUC-JP | 日本人 | | GB2312 | 简体中文 | | ISO-8859-1 | 西欧,拉丁语-1 | | ISO-8859-5 | 很少使用的西里尔字符集(拉丁语/西里尔语)。 | | ISO-8859-15 | 西欧,拉丁语-9 | | KOI8-R | 俄语 | | Shift_JIS | 日本人 | | MacRoman | Mac OS 使用的字符集 | | UTF-8 | ASCII 兼容多字节 8 编码 |

最后一个可选参数 double_encode 将阻止htmlentities()对字符串中已经存在的任何 HTML 实体进行编码。在大多数情况下,如果您怀疑 HTML 实体已经存在于目标字符串中,您可能希望启用该参数。

以下示例转换 web 显示所需的字符:

<?php
    $advertisement = "Coffee at 'Cafè Française' costs $2.25.";
    echo  htmlentities($advertisement);
?>

这将返回以下内容:

Coffee at 'Caf&egrave; Fran&ccedil;aise' costs $2.25.

两个字符被转换,重音符()和变音符号()。由于默认的quote_style设置ENT_COMPAT,单引号被忽略。

将特殊 HTML 字符用于其他目的

一些字符在标记语言和人类语言中扮演着双重角色。当以后一种方式使用时,这些字符必须被转换成它们可显示的等价物。例如,“与”号必须转换成&,而大于号必须转换成>htmlspecialchars()函数可以帮你做到这一点,将下面的字符转换成它们兼容的对等物。其原型如下:

string htmlspecialchars(string str [, int quote_style [, string charset [, boolean double_encode]]])

可选的字符集双编码参数的操作方式与上一节对htmlentities()功能的解释相同。

htmlspecialchars()可以转换的字符列表及其结果格式如下:

  • &变成了&

  • "(双引号)变成"

  • '(单引号)变为'

  • <变成了<

  • >变成了>

这个函数在防止用户在交互式 web 应用(如留言板)中输入 HTML 标记时特别有用。

以下示例使用htmlspecialchars() :转换可能有害的字符

<?php
    $input = "I just can't get <<enough>> of PHP!";
    echo htmlspecialchars($input);
?>

查看源代码,您会看到以下内容:

I just can't get <<enough>> of PHP!

如果不需要翻译,也许更有效的方法是使用strip_tags() ,将标签从字符串中完全删除。

小费

如果是将htmlspecialchars()nl2br()等函数结合使用,应该在htmlspecialchars()之后执行nl2br();否则,用nl2br()生成的<br />标签将被转换为可见字符。

将文本转换为 HTML 格式

使用get_html_translation_table()是一种将文本翻译成其 HTML 等价物的便捷方式,返回两个翻译表中的一个(HTML_SPECIALCHARSHTML_ENTITIES)。其原型如下:

array get_html_translation_table(int table [, int quote_style])

然后,这个返回值可以与另一个预定义函数strtr()(在本节后面正式介绍)结合使用,将文本翻译成相应的 HTML 代码。

以下示例使用get_html_translation_table()将文本转换为 HTML:

<?php
    $string = "La pasta è il piatto più amato in Italia";
    $translate = get_html_translation_table(HTML_ENTITIES);
    echo strtr($string, $translate);
?>

这将返回浏览器呈现所需格式的字符串:

La pasta &egrave; il piatto pi&ugrave; amato in Italia

有趣的是,array_flip()能够逆转文本到 HTML 的翻译,反之亦然。假设您没有打印前面代码示例中strtr()的结果,而是将它赋给了变量$translated_string

下一个例子使用array_flip()将一个字符串返回到它的初始值:

<?php
    $entities = get_html_translation_table(HTML_ENTITIES);
    $translate = array_flip($entities);
    $string = "La pasta &egrave; il piatto pi&ugrave; amato in Italia";
    echo strtr($string, $translate);
?>

这将返回以下内容:

La pasta é il piatto più amato in italia

创建自定义转换列表

函数的作用是:将一个字符串中的所有字符转换成在一个预定义的数组中找到的相应匹配。其原型如下:

string strtr(string str, array replacements)

本示例将不推荐使用的粗体(<b>)字符转换为其 XHTML 等效字符:

<?php
    $table = array('<b>' => '<strong>', '</b>' => '</strong>');
    $html = '<b>Today In PHP-Powered News</b>';
    echo strtr($html, $table);
?>

这将返回以下内容:

<strong>Today In PHP-Powered News</strong>

将 HTML 转换为纯文本

有时您可能需要将 HTML 文件转换为纯文本。您可以使用strip_tags()函数来实现,该函数从字符串中移除所有 HTML 和 PHP 标签,只留下文本实体。其原型如下:

string strip_tags(string str [, string allowable_tags])

可选的 allowable_tags 参数允许您指定在此过程中希望跳过哪些标签。跳过标签不会处理被跳过标签中的任何属性。如果输入是由用户提供的,并且那些属性包含 JavaScript,这可能是危险的。这个例子使用strip_tags()从一个字符串中删除所有的 HTML 标签:

<?php
    $input = "Email <a href='spammer@example.com'>spammer@example.com</a>";
    echo strip_tags($input);
?>

这将返回以下内容:

Email spammer@example.com

以下示例去除除了<a>标签之外的所有标签:

<?php
    $input = "This <a href='http://www.example.com/'>example</a>
              is <b>awesome</b>!";
    echo strip_tags($input, "<a>");
?>

这将返回以下内容:

This <a href='http://www.example.com/'>example</a> is awesome!

注意

另一个类似于strip_tags()的函数是fgetss()。该功能在第十章中描述。

正则表达式函数的替代方法

当您处理大量信息时,正则表达式函数会大大降低速度。只有当您对解析需要使用正则表达式的相对复杂的字符串感兴趣时,才应该使用这些函数。如果您对简单表达式的解析感兴趣,有各种预定义的函数可以大大加快这个过程。本节将介绍这些功能。

基于预定义字符对字符串进行标记

记号化是一个计算机术语,用于将字符串分割成更小的部分。编译器用它来将程序转换成单独的命令或标记。strtok()函数根据预定义的字符列表对字符串进行标记。其原型如下:

string strtok(string str, string tokens)

关于strtok()的一个奇怪之处是,为了完全标记化一个字符串,必须不断地调用它;每个调用只标记字符串的下一部分。然而, str 参数只需要指定一次,因为该函数会跟踪它在str中的位置,直到它完全标记 str 或者指定了新的 str 参数。它的行为可以通过一个例子得到最好的解释:

<?php
    $info = "J. Gilmore:jason@example.com|Columbus, Ohio";

    // delimiters include colon (:), vertical bar (|), and comma (,)
    $tokens = ":|,";
    $tokenized = strtok($info, $tokens);

    // print out each element in the $tokenized array
    while ($tokenized) {
        echo "Element = $tokenized<br>";
        // Don't include the first argument in subsequent calls.
        $tokenized = strtok($tokens);
    }
?>

这将返回以下内容:

Element = J. Gilmore
Element = jason@example.com
Element = Columbus
Element = Ohio

根据预定义的分隔符分解字符串

explode()函数将字符串str分成一个子字符串数组。其原型如下:

array explode(string separator, string str [, int limit])

根据separator指定的字符分隔符,原始字符串被分成不同的元素。可以通过可选的 limit 来限制元素的数量。让我们结合使用explode()sizeof()strip_tags()来确定给定文本块中的总字数:

<?php
    $summary = <<<summary
    The most up to date source for PHP documentation is the PHP manual.
    It contins many examples and user contributed code and comments.
    It is available on the main PHP web site
    <a href="http://www.php.net">PHP’s</a>.
summary;
    $words = sizeof(explode(' ',strip_tags($summary)));
    echo "Total words in summary: $words";
?>

这将返回以下内容:

Total words in summary: 46

explode()功能总是比preg_split()快得多。因此,当正则表达式不是必需的时候,总是用它来代替其他的。

注意

您可能想知道为什么前面的代码以不一致的方式缩进。多行字符串使用 heredoc 语法分隔,这要求结束标识符不能缩进,甚至不能缩进一个空格。有关 heredoc 的更多信息,请参见第三章。

将数组转换为字符串

正如您可以使用explode()函数将分隔的字符串分成不同的数组元素一样,您可以使用implode()函数将数组元素连接起来形成一个分隔的字符串。其原型如下:

string implode(string delimiter, array pieces)
This example forms a string out of the elements of an array:
<?php
    $cities = array("Columbus", "Akron", "Cleveland", "Cincinnati");
    echo implode("|", $cities);
?>

这将返回以下内容:

Columbus|Akron|Cleveland|Cincinnati

执行复杂字符串解析

strpos()函数查找字符串中第一个区分大小写的a substring的位置。其原型如下:

int strpos(string str, string substr [, int offset])

可选输入参数 offset 指定开始搜索的位置。如果 substr 不在 str 中,strpos()将返回 FALSE。可选参数 offset 决定了strpos()开始搜索的位置。以下示例确定第一次访问index.html的时间戳:

<?php
    $substr = "index.html";
    $log = <<< logfile
    192.168.1.11:/www/htdocs/index.html:[2010/02/10:20:36:50]
    192.168.1.13:/www/htdocs/about.html:[2010/02/11:04:15:23]
    192.168.1.15:/www/htdocs/index.html:[2010/02/15:17:25]
logfile;

   // What is first occurrence of the time $substr in log?
   $pos = strpos($log, $substr);

   // Find the numerical position of the end of the line
   $pos2 = strpos($log,"\n",$pos);

   // Calculate the beginning of the timestamp
   $pos = $pos + strlen($substr) + 1;

   // Retrieve the timestamp
   $timestamp = substr($log,$pos,$pos2-$pos);
   echo "The file $substr was first accessed on: $timestamp";
?>

这将返回首次访问文件index.html的位置:

The file index.html was first accessed on: [2010/02/10:20:36:50]

函数stripos()的操作与strpos(),相同,只是它不敏感地执行其搜索案例。

查找字符串的最后一个匹配项

strrpos()函数查找最后一个出现的字符串,返回它的数字位置。其原型如下:

int strrpos(string str, char substr [, offset])

可选参数偏移量决定了strrpos()开始搜索的位置。假设您想要削减冗长的新闻摘要,截断摘要并用省略号替换被截断的部分。然而,不是简单地在期望的长度上显式地截断摘要,而是希望它以用户友好的方式操作,在最接近截断长度的单词末尾截断。这个函数非常适合这样的任务。考虑这个例子:

<?php
    // Limit $summary to how many characters?
    $limit = 100;

    $summary = <<< summary
    The most up to date source for PHP documentation is the PHP manual.
    It contins many examples and user contributed code and comments.
    It is available on the main PHP web site
    <a href="http://www.php.net">PHP’s</a>.
summary;

    if (strlen($summary) > $limit)
        $summary = substr($summary, 0, strrpos(substr($summary, 0, $limit),
                          ' ')) . '...';
    echo $summary;
?>

这将返回以下内容:

The most up to date source for PHP documentation is the PHP manual.
It contins many...

用另一个字符串替换一个字符串的所有实例

函数 case 敏感地将一个字符串的所有实例替换为另一个。其原型如下:

mixed str_replace(string occurrence, mixed replacement, mixed str [, int count])

如果在str中找不到occurrence,则原始字符串被不加修改地返回。如果定义了可选参数计数,则只有在str中发现的count事件会被替换。

此功能非常适合在自动电子邮件地址检索程序中隐藏电子邮件地址:

<?php
    $author = "jason@example.com";
    $author = str_replace("@","(at)",$author);
    echo "Contact the author of this article at $author.";
?>

这将返回以下内容:

Contact the author of this article at jason(at)example.com.

函数str_ireplace()的操作与str_replace(),相同,只是它能够执行不区分大小写的搜索。

检索字符串的一部分

strstr()函数从预定义字符串的第一次出现开始返回字符串的剩余部分。其原型如下:

string strstr(string str, string occurrence [, bool before_needle])

可选的 before_needle 参数修改strstr() ,的行为,使函数返回在第一次出现之前找到的字符串部分。

此示例结合使用函数和ltrim()函数来检索电子邮件地址的域名:

<?php
    $url = "sales@example.com";
    echo ltrim(strstr($url, "@"),"@");
?>

这将返回以下内容:

example.com

基于预定义的偏移量返回字符串的一部分

substr()函数返回位于预定义的起始偏移量和长度位置之间的字符串部分。其原型如下:

string substr(string str, int start [, int length])

如果没有指定可选的长度参数,则认为子串是从start开始到str结束的字符串。使用此功能时,请记住四点:

  • 如果 start 为正数,返回的字符串将从字符串的开始位置开始。

  • 如果start为负,返回的字符串将从字符串的length - start位置开始。

  • 如果提供了length并且是正数,返回的字符串将由startstart + length之间的字符组成。如果这个距离超过了字符串的总长度,则只返回从开始到结束之间的字符串。

  • 如果提供了length并且为负,则返回的字符串将从str的结尾开始结束length个字符。

请记住, start 是从str的第一个字符开始的偏移量,字符串(如数组)的索引是 0。考虑一个基本的例子:

<?php
    $car = "1944 Ford";
    echo substr($car, 5);
?>

这将从位置 5 处的第六个字符开始返回以下内容:

Ford

以下示例使用了长度参数:

<?php
    $car = "1944 Ford";
    echo substr($car, 0, 4);
?>

这将返回以下内容:

1944

最后一个例子使用负的长度参数:

<?php
    $car = "1944 Ford";
    echo substr($car, 2, -5);
?>

这将返回以下内容:

44

确定字符串出现的频率

substr_count()函数返回一个字符串在另一个字符串中出现的次数。该函数区分大小写。其原型如下:

int substr_count(string str, string substring [, int offset [, int length]])

可选的 offsetlength 参数分别决定了开始尝试匹配字符串中子字符串的字符串偏移量,以及偏移量之后要搜索的字符串的最大长度。

以下示例确定了 IT 顾问在其演示文稿中使用各种流行词汇的次数:

<?php
    $buzzwords = array("mindshare", "synergy", "space");

    $talk = <<< talk
    I'm certain that we could dominate mindshare in this space with
    our new product, establishing a true synergy between the marketing
    and product development teams. We'll own this space in three months.
talk;

    foreach($buzzwords as $bw) {
        echo "The word $bw appears ".substr_count($talk,$bw)." time(s).<br />";
    }
?>

这将返回以下内容:

The word mindshare appears 1 time(s).
The word synergy appears 1 time(s).
The word space appears 2 time(s).

用另一个字符串替换一个字符串的一部分

substr_replace()函数用替换字符串替换字符串的一部分,从指定的起始位置开始替换,到预定义的替换长度结束。其原型如下:

string substr_replace(string str, string replacement, int start [, int length])

或者,替换将在str中的replacement完全放置时停止。关于startlength的值,有几个行为你应该记住:

  • 如果start为正数,replacement将从字符start开始。

  • 如果start为负,replacement将从str 长度- start开始。

  • 如果提供了length并且是正数,replacement的长度将为length个字符。

  • 如果提供了length并且为负,replacement将在str 长度- length字符处结束。

假设您构建了一个电子商务站点,并且在用户配置文件界面中,您希望只显示所提供的信用卡号的最后四位数字。这个函数非常适合这样的任务:

<?php
    $ccnumber = "1234567899991111";
    echo substr_replace($ccnumber,"************",0,12);
?>

这将返回以下内容:

************1111

填充和剥离字符串

出于格式原因,有时需要通过填充字符或剥离字符来修改字符串长度。PHP 为此提供了许多函数。本节研究了许多常用的函数。

从字符串的开头修剪字符

ltrim()函数删除字符串开头的各种字符,包括空格、水平制表符(\t)、换行符(\n)、回车符(\r)、NULL ( \0)和垂直制表符(\x0b)。其原型如下:

string ltrim(string str [, string charlist])

您可以通过在可选参数 charlist 中定义其他字符来指定要删除的字符。

从字符串末尾修剪字符

rtrim()函数的操作与ltrim(),相同,除了它从字符串的右边移除指定的字符。其原型如下:


string rtrim(string str [, string charlist])

从字符串的两边修剪字符

你可以把trim()函数看作是ltrim()rtrim(),的组合,除了它从字符串的两边移除指定的字符:

string trim(string str [, string charlist])

填充字符串

函数用指定数量的字符填充字符串。其原型如下:

string str_pad(string str, int length [, string pad_string [, int pad_type]])

如果可选参数 pad_string 没有定义,str将用空格填充;否则,它将用 pad_string 指定的字符模式填充。默认情况下,字符串将被填充到右侧;然而,可选参数 pad_type 可能被赋予值STR_PAD_RIGHT(默认)、STR_PAD_LEFTSTR_PAD_BOTH,从而填充字符串。此示例显示了如何使用此函数填充字符串:

<?php
    echo str_pad("Salad", 10)." is good.";
?>

这将返回以下内容:

Salad     is good.

这个例子使用了str_pad()的可选参数:

<?php
    $header = "Log Report";
    echo str_pad ($header, 20, "=+", STR_PAD_BOTH);
?>

这将返回以下内容:

=+=+=Log Report=+=+=

注意,如果在完成图案的整个重复之前达到长度,则str_pad()会截断由pad_string定义的图案。

计算字符和单词

确定给定字符串中的字符或单词总数通常很有用。尽管 PHP 在字符串解析方面的强大功能长期以来使这项任务变得微不足道,但还是添加了以下两个函数来使这个过程形式化。

计算字符串中的字符数

函数count_chars()提供关于在字符串中找到的字符的信息。这个函数只对单字节字符有效。其原型如下:

mixed count_chars(string str [, int mode])

其行为取决于可选参数模式的定义方式:

  • 0:返回一个数组,由每个找到的字节值(0-255 代表每个可能的字符)作为键,对应的频率作为值,即使频率为零。这是默认设置。

  • 1:与 0 相同,但只返回那些频率大于零的字节值。

  • 2:与0相同,但只返回那些频率为零的字节值。

  • 3:返回一个包含所有已定位字节值的字符串。

  • 4:返回包含所有未使用字节值的字符串。

  • 下面的例子统计了$sentence中每个字符的出现频率:

<?php
    $sentence = "The rain in Spain falls mainly on the plain";

    // Retrieve located characters and their corresponding frequency.
    $chart = count_chars($sentence, 1);

    foreach($chart as $letter=>$frequency) 

        echo "Character ".chr($letter)." appears $frequency times<br />";
?>

这将返回以下内容:

Character appears 8 times
Character S appears 1 times
Character T appears 1 times
Character a appears 5 times
Character e appears 2 times
Character f appears 1 times
Character h appears 2 times
Character i appears 5 times
Character l appears 4 times
Character m appears 1 times
Character n appears 6 times
Character o appears 1 times
Character p appears 2 times
Character r appears 1 times
Character s appears 1 times
Character t appears 1 times

Character y appears 1 times

计算字符串中的单词总数

函数str_word_count()提供了关于在一个字符串中找到的单词总数的信息。根据本地设置,单词被定义为字母字符串,可以包含但不以–和'开头。其原型如下:

mixed str_word_count(string str [, int format])

如果可选参数格式没有定义,将返回总字数。如果定义了格式,它会根据其值修改函数的行为:

  • 1:返回一个由位于str中的所有单词组成的数组。

  • 2:返回一个关联数组,其中键是单词在str中的数字位置,值是单词本身。

考虑一个例子:

<?php
    $summary = <<< summary
    The most up to date source for PHP documentation is the PHP manual.
    It contins many examples and user contributed code and comments.
    It is available on the main PHP web site
    <a href="http://www.php.net">PHP's</a>.
summary;
   $words = str_word_count($summary);
   printf("Total words in summary: %s", $words);
?>

这将返回以下内容:

Total words in summary: 41

您可以结合使用此函数和array_count_values()来确定每个单词在字符串中出现的频率:

<?php
$summary = <<< summary
    The most up to date source for PHP documentation is the PHP manual.
    It contins many examples and user contributed code and comments.
    It is available on the main PHP web site
    <a href="http://www.php.net">PHP’s</a>.
summary;
   $words = str_word_count($summary,2);
   $frequency = array_count_values($words);
   print_r($frequency);
?>

这将返回以下内容:

Array ( [The] => 1 [most] => 1 [up] => 1 [to] => 1 [date] => 1 [source] => 1 [for] => 1 [PHP] => 4 [documentation] => 1 [is] => 2 [the] => 2 [manual] => 1 [It] => 2 [contins] => 1 [many] => 1 [examples] => 1 [and] => 2 [user] => 1 [contributed] => 1 [code] => 1 [comments] => 1 [available] => 1 [on] => 1 [main] => 1 [web] => 1 [site] => 1 [a] => 2 [href] => 1 [http] => 1 [www] => 1 [php] => 1 [net] => 1 [s] => 1 )

摘要

本章介绍的许多函数都是 PHP 应用中最常用的,因为它们构成了该语言字符串操作能力的核心。

下一章研究另一组常用的函数:那些致力于文件和操作系统的函数。

十、使用文件和操作系统

如今,很少编写完全自给自足的应用,也就是说,不依赖于与外部资源(如底层文件和操作系统,甚至其他编程语言)进行某种程度的交互。原因很简单:随着语言、文件系统和操作系统的成熟,开发人员能够将每种技术最强大的功能集成到一个单一的产品中,因此创建更高效、可伸缩和及时的应用的机会大大增加了。当然,诀窍是选择一种提供方便有效的方法来实现这一点的语言。幸运的是,PHP 很好地满足了这两个条件,不仅为程序员提供了一系列处理文件系统输入和输出的工具,还提供了在 shell 级别执行程序的工具。本章介绍了这些功能,包括以下主题:

  • **文件和目录:**您将学习如何执行文件系统询问,揭示文件和目录大小和位置、修改和访问次数等细节。

  • 文件 I/O: 您将学习如何与数据文件交互,这将让您执行各种实际任务,包括创建、删除、读取和写入文件。

  • **目录内容:**您将学习如何轻松检索目录内容。

  • Shell 命令(Shell commands):您可以通过许多内置的函数和机制在 PHP 应用中利用操作系统和其他语言级别的功能。

  • 本节展示了 PHP 的输入净化功能,向您展示了如何阻止用户传递可能对您的数据和操作系统造成潜在危害的数据。

注意

PHP 特别擅长处理底层文件系统,以至于它作为命令行解释器(CLI)越来越受欢迎。这允许从命令行脚本完全访问所有 PHP 特性。

了解文件和目录

将相关数据组织成通常被称为文件目录的实体,长期以来一直是现代计算环境中的核心概念。由于这个原因,程序员经常需要获得关于文件和目录的细节,比如位置、大小、最后修改时间、最后访问时间和其他定义信息。这一节介绍了 PHP 的许多内置函数来获取这些重要的细节。

目录分隔符

在基于 Linux 和 Unix 的操作系统上,斜线(/)用于分隔文件夹。在基于 Windows 的系统中,使用反斜杠()可以实现同样的功能。当在双引号字符串中使用反斜杠时,它也作为转义字符使用,因此\t 变成一个制表符,\n 变成一个换行符,\变成一个反斜杠字符。PHP 允许在基于 Linux 和基于 Windows 的系统上使用斜杠(/)。这使得在系统之间移动脚本变得容易,而不必使用特殊的逻辑来处理分隔符。

解析目录路径

解析各种属性的目录路径通常很有用,例如结尾扩展名、目录组件和基本名称。有几个函数可用于执行此类任务,本节将介绍所有这些函数。

检索路径的文件名

函数的作用是:返回路径的文件名部分。其原型如下:

string basename(string path [, string suffix])

如果提供了可选的suffix参数,如果返回的文件名包含该扩展名,则该后缀将被省略。下面是一个例子:

<?php
    $path = '/home/www/data/users.txt';
    printf("Filename: %s <br />", basename($path));
    printf("Filename without extension: %s <br />", basename($path, ".txt"));
?>

执行此示例会产生以下输出:

Filename: users.txt
Filename without extension: users

检索路径的目录

dirname()函数本质上是basename()的对应函数,提供路径的目录组件。其原型如下:

string dirname(string path)

以下代码将检索指向文件名users.txt的路径:

<?php
    $path = '/home/www/data/users.txt';
    printf("Directory path: %s", dirname($path));
?>

这将返回以下内容:

Directory path: /home/www/data

了解有关路径的更多信息

pathinfo()函数创建一个关联数组,包含路径的三个部分,即目录名、基本名和扩展名。其原型如下:

array pathinfo(string path [, options])

考虑以下路径:

/home/www/htdocs/book/chapter10/index.html

pathinfo()函数可用于将该路径解析为以下四个部分:

  • 目录名:/home/www/htdocs/book/chapter10

  • 基础名称:index.html

  • 文件扩展名:html

  • 文件名:index

您可以像这样使用pathinfo()来检索这些信息:

<?php
    $pathinfo = pathinfo('/home/www/htdocs/book/chapter10/index.html');
    printf("Dir name: %s <br />", $pathinfo['dirname']);
    printf("Base name: %s <br />", $pathinfo['basename']);
    printf("Extension: %s <br />", $pathinfo['extension']);
    printf("Filename: %s <br />", $pathinfo['filename']);
?>

这会产生以下输出:

Dir name: /home/www/htdocs/book/chapter10
Base name: index.html
Extension: html
Filename: index

可选的$options参数可用于修改返回四个支持属性中的哪一个。例如,通过将其设置为PATHINFO_FILENAME,只有 filename 属性将被填充到返回的数组中。关于支持的$options值的完整列表,请参见 PHP 文档。

识别绝对路径

realpath()函数将位于path中的所有符号链接和相对路径引用转换成它们的绝对对应物。其原型如下:

string realpath(string path)

例如,假设您的目录结构采用以下路径:

/home/www/htdocs/book/img/

您可以使用realpath()来解析任何本地路径引用:

<?php
    $imgPath = '../../img/cover.gif';
    $absolutePath = realpath($imgPath);
    // Returns /www/htdocs/book/img/cover.gif
?>

计算文件、目录和磁盘大小

计算文件、目录和磁盘大小是各种应用中的常见任务。本节介绍了许多适合这项任务的标准 PHP 函数。

确定文件的大小

函数的作用是:返回指定文件的大小,以字节为单位。其原型如下:

int filesize(string filename)

下面是一个例子:

<?php
    $file = '/www/htdocs/book/chapter1.pdf';
    $bytes = filesize($file);
    $kilobytes = round($bytes/1024, 2);
    printf("File %s is $bytes bytes, or %.2f kilobytes", basename($file), $kilobytes);
?>

这将返回以下内容:

File chapter1.pdf is 91815 bytes, or 89.66 kilobytes

计算磁盘的可用空间

函数disk_free_space()返回分配给存放指定目录的磁盘分区的可用空间,以字节为单位。其原型如下:

float disk_free_space(string directory)

下面是一个例子:

<?php
    $drive = '/usr';
    printf("Remaining MB on %s: %.2f", $drive,
             round((disk_free_space($drive) / 1048576), 2));
?>

这将返回所用系统的以下信息:

Remaining MB on /usr: 2141.29

注意,返回的数字是以兆字节(MB)为单位的,因为从disk_free_space()返回的值除以 1,048,576,相当于 1MB。

计算总磁盘大小

函数的作用是:返回包含指定目录的磁盘分区的总大小,以字节为单位。其原型如下:

float disk_total_space(string directory)

如果将这个函数与disk_free_space()结合使用,很容易提供有用的空间分配统计数据:

<?php

    $partition = '/usr';

    // Determine total partition space
    $totalSpace = disk_total_space($partition) / 1048576;

    // Determine used partition space
    $usedSpace = $totalSpace - disk_free_space($partition) / 1048576;

    printf("Partition: %s (Allocated: %.2f MB. Used: %.2f MB.)",
      $partition, $totalSpace, $usedSpace);
?>

这将返回所用系统的以下信息:

Partition: /usr (Allocated: 36716.00 MB. Used: 32327.61 MB.)

检索目录大小

PHP 目前没有提供检索目录总大小的标准函数,这是一项比检索总磁盘空间更常见的任务(见上一节disk_total_space())。尽管您可以使用exec()system()du进行系统级调用(这两个函数将在后面的“PHP 的程序执行函数”一节中介绍),但出于安全原因,这些函数通常会被禁用。另一个解决方案是编写一个定制的 PHP 函数来完成这个任务。递归函数似乎特别适合这项任务。清单 10-1 中提供了一种可能的变化。

注意

Unix du命令将总结文件或目录的磁盘使用情况。有关使用信息,请参见相应的手册页。

<?php
    function directorySize($directory) {
        $directorySize=0;

        // Open the directory and read its contents.
        if ($dh = opendir($directory)) {

            // Iterate through each directory entry.
            while (($filename = readdir ($dh))) {

                // Filter out some of the unwanted directory entries
                if ($filename != "." && $filename != "..")
                {

                    // File, so determine size and add to total
                    if (is_file($directory."/".$filename))
                        $directorySize += filesize($directory."/".$filename);

                    // New directory, so initiate recursion
                    if (is_dir($directory."/".$filename))
                        $directorySize += directorySize($directory."/".$filename);
                }
            }
        }
        closedir($dh);
        return $directorySize;

    }

    $directory = '/usr/book/chapter10/';
    $totalSize = round((directorySize($directory) / 1048576), 2);
    printf("Directory %s: %f MB", $directory, $totalSize);
?>

Listing 10-1Determining the Size of a Directory’s Contents

执行该脚本将产生类似如下的输出:

Directory /usr/book/chapter10/: 2.12 MB

opendir()closedir()函数有利于过程化实现,但是 PHP 也通过使用清单 10-2 中所示的DirectoryIterator类提供了一种更现代的面向对象的方法。

<?php
    function directorySize($directory) {
        $directorySize=0;

        // Open the directory and read its contents.
        $iterator = new DirectoryIterator($directory);
        foreach ($iterator as $fileinfo) {
            if ($fileinfo->isFile()) {
                $directorySize += $fileinfo->getSize();
            }
            if ($fileinfo->isDir() && !$fileinfo->isDot()) {
                $directorySize += directorySize($directory.'/'.$fileinfo->getFilename());
            }
        }

        return $directorySize;

    }

    $directory = '/home/frank';
    $totalSize = round((directorySize($directory) / 1048576), 2);
    printf("Directory %s: %f MB", $directory, $totalSize);
?>

Listing 10-2Determining the Size of a Directory’s Contents

确定访问和修改时间

确定文件的最后访问和修改时间的能力在许多管理任务中扮演着重要的角色,尤其是在涉及网络或 CPU 密集型更新操作的 web 应用中。PHP 提供了三个函数来确定文件的访问、创建和最后修改时间,所有这些都将在本节中介绍。

确定文件的上次访问时间

fileatime()函数以 Unix 时间戳的形式返回文件的最后访问时间,如果出错则返回 FALSE。Unix 时间戳是从 UTC 时区的 1970 年 1 月 1 日 st 开始的秒数。这个函数在 Linux/Unix 和 Windows 系统上都有效。其原型如下:

int fileatime(string filename)

下面是一个例子:

<?php
    $file = '/var/www/htdocs/book/chapter10/stat.php';
    printf("File last accessed: %s", date("m-d-y  g:i:sa", fileatime($file)));
?>

这将返回以下内容:

File last accessed: 06-09-10 1:26:14pm

确定文件的上次更改时间

filectime()函数以 Unix 时间戳格式返回文件的最后更改时间,或者在出错时返回FALSE。其原型如下:

int filectime(string filename)

下面是一个例子:

<?php
    $file = '/var/www/htdocs/book/chapter10/stat.php';
    printf("File inode last changed: %s", date("m-d-y  g:i:sa", filectime($file)));
?>

这将返回以下内容:

File inode last changed: 06-09-10 1:26:14pm

注意

最后更改时间不同于最后修改时间,因为最后更改时间是指文件的索引节点数据的任何更改,包括权限、所有者、组或其他索引节点特定信息的更改,而最后修改时间是指文件内容的更改(特别是字节大小)。

确定文件的上次修改时间

filemtime()函数以 Unix 时间戳格式返回文件的最后修改时间,否则返回FALSE。其原型如下:

int filemtime(string filename)

下面的代码演示了如何在网页上放置“上次修改”时间戳:

<?php
    $file = '/var/www/htdocs/book/chapter10/stat.php';
    echo "File last updated: ".date("m-d-y  g:i:sa", filemtime($file));
?>

这将返回以下内容:

File last updated: 06-09-10 1:26:14pm

使用文件

Web 应用很少是 100%独立的;也就是说,大多数都依赖某种外部数据源来做有趣的事情。这种数据源的两个主要例子是文件和数据库。在这一节中,通过介绍 PHP 众多与文件相关的标准函数,您将学习如何与文件进行交互。但是首先有必要介绍一些与这个主题相关的基本概念。

资源的概念

术语资源通常用于指代任何可以从中发起输入或输出流的实体。标准输入或输出、文件和网络套接字都是资源的例子。因此,您将经常看到本节中介绍的许多功能是在资源处理的上下文中讨论的,而不是在文件处理的上下文中讨论的,本质上,因为所有这些功能都能够处理上述资源。然而,因为它们与文件结合使用是最常见的应用,所以讨论将主要限于这一目的,尽管术语资源文件在全文中可以互换使用。

识别换行符

换行符由\n字符序列(Windows 上的\r\n)表示,表示文件中一行的结束。当您需要一次输入或输出一行信息时,请记住这一点。本章其余部分介绍的几个函数提供了专门处理换行符的功能。这些功能包括file()fgetcsv()fgets()

识别文件结尾字符

程序需要一种标准化的方法来识别什么时候到达了文件的末尾。这个标准通常被称为文件尾,或者文件尾,字符。这是一个非常重要的概念,几乎每种主流编程语言都提供了一个内置函数来验证解析器是否已经到达 EOF。对于 PHP 来说,这个函数就是feof()feof()函数确定资源的 EOF 是否已经达到。它在文件 I/O 操作中非常常用。其原型如下:

int feof(string resource)

在下面的例子中,在执行读取功能之前,不检查文件是否存在。这将导致一个连续的循环。最好在使用 fopen()函数之前验证它是否返回文件句柄:

<?php
    // Open a text file for reading purposes
    $fh = fopen('/home/www/data/users.txt', 'r');

    // While the end-of-file hasn't been reached, retrieve the next line
    while (!feof($fh)) echo fgets($fh);

    // Close the file
    fclose($fh);
?>

打开和关闭文件

通常,在对文件内容做任何事情之前,您需要创建一个所谓的句柄。同样,一旦您完成了对该资源的处理,您应该销毁该句柄。有两个标准函数可用于此类任务,这两个函数都将在本节中介绍。

打开文件

函数将一个文件绑定到一个句柄。一旦绑定,脚本就可以通过句柄与这个文件进行交互。其原型如下:

resource fopen(string resource, string mode [, int use_include_path
               [, resource context]])

虽然fopen()最常用于打开文件进行读取和操作,但它也能够通过许多协议打开资源,包括 HTTP、HTTPS 和 FTP,这是在第十六章中讨论的概念。

在资源打开时分配的模式决定了该资源可用的访问级别。各种模式在表 10-1 中定义。完整列表请点击 https://php.net/manual/en/function.fopen.php

表 10-1

文件模式

|

模式

|

描述

| | --- | --- | | r | 只读。文件指针放在文件的开头。 | | r+ | 读写。文件指针放在文件的开头。 | | w | 只写。在写入之前,删除文件内容并将文件指针返回到文件的开头。如果该文件不存在,请尝试创建它。 | | w+ | 读写。在读取或写入之前,删除文件内容并将文件指针返回到文件的开头。如果该文件不存在,请尝试创建它。 | | a | 只写。文件指针放在文件的末尾。如果该文件不存在,请尝试创建它。这种模式更好地被称为Append。 | | a+ | 读写。文件指针放在文件的末尾。如果该文件不存在,请尝试创建它。这个过程被称为追加到文件的*。* | | x | 创建并打开只写的文件。如果文件存在,fopen()将失败,并产生 E_WARNING 级错误。 | | x+ | 创建并打开文件进行写和写。如果文件存在,fopen()将失败,并产生 E_WARNING 级错误。 |

如果在本地文件系统上找到了资源,PHP 希望它可以通过它前面的路径获得。或者,您可以为fopen()use_include_path 参数分配1的值,这将使 PHP 在由include_path配置指令指定的路径中查找资源。

最后一个参数,上下文,用于设置特定于文件或流的配置参数,并用于在多个fopen()请求之间共享特定于文件或流的信息。该主题将在第十六章中详细讨论。

让我们考虑几个例子。第一个打开驻留在本地服务器上的文本文件的只读句柄:

$fh = fopen('/var/www/users.txt', 'r');

下一个示例演示如何打开 HTML 文档的写句柄:

$fh = fopen('/var/www/docs/summary.html', 'w');

下一个例子引用同一个 HTML 文档,只是这次 PHP 将在由include_path指令指定的路径中搜索文件(假设summary.html文档位于上一个例子中指定的位置,include_path将需要包含路径/usr/local/apache/data/docs/):

$fh = fopen('summary.html', 'w', 1);

最后一个例子打开一个远程文件的只读流Example Domain.html.。文件名是服务器提供的默认文档,如果给出完整路径而不仅仅是域名,它可以是 index.html、index.php 或特定文件。

$fh = fopen('http://www.example.com/', 'r');

当然,请记住fopen()只是为即将到来的操作准备资源。除了建立句柄之外,它不需要您使用其他函数来实际执行读写操作。这些功能将在接下来的章节中介绍。

关闭文件

良好的编程实践表明,一旦使用完任何资源,就应该销毁指向它们的指针。fclose()函数为您处理这个问题,关闭由文件句柄指定的先前打开的文件指针,如果成功返回 TRUE,否则返回 FALSE。其原型如下:

boolean fclose(resource filehandle)

文件句柄必须是使用fopen()fsockopen()打开的现有文件指针。当脚本终止时,PHP 将关闭未被脚本关闭的文件句柄。在 web 上下文中,这通常会在请求发起后的几毫秒或几秒内发生。如果 PHP 被用作一个 shell 脚本,这个脚本可能会运行很长时间,当不再使用时,文件句柄应该被关闭。

从文件中读取

PHP 提供了许多从文件中读取数据的方法,从一次只读取一个字符到一次读取整个文件。本节介绍了许多最有用的功能。

将文件读入数组

在前面的例子中,我们已经使用了文件句柄来打开、访问和关闭文件系统中的文件。一些文件处理函数可以执行文件操作,其中打开和关闭步骤内置于函数调用中。这使得处理更小的文件(更少的代码)很方便。对于较大的文件,可能有必要使用文件句柄并以小块的形式处理文件,以便节省内存。file()函数能够将一个文件读入一个数组,用换行符分隔每个元素,换行符仍然附加在每个元素的末尾。其原型如下:

array file(string filename [int use_include_path [, resource context]])

尽管过于简单,但这个函数的重要性不能被夸大,因此它保证了一个简单的演示。考虑以下名为users.txt的示例文本文件:

Ale ale@example.com
Nicole nicole@example.com
Laura laura@example.com

下面的脚本读入users.txt并解析和转换数据为方便的基于 web 的格式:

<?php

    // Read the file into an array
    $users = file('users.txt');

    // Cycle through the array
    foreach ($users as $user) {

        // Parse the line, retrieving the name and e-mail address
        list($name, $email) = explode(' ', $user);

        // Remove newline from $email
        $email = trim($email);

        // Output the formatted name and e-mail address
        echo "<a href=\"mailto:$email\">$name</a> <br /> ";

    }

?>

该脚本产生以下 HTML 输出:

<a href="mailto:ale@example.com">Ale</a><br />
<a href="mailto:nicole@example.com">Nicole</a><br />
<a href="mailto:laura@example.com">Laura</a><br />

fopen()一样,你可以通过将 use_include_path 设置为1来告诉file()搜索include_path配置参数中指定的路径。context参数指的是一个流上下文。在第十六章你会学到更多关于这个主题的知识。

将文件内容读入字符串变量

file_get_contents()函数是另一个函数,除了读取所有内容之外,它还处理文件的打开和关闭。它将文件的内容读入一个字符串。其原型如下:

string file_get_contents(string filename [, int use_include_path [, resource context [, int offset [, int maxlen]]]])

通过修改前一节中的脚本,使用file_get_contents()函数代替file(),您将获得以下代码:

<?php

    // Read the file into a string variable
    $userfile= file_get_contents('users.txt');

    // Place each line of $userfile into array
    $users = explode("\n", $userfile);

    // Cycle through the array
    foreach ($users as $user) {

        // Parse the line, retrieving the name and e-mail address
        list($name, $email) = explode(' ', $user);

        // Output the formatted name and e-mail address
        printf("<a href='mailto:%s'>%s</a> <br />", $email, $name);
    }

?>

使用 _ 包含 _ 路径上下文参数的操作方式与上一节中定义的方式相同。可选的偏移参数决定了文件中file_get_contents()函数开始读取的位置。可选的 maxlen 参数决定读入字符串的最大字节数。

将 CSV 文件读入数组

方便的fgetcsv()函数解析以 CSV 格式标记的文件的每一行。其原型如下:

array fgetcsv(resource handle [, int length [, string delimiter
              [, string enclosure]]])

阅读不会在新行上停止;相反,当length字符被读取时,它停止。省略length或将其设置为0将导致线路长度不受限制;但是,由于这会降低性能,所以选择一个一定会超过文件中最长行的数字总是一个好主意。可选的delimiter参数(默认设置为逗号)标识用于分隔每个字段的字符。可选的enclosure参数(默认情况下设置为双引号)标识用于括住字段值的字符,这在分配的分隔符值也可能出现在字段值中时非常有用,尽管是在不同的上下文中。

注意

在应用之间导入文件时,通常使用逗号分隔值(CSV)文件。Microsoft Excel 和 Access、MySQL、Oracle 和 PostgreSQL 只是能够导入和导出 CSV 数据的一些应用和数据库。此外,Perl、Python 和 PHP 等语言在解析分隔数据方面特别有效。

考虑这样一个场景,其中每周简讯订阅者数据被缓存到一个文件中,供营销人员阅读。该文件可能如下所示:

Jason Gilmore,jason@example.com,614-555-1234
Bob Newhart,bob@example.com,510-555-9999
Carlene Ribhurt,carlene@example.com,216-555-0987

假设市场部想要一个简单的方法在网上浏览这个列表。这个任务用fgetcsv()很容易完成。下列范例会剖析档案:

<?php

    // Open the subscribers data file
    $fh = fopen('/home/www/data/subscribers.csv', 'r');

    // Break each line of the file into three parts
    while (list($name, $email, $phone) = fgetcsv($fh, 1024, ',')) {
        // Output the data in HTML format
        printf("<p>%s (%s) Tel. %s</p>", $name, $email, $phone);
    }

?>

请注意,您不一定要使用fgetcsv()来解析这样的文件;只要文件内容简单(没有逗号作为任何列的一部分),那么file()list()函数就可以很好地完成这项工作。另一个(更好的)选择是用file_get_content()和用户str_getcsv()读取整个内容来解析内容。我们可以修改前面的示例,改为使用后面的函数:

<?php

    // Read the file into an array
    $users = file('/home/www/data/subscribers.csv');

    foreach ($users as $user) {

        // Break each line of the file into three parts
        list($name, $email, $phone) = explode(',', $user);

        // Output the data in HTML format
        printf("<p>%s (%s) Tel. %s</p>", $name, $email, $phone);

    }

?> 

读取特定数量的字符

fgets()函数返回通过打开的资源句柄读入的一定数量的字符,或者在遇到换行符或 EOF 字符时它已经读取的所有内容。其原型如下:

string fgets(resource handle [, int length])

如果省略可选的长度参数,它将一直读到第一个换行符或 EOF 符。下面是一个例子:

<?php
    // Open a handle to users.txt
    $fh = fopen('/home/www/data/users.txt', 'r');
    // While the EOF isn't reached, read in another line and output it
    while (!feof($fh)) echo fgets($fh);

    // Close the handle
    fclose($fh);
?>

从输入中去除标签

fgetss()函数的操作类似于fgets(),除了它也从输入中去除任何 HTML 和 PHP 标签。其原型如下:

string fgetss(resource handle, int length [, string allowable_tags])

如果您想忽略某些标签,请将它们包含在allowable_tags参数中。请注意,允许的标签可能包含 JavaScript 代码,如果内容作为网站的一部分提供给用户,这些代码可能是有害的。如果用户提供的内容被提供回网站。HTML 应该被剥离或转换成 HTML 实体,以便它们被显示为类型,而不是被浏览器解析(执行)为 HTML/JavaScript。作为一个例子,考虑一个场景,其中期望贡献者使用 HTML 标签的指定子集以 HTML 格式提交他们的工作。当然,贡献者并不总是遵循指示,所以在文件可以发布之前,必须过滤标签误用。对于fgetss(),这是微不足道的:

<?php

    // Build list of acceptable tags
    $tags = '<h2><h3><p><b><a><img>';

    // Open the article, and read its contents.
    $fh = fopen('article.html', 'r');

    while (! feof($fh)) {
        $article .= fgetss($fh, 1024, $tags);
    }
    // Close the handle
    fclose($fh);

    // Open the file up in write mode and output its contents.
    $fh = fopen('article.html', 'w');
    fwrite($fh, $article);

    // Close the handle
    fclose($fh);

?>

小费

如果您想从通过表单提交的用户输入中删除 HTML 标签,请查看第九章中介绍的 strip_tags()函数。

一次一个字符地读取文件

fgetc()函数从handle指定的开放资源流中读取一个字符。如果遇到 EOF,则返回一个值FALSE。其原型如下:

string fgetc(resource handle)

可以在 CLI 模式下使用此函数从键盘读取输入,如下例所示:

<?php
echo 'Are you sure you want to delete? (y/n) ';
$input = fgetc(STDIN);

if (strtoupper($input) == 'Y')
{
    unlink('users.txt');
}
?>

忽略换行符

fread()函数从handle指定的资源中读取length字符。当到达 EOF 或length字符被读取时,读取停止。其原型如下:

string fread(resource handle, int length)

注意,与其他读取函数不同,在使用fread()时,换行符是不相关的,这对于读取二进制文件非常有用。因此,使用filesize()来确定应该读入的字符数,可以方便地一次读入整个文件:

<?php

    $file = '/home/www/data/users.txt';

    // Open the file for reading
    $fh = fopen($file, 'r');

    // Read in the entire file
    $userdata = fread($fh, filesize($file));

    // Close the file handle
    fclose($fh);

?>

变量$userdata现在包含了users.txt文件的内容。这种方法通常用于读取和处理大块文件。它将允许在不将整个文件读入内存的情况下完成处理。对于较小的文件,使用file_get_contents()在一条语句中读取文件更有效。要以 1,024 字节为单位读取文件,可以使用以下示例:

<?php

    $file = '/home/www/data/users.txt';

    // Open the file for reading
    $fh = fopen($file, 'r');

    // Read in the entire file
    while($userdata = fread($fh, 1024)) {
      // process $userdata
    }

    // Close the file handle
    fclose($fh);

?>

输出整个文件

readfile()函数读取由filename指定的整个文件,并立即将其输出到输出缓冲区,返回读取的字节数。其原型如下:

int readfile(string filename [, int use_include_path])

如果文件太大,无法在内存中处理,可以使用fpassthru()打开文件,然后分块读取,并将输出发送给客户端。

启用可选的use_include_path参数告诉 PHP 搜索由include_path配置参数指定的路径。如果您只想将整个文件转储到发出请求的浏览器/客户端,此函数非常有用:

<?php

   $file = '/home/www/articles/gilmore.html';

   // Output the article to the browser.
   $bytes = readfile($file);

?>

这种方法允许在文档根目录之外存储文件,并在将文件发送到客户机之前使用 PHP 执行访问控制。对于较大的文件,此方法可能会超出内存限制,除非关闭输出缓冲。处理这些请求的更有效的方法是在 Apache 服务器中安装一个扩展(XSendFile)。这将仍然允许 PHP 用于访问控制,但是它将使用 Apache 来读取文件并将其发送给客户端。这通常是通过设置 HTTP 头来处理的,该头向 web 服务器提供文件位置。NginX 支持这一点,不需要扩展。

像 PHP 的许多其他文件 I/O 函数一样,如果配置参数fopen_wrappers被启用,远程文件可以通过它们的 URL 打开。请注意,远程文件可能包含恶意代码,只有当您对远程文件拥有 100%的控制权时,才应使用此功能。

根据预定义的格式读取文件

fscanf()函数提供了一种根据预定义格式解析资源的便捷方法。其原型如下:

mixed fscanf(resource handle, string format [, string var1])

例如,假设您想要解析以下由社会保险号(SSN) ( socsecurity.txt)组成的文件:

123-45-6789
234-56-7890
345-67-8901

以下示例解析socsecurity.txt文件:

<?php

    $fh = fopen('socsecurity.txt', 'r');

    // Parse each SSN in accordance with integer-integer-integer format

    while ($user = fscanf($fh, "%d-%d-%d")) {

        // Assign each SSN part to an appropriate variable
        list ($part1,$part2,$part3) = $user;
        printf("Part 1: %d Part 2: %d Part 3: %d <br />", $part1, $part2, $part3);
     }

   fclose($fh);

?>

当在浏览器中查看时,会产生类似于以下内容的输出:

Part 1: 123 Part 2: 45 Part 3: 6789
Part 1: 234 Part 2: 56 Part 3: 7890
Part 1: 345 Part 2: 67 Part 3: 8901

在每次迭代中,变量$part1$part2$part3分别被赋予每个 SSN 的三个分量,并输出到浏览器。

将字符串写入文件

函数将一个字符串变量的内容输出到指定的资源中。其原型如下:

int fwrite(resource handle, string string [, int length])

如果提供可选的length参数,当length字符被写入时,fwrite()将停止写入。否则,当发现string结束时,写入将停止。考虑这个例子:

<?php

   // Data we'd like to write to the subscribers.txt file
   $subscriberInfo = 'Jason Gilmore|jason@example.com';

   // Open subscribers.txt for writing
   $fh = fopen('/home/www/data/subscribers.txt', 'a');

   // Write the data
   fwrite($fh, $subscriberInfo);

   // Close the handle
   fclose($fh);

?>

提示移动文件指针

在一个文件中跳来跳去,在不同的位置读取和写入通常是很有用的。有几个 PHP 函数可以做到这一点。

将文件指针移动到特定的偏移量

fseek()函数将指针移动到由提供的偏移值指定的位置。其原型如下:

int fseek(resource handle, int offset [, int whence])

如果省略可选参数whence,则从文件开头的offset字节开始设置位置。否则,where可以设置为三个可能值之一,这会影响指针的位置:

  • SEEK_CUR:将指针位置设置到当前位置加上offset字节。

  • SEEK_END:将指针位置设置为 EOF 加offset字节。在这种情况下,offset必须设置为负值。

  • SEEK_SET:将指针位置设置为offset字节。这与省略whence有相同的效果。

检索当前指针偏移量

ftell()函数获取文件指针在资源中的偏移量的当前位置。其原型如下:

int ftell(resource handle)

将文件指针移回到文件的开头

函数的作用是:将文件指针移回到资源的开头。其原型如下:

int rewind(resource handle)

这个和fseek($res, 0)一样。

正在读取目录内容

读取目录内容的过程与读取文件的过程非常相似。本节介绍了可用于此任务的函数,还介绍了将目录内容读入数组的函数。

打开目录句柄

就像fopen()打开一个指向给定文件的文件指针一样,opendir()打开一个由路径指定的目录流。其原型如下:

resource opendir(string path [, resource context])

关闭目录句柄

closedir()函数关闭目录流。其原型如下:

void closedir(resource directory_handle)

解析目录内容

readdir()函数返回目录中的每个元素。其原型如下:

string readdir([resource directory_handle])

此外,您可以使用此函数列出给定目录中的所有文件和子目录:

<?php
    $dh = opendir('/usr/local/apache2/htdocs/');
    while ($file = readdir($dh))
        echo "$file <br />";
    closedir($dh);
?>

示例输出如下:

.
..
articles
images
news
test.php

注意,readdir()还返回典型 Unix 目录列表中常见的...条目。您可以使用if语句轻松地过滤掉这些:

if($file != "." && $file != "..")
  echo "$file <br />";

如果可选的 directory_handle 参数没有被赋值,那么 PHP 将试图从opendir()打开的最后一个链接中读取。

将目录读入数组

scandir()函数返回一个由在directory中找到的文件和目录组成的数组,或者在出错时返回FALSE。其原型如下:

array scandir(string directory [,int sorting_order [, resource context]])

将可选的sorting_order参数设置为1会以降序对内容进行排序,覆盖默认的升序。执行此示例(来自上一节):

<?php
    print_r(scandir('/usr/local/apache2/htdocs'));
?>

返回所用系统的以下信息:

Array ( [0] => . [1] => .. [2] => articles [3] => images
[4] => news [5] => test.php )

上下文参数指的是流上下文。你会在第十六章中了解到更多关于这个话题的内容。

scandir()函数不会递归扫描目录。如果您需要这样做,您可以将函数包装在一个递归函数中。

执行 Shell 命令

与底层操作系统交互的能力是任何编程语言的重要特性。虽然可以想象使用像exec()system()这样的函数来执行任何系统级命令,但是其中一些函数太普通了,以至于 PHP 开发人员认为将它们直接集成到语言中是个好主意。本节将介绍几个这样的函数。

删除目录

rmdir()函数试图删除指定的目录,如果成功返回TRUE,否则返回FALSE。其原型如下:

int rmdir(string dirname)

与 PHP 的许多文件系统函数一样,为了让rmdir()成功删除目录,必须正确设置权限。因为 PHP 脚本通常在服务器守护进程所有者的伪装下执行,rmdir()将会失败,除非该用户拥有对该目录的写权限。此外,目录必须为空。

要删除非空目录,您可以使用能够执行系统级命令的函数,如system()exec(),或者编写一个递归函数,在尝试删除目录之前删除所有文件内容。请注意,在这两种情况下,执行用户(服务器守护进程所有者)都需要对目标目录的父目录进行写访问。以下是后一种方法的一个示例:

<?php
    function deleteDirectory($dir)
    {
        // open a directory handle
        if ($dh = opendir($dir))
        {
            // Iterate through directory contents
            while (($file = readdir ($dh)) != false)
            {
                // skup files . and ..
                if (($file == ".") || ($file == "..")) continue;
                if (is_dir($dir . '/' . $file))
                    // Recursive call to delete subdirectory
                    deleteDirectory($dir . '/' . $file);
                else
                    // delete file
                    unlink($dir . '/' . $file);
            }

           closedir($dh);
           rmdir($dir);
        }
    }

    $dir = '/usr/local/apache2/htdocs/book/chapter10/test/';
    deleteDirectory($dir);
?>

重命名文件

函数的作用是重命名一个文件,如果成功则返回TRUE,否则返回FALSE。其原型如下:

boolean rename(string oldname, string newname [, resource context])

因为 PHP 脚本通常在服务器守护进程所有者的伪装下执行,rename()将会失败,除非该用户拥有对该文件的写权限。上下文参数指的是流上下文。你会在第十六章学到更多关于这个话题的知识。

rename功能可用于更改文件的名称或位置。参数 oldname 和 newname 都通过相对于脚本的文件路径或使用绝对路径来引用文件。

触摸文件

touch()函数设置文件filename的最后修改和最后访问时间,如果成功返回TRUE,如果错误返回FALSE。其原型如下:

int touch(string filename [, int time [, int atime]])

如果没有提供时间,则使用当前时间(由服务器指定)。如果提供了可选的 atime 参数,访问时间将被设置为该值;否则,与修改时间一样,它将被设置为time或当前服务器时间。

注意,如果filename不存在,它将被创建,假设脚本的所有者拥有足够的权限。

系统级程序执行

真正懒惰的程序员知道如何在开发应用时充分利用他们的整个服务器环境,包括在必要时利用操作系统、文件系统、已安装程序库和编程语言的功能。在本节中,您将了解 PHP 如何与操作系统交互,以调用操作系统级程序和第三方安装的应用。如果处理得当,它将为您的 PHP 编程清单增加一个全新的功能级别。如果做得不好,不仅对您的应用,而且对您的服务器的数据完整性都是灾难性的。也就是说,在深入研究这个强大的特性之前,先花点时间考虑一下在将用户输入传递到 shell 级别之前净化用户输入的主题。

净化输入

忽略对可能随后传递给系统级函数的用户输入进行清理,可能会使攻击者对您的信息存储和操作系统进行大规模内部破坏,篡改或删除 web 文件,并以其他方式获得对您的服务器的无限制访问。而这仅仅是开始。

注意

参见第十三章关于安全 PHP 编程的讨论。

作为净化输入如此重要的一个例子,考虑一个真实的场景。假设您提供一个从输入 URL 生成 pdf 的在线服务。一个很好的工具就是开源程序 wkhtmltopdf ( https://wkhtmltopdf.org/ ),它是一个开源的命令行工具,可以将 HTML 转换成 pdf:

%> wkhtmltopdf http://www.wjgilmore.com/ webpage.pdf

这将导致创建一个名为webpage.pdf的 PDF,其中将包含网站索引页面的快照。当然,大多数用户不能通过命令行访问您的服务器;因此,你需要创建一个更好控制的界面,比如一个网页。使用 PHP 的passthru()函数(在后面的“PHP 的程序执行函数”一节中介绍),您可以调用 wkhtmltopdf 并返回所需的 pdf,如下所示:

$document = $_POST['userurl'];
passthru("wkhtmltopdf $document webpage.pdf");

如果一个有事业心的攻击者冒昧地传递与所需 HTML 页面无关的附加输入,输入如下内容,会怎么样呢:

http://www.wjgilmore.com/ ; cd /var/www/; rm –rf *;

大多数 Unix shells 会将passthru()请求解释为三个独立的命令。首先是这样的:

wkhtmltopdf http://www.wjgilmore.com/

第二个命令是:

cd /var/www

第三个命令是:

rm -rf *

最后的命令是这样的:

webpage.pdf

其中两个命令肯定是意外的,可能会导致删除整个 web 文档树。防止这种尝试的一种方法是在将用户输入传递给 PHP 的任何程序执行函数之前对其进行净化。为此,有两个标准函数可以方便地使用:escapeshellarg()escapeshellcmd()

定界输入

escapeshellarg()函数用单引号和输入中的前缀(转义)引号分隔提供的参数。其原型如下:

string escapeshellarg(string arguments)

其效果是,当参数被传递给 shell 命令时,它将被认为是一个参数。这一点非常重要,因为它降低了攻击者将附加命令伪装成 shell 命令参数的可能性。因此,在之前噩梦般的场景中,整个用户输入将被括在单引号中,如下所示:

'http://www.wjgilmore.com/ ; cd /usr/local/apache/htdoc/; rm –rf *;'

结果将是 wkhtmltopdf 将简单地返回一个错误,而不是删除整个目录树,因为它不能解析拥有该语法的 URL。

逃避潜在的危险输入

escapeshellcmd()函数在与escapeshellarg()相同的前提下运行,通过转义 shell 元字符来清除潜在的危险输入。其原型如下:

string escapeshellcmd(string command)

这些字符包括以下:# & ; , | * ? , ~ < > ^ ( ) [ ] { } $ \\ \x0A \xFF

escapeshellcmd()应该用于整个命令,而escapeshellarg()应该用于单个参数。

PHP 的程序执行函数

本节介绍了几个函数(除了反斜线执行操作符之外),用于通过 PHP 脚本执行系统级程序。虽然乍一看,它们似乎在操作上完全相同,但每一个都有其语法上的细微差别。

执行系统级命令

exec()函数最适合于执行打算在服务器后台继续运行的操作系统级应用。其原型如下:

string exec(string command [, array &output [, int &return_var]])

虽然最后一行输出将被返回,但您可能希望所有的输出都被返回以供审查;您可以通过包含可选参数 output 来做到这一点,该参数将在完成由exec()指定的命令时由每行输出填充。此外,您可以通过包含可选参数 return_var 来发现所执行命令的返回状态。

虽然我可以采取简单的方法,演示如何使用exec()来执行 ls 命令(对于 Windows 用户来说是 dir ),返回目录列表,但是提供一个更实际的例子更有意义:如何从 PHP 调用 Perl 脚本。考虑下面的 Perl 脚本(languages.pl):

#! /usr/bin/perl
my @languages = qw[perl php python java c];
foreach $language (@languages) {
     print $language."<br />";
}

注意下面的例子要求 Perl 安装在您的系统上。Perl 是许多 Linux 发行版的一部分,它也可以安装在 Windows 系统上。可以从 ActiveState https://www.activestate.com/activeperl/downloads 下载版本。

Perl 脚本非常简单;不需要第三方模块,所以您只需投入很少的时间就可以测试这个示例。如果您运行的是 Linux,那么您很有可能立即运行这个例子,因为 Perl 安装在每一个值得尊敬的发行版上。如果您运行的是 Windows,请查看 ActiveState 的( https://www.activestate.com ) ActivePerl 发行版。

languages.pl一样,这里显示的 PHP 脚本并不完全是火箭科学;它只是调用 Perl 脚本,指定将结果放入名为results的数组中。results 的数组中。result s的内容随后被输出到浏览器:

<?php
    $outcome = exec("languages.pl", $results);
    foreach ($results as $result) echo $result;
?>

结果如下:

perl
php
python
java
c

检索系统命令的结果

当您想要输出执行命令的结果时,system()功能很有用。其原型如下:

string system(string command [, int return_var])

不像exec()那样通过可选参数返回输出,输出的最后一行直接返回给调用者。但是,如果您想查看被调用程序的执行状态,您需要使用可选参数return_var指定一个变量。

例如,假设您想要列出特定目录中的所有文件:

$mymp3s = system("ls -1 /tmp/ ");

下面的例子调用前面提到的languages.pl脚本,这次使用的是system():

<?php
    $outcome = system("languages.pl", $results);
    echo $outcome
?>

返回二进制输出

passthru()函数在功能上与exec()相似,除了如果您想将二进制输出返回给调用者,应该使用它。其原型如下:

void passthru(string command [, int &return_var])

例如,假设您想在将 GIF 图像显示到浏览器之前将其转换为 PNG。您可以使用 Netpbm 图形包,可在 GPL 许可下从 https://netpbm.sourceforge.net 获得:

<?php
    header('ContentType:image/png');
    passthru('giftopnm cover.gif | pnmtopng > cover.png');
?>

执行带反斜杠的 Shell 命令

用反斜杠分隔字符串向 PHP 发出信号,表明该字符串应该作为 shell 命令执行,并返回任何输出。请注意,反引号不是单引号,而是一个倾斜的兄弟,通常与大多数美国键盘上的波浪号(~)共用一个键。下面是一个例子:

<?php
    $result = `date`;
    printf("<p>The server timestamp is: %s", $result);
?>

这将返回类似于以下内容的内容:

The server timestamp is: Sun Mar 3 15:32:14 EDT 2010

在基于 Windows 的系统上,date 函数的功能略有不同,输出将包含输入新日期的提示。

反勾运算符在操作上与下面的shell_exec()函数相同。

反斜线的替代方法

shell_exec()函数提供了反斜杠的语法替代,执行 shell 命令并返回输出。其原型如下:

string shell_exec(string command)

重新考虑前面的例子,这次我们将使用shell_exec()函数来代替反斜线:

<?php
    $result = shell_exec('date');
    printf("<p>The server timestamp is: %s</p>", $result);
?>

摘要

虽然单独使用 PHP 来构建有趣而强大的 web 应用肯定会有很长的路要走,但是当功能与底层平台和其他技术集成时,这种能力会大大扩展。在本章中,这些技术包括底层操作系统和文件系统。在本书的其余部分,你会反复看到这个主题。

在下一章,将向您介绍 PHP 扩展和应用库(PEAR)。