PHP7 Zend 认证学习指南(二)

原文:PHP 7 ZEND CERTIFICATION STUDY GUIDE

协议:CC BY-NC-SA 4.0

三、字符串和模式

PHP 字符串是一系列字节,不包含任何关于如何将这些字节转换成字符的信息。

PHP 存储字符串的长度及其内容,并且不依赖终止字符来表示字符串的结尾。这有助于使字符串二进制安全,因为字符串中的空字符不会引起混淆。

在 32 位系统上,一个字符串可以长达 2 GB。在 64 位 PHP 系统中,字符串的长度没有特别的限制。

声明字符串

在 PHP 中,字符串可以声明为简单类型或复杂类型。不同之处在于,复杂字符串将根据控制字符和变量进行计算。

简单字符串在'single quote marks'中声明,而复杂字符串在"double quote marks"中声明。

在这个例子中,换行符在Hello Bob之后输出,但是在简单字符串中,输出的是文字字符。

<?php
$name = 'Bob';
$a = 'Hello $name\n';
$b = "Hello $name\n";
echo $a;       // Hello $name\n
echo $b;       // Hello Bob

还要注意,变量$name被评估为字符串"Bob",并在输出时被插入到复杂变量$b中。我们将在下一节更详细地讨论这个问题。

嵌入变量

复杂字符串的主要优点之一是 PHP 将解析它们并自动计算其中包含的变量名。

当使用不计算的简单字符串时,您需要终止字符串并将变量连接到它。

在 PHP 中,变量名由一个$标记。当解析器在字符串中遇到一个时,它会尝试通过添加尽可能多的字母数字字符来形成一个有效的变量名。

以下示例说明了将变量连接到字符串和将它们嵌入复杂字符串之间的区别。

<?php
$catfood = "Cheeseburgers";
echo 'I can haz $catfood';         // I can haz $catfood
echo 'I can haz ' . $catfood;      // I can haz Cheeseburgers?
echo "I can haz $catfood?";        // I can haz Cheeseburgers?

注意,第一个字符串是用单引号标记的,所以$catfood不是一个变量。而是作为文字字符输出。要在简单的字符串中包含变量,您需要将它们连接起来,如第二个示例所示。

第三个echo语句显示了一个在复杂字符串中计算变量名的例子。解析器遇到$符号,然后获取它后面所有合法的变量名字符。问号符号不允许出现在变量名中,所以 PHP 将变量$catfood的文字值插入到字符串中。

也可以用双引号包含数组和对象语法:

<?php
$dogfood = ['Pellets'];
$catfood = new stdClass();
$catfood->favorite = "Cheeseburger";
echo "$dogfood[0]";             // Pellets
echo "$catfood->favorite";      // Cheeseburger

PHP 允许使用花括号来明确告诉解析器必须对字符串的一部分求值。

例如,当输出数组中的元素时,可能无法立即确定方括号是作为字符串中的标点符号还是作为引用数组中元素的语法,这是必要的。

让我们看一些它的用法的例子:

<?php
$burger = "Cheeseburger";
echo "I can haz {$burger}";         // I can haz Cheeseburger
echo "I can haz ${burger}";         // I can haz Cheeseburger
echo "I can haz $burgers";          // no variable $burgers
echo "I can haz {$burger}s";        // I can haz Cheeseburgers
echo "I can haz { $burger }";       // I can haz { Cheeseburger }

请注意,在大括号和要计算的变量之间不能使用空格。因为大括号明确表示字符串中变量的结尾,所以可以包含紧跟其后的字符。在之前的例子中,我们看到"{$burger}s"被渲染为芝士汉堡。

让我们看一个混合了数组和对象属性语法的例子,来演示花括号是如何起作用的:

<?php
$catfood = new stdClass();
$catfood->name = "Cheeseburgers";
$cat = new stdClass();
$cat->canhaz = [$catfood];
echo "$cat->canhaz[0]->name";       // array to string conversion
echo "{$cat->canhaz[0]->name}";     // Cheeseburgers

控制字符

当 PHP 遇到一个复杂的字符串,一个它用双引号声明的字符串,它将计算它的变量和控制字符。

控制字符由代码后面的反斜杠标记。使用反斜杠后跟除控制字符以外的任何字符将导致显示反斜杠。

<?php
echo "Hello \World"; // Hello \World

关于转义序列 1 的 PHP 手册页有一个可以使用的控制字符列表,但这里是以表格的形式:

顺序意义
\n换行
\r回车
\t标签
\v垂直标签
\e逃跑
\f换页
\\反斜线符号
\$美元符号
[0-7]{1,3}匹配这个正则表达式的序列用八进制表示
\x[0-9A-Fa-f]{1,2}匹配序列采用十六进制表示法
\u{{0-9a-f}{1,6}}匹配序列是一个 Unicode 码点,它将作为码点 UTF-8 表示输出到字符串

表情符号有 Unicode 端点,所以我们可以像这样输出大象:

<?php

echo "\u{1F418}";  // 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当然,Unicode 的一个更正式的用例是国际化(i18n)。稍后我们将了解更多相关信息。

Heredoc 和 Nowdoc

heredoc 是声明跨越多行的字符串的一种便捷方式。您可以用一种简单的格式声明字符串,而不必添加多个换行符。

Heredoc 字符串被评估为控制字符和变量,就像双引号字符串一样。

heredoc 的常见用途包括创建 SQL 查询,或者为电子邮件或网页创建格式化的 HTML 片段。你也可以用它们来初始化变量,或者任何你想使用跨多行字符串的地方。

Nowdoc 是在 PHP 5.3.0 中引入的,它将单引号字符串转化为双引号字符串。换句话说,不对 nowdocs 进行特殊字符和变量的计算。

这里文档使用如下语法:

<?php
echo <<<HEREDOC
  This is a heredoc string, note:
  1) the capitalization of the tag
  2) the tag name follows variable naming rules
  3) where the closing tag is
HEREDOC;

Note

结束标记必须从新行的第一个字符开始。

通过用单引号将标签括起来,可以指定字符串是 nowdoc 而不是 heredoc,如下所示:

<?php
echo <<<'NOWDOC'
This is a nowdoc string, note:
    1) Single quotes around the label
    2) Variables will not be evaluated
    3) Control characters will not be evaluated
NOWDOC;

引用字符串中的字符

通过使用方括号或花括号来表示要引用的从零开始的整数位置,可以引用字符串中的位置。

<?php
$hello = "world";
echo $hello[0]; // w
echo $hello{1}; // o

Caution

请记住,字符串是一系列字节,您引用的是字节位置。如果您的字符集每个字符使用一个以上的字节,您将不会得到您期望的结果。

在其当前版本中,如果您试图写入字符串的负位置,或者如果您没有指定整数位置,PHP 将发出一个范围警告。

写入超出范围的位置将导致字符串被空格填充以容纳缺失的部分。

<?php
$hello = "world";
$hello[10] = "*";
echo $hello; //  world     *

请注意前面示例中的尾随星号。

PHP 和多字节字符串

PHP 将字符串实现为一个字节数组,用一个整数表示缓冲区的长度(不以空值结束)。PHP 不存储关于字符串如何编码的信息。

可变宽度编码方案使用不同长度的代码对字符集进行编码。多字节编码使用不同数量的字节来编码字符。

多字节编码允许在计算机上编码和表示大量的字符。你在 PHP 中经常会遇到的编码方案之一是 UTF-8。 2 这是 PHP 将尝试使用的多字节编码的默认方案。

PHP 中的原生字符串函数假设字符串是一个单字节数组,所以像substr()strpos()strlen()strcmp()这样的函数不能处理多字节字符串。

您应该使用这些函数的多字节等价物,例如mb_substr()

统一码

Unicode 试图统一所有代表字符的代码集。Unicode 定义了代码点,这些代码点是字符的抽象概念。一个 Unicode 码点代表一个字符,写成这样:U+0041。该数字被指定为大写字母“A”。

Unicode 可以存储的字符没有限制。最初对于 Unicode 是两个字节有一些混淆,但这与编码方案有关,而与 Unicode 本身无关。

Note

Unicode 本身不是编码系统。编码是表示 Unicode 字符的方式。

UTF-8 将从 0 到 127 的所有码点存储在一个字节中。这涵盖了英语字母表、数字和一些符号的整个范围。127 以上的码点存储在多个字节中(最多 6 个字节)。

因为 0-127 的 Unicode 码位与 0-127 的 ASCII 表相匹配,所以用 UTF 8 编码的英语文本看起来就像用 ASCII 编码的一样。

只有那些用重音符号书写字符的人最终会得到一个与 ASCII 编码不同的文件。有数百种编码方案可以存储部分 Unicode 码位,但不是全部。

如果您使用这些编码之一并遇到无法表示的 Unicode 字符,您将看到一个问号或一个空框。

例如,如果您的编码方案适合存储希伯来语字符,而您试图在其中存储俄语字符,您将得到一串问号而不是俄语字符,因为编码方案不支持它们。

告诉客户端字符串是如何编码的

您无法确定地检测一个字符串是如何编码的(除非您自己编码),使用您的输出的客户端也无法确定。除非客户端知道字符串是如何编码的,否则它无法自信地显示它。作为一名 PHP 程序员,你的工作是告诉客户你的 HTML 输出是如何编码的。

您应该指定在Content-Type HTTP 头中使用的字符编码方案。这让客户端知道您的输出是如何编码的,从而知道如何正确显示它。

将内容类型作为一个meta标签放在 HTML 中稍微不太令人满意,因为除非客户端知道编码类型,否则它将无法读取 HTML 来确定编码。你这样做可以逃脱惩罚,但最好不要这样做。

在编码方案之间切换

mbstring扩展提供了许多功能,可以用来帮助检测编码方案并在编码方案之间进行转换。

mb_detect_encoding()函数将遍历可能的编码列表,并尝试确定字符串是如何编码的。

您可以使用mb_detect_order()功能或通过提供 CSV 或数组形式的编码列表来更改检测顺序。

您可以使用mb_convert_encoding()在编码格式之间转换字符串。

实际例子

这个例子展示了 PHP 中字符串行为的一些方面。它用三种不同的方式声明了一个数组,然后对每种方式运行一些字符串命令来说明一些要点。

<?php
$waysToSayHello = [
        'emoji' => "\u{1F44B}",
        'latinchars' => "Hello",
        'accentedChars' => "ça va?"
    ];
foreach ($waysToSayHello as $method => $string) {
    echo "$method : encoding [" . mb_detect_encoding($string, 'ISO-8859-1') . '] ' .
        'encoding [' . mb_detect_encoding($string, ['ASCII','UTF-8']) . '] ' .
        'strlen [' . strlen($string) . '] ' .
        'mb_strlen [' . mb_strlen($string) . '] ' .
        'first character[' . $string[0] . ']';
    echo "\r\n";
}
/*

emoji : encoding [ISO-8859-1] encoding [UTF-8] strlen [4] mb_strlen [1] first character![A456636_1_En_3_Figb_HTML.gif

latinchars : encoding [ISO-8859-1] encoding [ASCII] strlen [5] mb_strlen [5] first character[H]

accentedChars : encoding [ISO-8859-1] encoding [UTF-8] strlen [7] mb_strlen [6] first character![A456636_1_En_3_Figc_HTML.gif

*/

记住 PHP 不在字符串中存储编码信息,所以它只能猜测字符串是如何编码的。mb_detect_encoding函数将检查字符串并尝试确定它是什么。

它通过将字符串与编码方案列表进行比较,并选择第一个方案来对字符串进行有效编码。您可以指定编码(按顺序)让 PHP 尝试或依赖默认编码。这解释了为什么对于同一个字符串,mb_detect_encoding的输出是不同的——我们给了 PHP 不同的提示。

请注意,strlen()功能的输出与mb_strlen不同。PHP 函数strlen返回字符串中有多少字节,而不是多少字符。

最后,请注意,如果我们使用数组表示法来访问字符串中的某个位置,只有当字符串以单字节格式编码时,我们才能获得有意义的结果。

匹配字符串

当您试图匹配不同的变量类型时,在 PHP 中比较字符串应该以适当的谨慎程度进行。在第一章“造型变量”一节中,我们检查了与造型相关的手册页。确保您熟悉 PHP 如何将各种变量类型转换为字符串。

使用像><这样的比较操作符可能并不总是像预期的那样工作。通常预期 PHP 会使用字母顺序来计算这些操作符的字符串。

PHP 使用字符的 ASCII 值进行比较,而不是使用字母排序。小写字母比大写字母具有更高的 ASCII 值,因此您可能会遇到小写字母放在大写字母后面的情况,如下所示:

<?php
$a = "PHP";
$b = "developer";
if ($a > $b) {
    echo "$a > $b";
} else {
    echo "$a < $b";
}
// developer comes before PHP in the alphabet
// but this script outputs
// PHP < developer

回想一下“转换变量”一节中讨论的将字符串转换为整数的规则。在下面的例子中,字符串被转换为一个整数值12,它等于浮点值12.00,因此消息被回显。

<?php
$a = "12 o'clock";
$b = 12.00;
if ($a == $b) {
    echo "The mouse ran up the clock";
}

除非您对正在比较的字符串有把握,否则您应该考虑使用标识运算符===来进行这种比较。

除了使用操作符,PHP 还提供了许多字符串比较函数。

strcmp()是一个执行二进制安全字符串比较的函数。它以两个字符串作为参数,如果str1小于str2,则返回< 0;如果str1大于str2,则为> 0,如果它们相等,则为0

Tip

还记得宇宙飞船操作员吗?运算符可用于任何变量类型,但strcmp专用于字符串。

还有一个名为strcasecmp()的不区分大小写的版本,它首先将字符串转换成小写,然后进行比较。

此示例显示了不同之处:

<?php
$a = "PHP";
$b = "developer";
$comparison = strcmp($a, $b);
echo $comparison . PHP_EOL; // -20
$caseInsensitive = strcasecmp($a, $b);
echo $caseInsensitive . PHP_EOL; // 12

函数strncmp()strcasencmp()只能用来比较两个字符串的前“n”个字符。

PHP 有一个非常强大的函数叫做similar_text(),可以计算两个字符串之间的相似度。对于较长的文本,这可能是一个计算量非常大的过程,所以在使用之前要小心。还要注意你传递参数的顺序很重要,所以similar_text($a, $b) != similar_text($b, $a)

另一个函数levenshtein()可以用来计算两个字符串之间的 Levenshtein 距离。Levenshtein 距离被定义为将str1转换为str2所需要替换、插入或删除的最少字符数。

要比较子字符串,可以使用二进制安全的substr_compare()函数。

PHP 有两个函数可以让你处理字符串的发音。soundex()函数根据字符串的发音来计算音调。发音相同的琴弦会有相同的soundex键。

类似地,metaphone()函数为相似的发声字符串创建相同的键。它比soundex()更准确,因为它知道英语发音的基本规则。当然,这在其他语言中很可能帮助不大!

还有另外两种比较字符串的方法,但是会在关于安全性的第六章中讨论。hash_equals()函数是一种比较字符串的定时攻击安全方式,而password_verify()是一种检查密码是否与哈希匹配的安全方式。稍后您将更详细地了解它们,但请记住它们是字符串函数。

提取字符串

字符串中的单个位置可以用与数组元素相同的语法引用。字符串中的所有位置总是从零开始,即字符串中的第一个字符是位置 0。

<?php
$string = 'abcdef';
echo $string[0];    // a

您可以使用substr()函数返回字符串的一部分或片段。substr()的 PHP 手册显示了该命令的语法,如下所示:

`string substr ( string $string , int $start [, int $length ] )`

您可以看到它有两个强制参数和一个可选参数。起始参数和长度参数都可以是正数或负数。如果起始值大于字符串的长度,substr()将返回false。如果起始值为正(或 0),则返回的字符串片段从字符串的第start个位置开始,从开始算起。

否则,如果为负,切片从字符串末尾的第start个位置开始。

<?php
echo substr("abcdef", 2) . PHP_EOL;    // cdef
echo substr("abcdef", -2) . PHP_EOL;   // ef

如果省略 length,如前面的示例所示,那么切片将从切片起点继续到字符串的结尾。如果长度是正数,那么最多返回length个字符。如果长度是一个负数,那么在字符串的末尾会省略掉许多字符:

<?php
echo substr("abcdef", 0, 2) . PHP_EOL;    // ab
echo substr("abcdef", 0, -2) . PHP_EOL;   // abcd

如果长度给定并且是0FALSENULL,则返回一个空字符串。当开始参数大于或等于字符串时,也会发生同样的情况。

PHP 手册 3 给出了更多的例子:

<?php
echo substr('abcdef', 1);     // bcdef
echo substr('abcdef', 1, 3);  // bcd
echo substr('abcdef', 0, 4);  // abcd
echo substr('abcdef', 0, 8);  // abcdef
echo substr('abcdef', -1, 1); // f

搜索字符串

因为 PHP 是为 web 编写的,所以它在处理字符串方面特别强。您应该知道字符串操作函数的来龙去脉。本节介绍用于搜索字符串的函数。强烈建议您尝试这些函数,并阅读它们的手册页。Zend 考试非常适合奖励经验,而不是手册的百科知识。

有用的提示

对 PHP 的一个常见抱怨是,很难判断搜索字符串和数组时参数的顺序。

PHP 搜索参数有一个$haystack,我们正在搜索一个$needle。比较用于strpos()array_search()的参数顺序:

<?php
$arr = ['a', 'b', 'c', 'd', 'e', 'f' ];
$str = 'abcdef';
echo strpos($str, 'c') . PHP_EOL;
echo array_search('c', $arr) . PHP_EOL;

乍一看好像有时候是$needle参数先来,有时候是$haystack参数先来。

然而,当你记得 PHP 使用底层 C 库并且一致的规则是:

  • 对于字符串搜索功能,顺序始终是$haystack然后是$needle
  • 对于数组搜索功能,顺序总是$needle然后$haystack

下一个有用的提示是记住0false的区别。尽管布尔值 false 的计算结果为0,但是如果将其转换为整数,则数字0与布尔值false并不相同。这里有一个例子,我们似乎在字符串"abcdef"中找不到字母“a”:

<?php
$string = 'abcdef';
if (strpos($string, 'a') == false) {
  echo "False negative!" . PHP_EOL;
}

记住字符串是从零开始的,所以第一个位置是位置 0。strpos()正在返回整数 0,因为它在第一个位置找到了“a”。我们使用等式运算符==来检查strpos()的结果,因此我们错误地报告字母“a”没有出现在这个字符串中。

Tip

为了处理确实找不到子串的情况,应该使用 identity ===操作符。

搜索功能快速概述

PHP 有几个函数用来搜索字符串。一般来说,不区分大小写的函数在前缀后有一个“I”。下表列出了字符串搜索函数的 PHP 手册定义。

功能用于
substr_count()返回字符串中子字符串出现的次数。
strstr()在字符串中搜索子字符串,并返回干草堆中出现在第一个匹配项之后的部分。如果没有找到匹配项,则返回false。注意使用strpos()更好,因为它更快。
stristr()不区分大小写的版本strstr()
strchr()返回第一次出现针之前的字符串部分。
strpos()返回指针第一次出现的位置。 4
stripos()不区分大小写的版本strpos()
strspn()查找完全由给定掩码中包含的字符组成的字符串的起始段的长度。 5
strcspn()返回不包含掩码中任何字符的 subject 的初始段的长度。换句话说,它搜索字符串中任何掩码字母的第一个匹配项,并返回在找到它之前存在的字符数。 6

替换字符串

PHP 有三个替换字符串的函数。

str_replace()及其不区分大小写的版本str_ireplace()可用于基本替换。

<?php
echo str_replace('foo', 'bar', 'Delicious food'); // Delicious bard

它们都有三个强制参数——搜索字符串、替换字符串和要操作的字符串。如果您传递可选的第四个变量(它是一个引用参数),它将被设置为 PHP 执行的替换次数。

搜索和替换参数都可以是数组。这使您可以在一次调用中替换多个值,如下例所示:

<?php
$string = "I like black hot coffee";
$search = ['black', 'coffee'];
$replace = ['green', 'tea'];
echo str_replace($search, $replace, $string); // I like green hot tea

您可以使用substr_replace()函数来替换子字符串。substr_replace()用替换中给出的字符串替换由 start 和(可选)length 参数分隔的字符串副本。

strtr()是另一个替换子字符串和字符的函数。如果只提供了两个参数,第二个参数应该是替换对的数组。否则,它需要三个参数,如 PHP 手册中的示例所示,它用于将带重音符号的字符转换为英语格式的字符:

<?php
$address = "09479 Huopainenkylä, Pöhjois-Karjala";
$address = strtr($address, "äåö", "aao");
echo $address; // 09479 Huopainenkyloa, Pohjois-Karjala

替换字符串最灵活、最强大的方法是使用preg_match()函数,它允许您使用正则表达式来查找要替换的字符串片段。在本章后面的“字符串模式:正则表达式”一节中,你会学到更多关于正则表达式的知识。

格式化字符串

printf()函数用于输出格式化的字符串。你应该仔细阅读 PHP 手册 7 并确保你已经练习过使用它。一般的用法是指定一个格式化字符串和需要放入其中的值。

<?php
$minutes = 60;
$timeUnit = "an hour";
printf("There are %u minutes in %s.", $minutes, $timeUnit);

在这个例子中,您会注意到第一个参数printf()有两个用百分比符号标记的占位符。以下参数是必须进行类型转换并插入到这些占位符中的值。

有许多符号可用于格式化参数。你可以在 PHP 网站上找到这个列表, 8 但是为了方便起见,我把它包含在这里:

标志格式
%%文字百分比字符。不需要任何参数。
%b该参数被视为整数,并以二进制数表示。
%c该参数被视为一个整数,并表示为带有该 ASCII 值的字符。
%d该参数被视为整数,并表示为(有符号的)十进制数。
%e该参数被视为科学符号(例如,1.2e+2)。从 PHP 5.2.1 开始,精度说明符代表小数点后的位数。在早期版本中,它被视为有效位数(少一位)。
%E%e相似,但使用大写字母(例如 1.2E+2)。
%f该参数被视为一个浮点数,并表示为一个浮点数(支持区域设置)。
%F该参数被视为一个浮点数,并表示为一个浮点数(不区分语言环境)。从 PHP 4.3.10 和 PHP 5.0.3 开始可用。
%g%e%f中较短的一个。
%G%E%f中较短的一个。
%o该参数被视为一个整数,并表示为一个八进制数。
%s参数被视为并显示为字符串。
%u该参数被视为整数,并表示为无符号十进制数。
%x该参数被视为整数,并以十六进制数(小写字母)表示。
%X该参数被视为整数,并以十六进制数(大写字母)表示。

PHP 格式是区域敏感的,这影响了它们表示数字和日期的方式。例如,如果您将区域设置为荷兰语,那么日期将以荷兰语输出。PHP 手册上的一个例子显示了这一点:

<?php
// Set locale to Dutch
setlocale(LC_ALL, 'nl_NL');
// Output: vrijdag 22 december 1978
echo strftime("%A %e %B %Y", mktime(0, 0, 0, 12, 22, 1978));

Caution

区域设置信息是按进程维护的,而不是按线程。

如果您在多线程服务器 API 上运行 PHP,如 Windows 上的 IIS、HHVM 或 Apache,您可能会在脚本运行时遇到语言环境设置的突然变化,尽管脚本本身从未调用过setlocale()

这是由于其他脚本同时在同一个进程的不同线程中运行,使用setlocale()改变了整个进程的语言环境。

在 POSIX 系统上,您可以使用 shell 命令locale –a列出它支持的所有语言环境。在 Windows 机器上,MSDN 上有列出地区的页面,您可以在控制面板中查看这些页面。

格式化数字

number_format()函数是格式化数字的一种简单方法。

number_format()不支持区域设置,因此不会自动为您选择分隔符。默认情况下,千位分隔符是逗号,不显示小数位。

该函数的参数包括要格式化的数字、要显示的小数位数、小数点字符和千位分隔符。

您可以向该函数传递一个、两个或四个参数。这里有一个例子:

<?php
$number = 1234.5678;
// 1,235
echo number_format($number) . PHP_EOL;
// 1,234.568
echo number_format($number, 3) . PHP_EOL;
// 1.234,57
echo number_format($number, 2, ',', '.') . PHP_EOL;

要格式化货币,可以使用money_format()功能。它是区域感知的,并使用由主机系统设置的信息。

<?php
// Locale is British English
setlocale(LC_MONETARY, 'en_GB');
echo money_format('%.2n', "5000000.123");
// Locale is Denmark
setlocale(LC_MONETARY, 'da_DK');
echo money_format('%.2n', "5000000.123");

输出如下所示:

£5,000,000.12
kr 5.000.000,12

字符串模式:正则表达式

正则表达式是一组匹配字符串的规则。规则被写成一个字符串,使用一种描述您正在搜索的模式的格式。正则表达式有几种风格;PHP 使用 Perl 兼容的正则表达式(PCRE)。

学习正则表达式时,应该找一个自己喜欢的在线正则表达式测试器。有几个可供选择,它们使得处理表达式和查看它们如何匹配字符串变得更快。 9

定界符

正则表达式由出现在表达式中每个模式的开头和结尾的字符分隔。通常使用正斜杠,但#!也很常见。

可以使用任何字符,但是需要在表达式中对分隔符进行转义,因此标准做法是选择不太可能出现在搜索表达式中的分隔符。例如,如果您要搜索目录以找到匹配某个模式的目录,正斜杠字符可能不是分隔符的最佳选择。

元字符

元字符被解释为在搜索模式中有意义。如果您打算将它们作为表达式的文字部分,则需要对它们进行转义。下表列出了它们。

性格;角色;字母意义
\通用转义字符
^主题或行的开始
$主题或行尾
.匹配除换行符以外的任何字符
[开始定义一个角色类
]结束定义字符类
&#124;备用分支的开始(如“或”)
(子模式的开始
)子模式的结尾
?零或一个量词
*零个或多个量词
+一个或多个量词
{最小/最大数量化开始
}结束最小/最大量化

在本节中,我们将在此基础上继续学习,但现在只需注意这些符号在正则表达式或模式中传达了某种意义。你需要在参加考试前熟悉它们。

通用字符类型

Regex 为您提供了一种方式来指定搜索字符串中的字符可以是任何特定类型。您可以使用反斜杠(转义)元字符来指定它们,然后提供该类型的字母。

下表列出了 PCRE 中可用的字符类型。

标志字符类型
\d任何十进制数字
\h任何水平空白字符
\s任何空白字符
\v任何垂直空白字符
\w任何“单词”字符
\D任何不是十进制数字的字符
\H任何不是水平空格的字符
\S任何非空白字符
\V任何不是垂直空白字符的字符
\W任何“非单词”字符

您应该立即发现大写符号是小写符号的反码。

“单词”字符是任何字母、数字或下划线字符。其中包含的实际字符是区域设置敏感的。

边界

单词边界是字符串中当前字符和前一个字符都不匹配\wW的位置。

换句话说,它是字符串中一个单词开始或结束的位置,或者是其中一个字符匹配\w而另一个匹配W的位置。

标志分界线
\b单词边界
\B不是一个单词边界
\A主题的开始
\Z主题结尾或结尾换行
\z主题结束
\G主题中的第一个匹配位置

Tip

PHP 使用 PCRE 表达式。您可以在 http://www.pcre.org/original/doc/html/pcrepattern.html 的原始规范文档中找到此表。

字符类别

字符类是定义搜索字符串中可以匹配的字符集的非常灵活的方法。通过在模式中指定一个小的字符序列,您可以在搜索字符串中匹配一个大得多的字符集。

您在元字符表中看到,您通过将字符类放在方括号中来创建它。字符类的一个例子是[A-Z],它代表大写字母表中的所有字母。

您也可以在字符类中使用所有的通用类型,因此[A-Z\d]将匹配所有的大写字母和数字。

匹配不止一次

应用于字符串"abc123ABCabc"的表达式/[A-Z\d]/将匹配"1"字符。换句话说,它匹配搜索字符串中与表达式匹配的第一个字符。

如果您回头参考元字符表,您可以看到,+符号可以用来指定您想要一个或多个模式。所以对字符串"abc123ABCabc"应用的表达式/[A-Z\d]+/将匹配"123ABC"字符。 10

您可以使用大括号来限制匹配的数量。语法最好显示在一个表中,您可以将表达式与字符串"abc123ABCabc"进行匹配:

表示限制输出
/[A-Z\d]+/一个或无限公元前 123 年
[A-Z\d]{3}正好三个One hundred and twenty-three
[A-Z\d]{3,}三个或更多公元前 123 年
[A-Z\d]{3,5}三到五点之间123AB
[A-Z\d]{50}正好 50不匹配

捕获组

捕获组由括号描述,允许您对组应用限定符。它们还生成存储匹配值的编号组,并且可以在表达式的其他地方引用它们。

在本例中,我们围绕单词“cheeseburger”创建了一个捕获组,并使用该组来指定零个或一个匹配项。

<?php
$subject = "I can haz Cheeseburgers";
$pattern = "/I can haz (Cheeseburger)?/";
$matches = [];
preg_match($pattern, $subject, $matches);
var_dump($matches[0]);

这输出string(22) "I can haz Cheeseburger"。请注意,字符串末尾的“s”不匹配。

Tip

作为练习,在您最喜欢的编辑器中使用正则表达式,看看如果使用主题“I can haz”(字符串末尾没有空格)会发生什么。

您可以使用非捕获组来优化您的查询。当你不需要捕捉比赛时,你应该使用这些。

他们通过在您的组的开头放置一个?:标记来进行标记。前面的例子可以写成/I can haz (?:Cheeseburger)?/。注意,这个表达式仍然会像以前一样将字符串返回给 PHP,但是它不会将字符串Cheeseburger作为一个组存储,以供表达式引用。

令人困惑的是,?是一个量词,也表示一个非捕获组。请记住,量词不能出现在组的开头,因为没有什么可以量化的。

贪婪和懒惰

默认情况下,匹配是“贪婪”的,将匹配尽可能多的字符串。考虑一个你将要使用的例子。假设您想要匹配 HTML 标签,那么您可以尝试以下方法:

<?php
$subject = "Some <strong>html</strong> text";
$pattern = "/<.*>/";
$matches = [];
preg_match($pattern, $subject, $matches);
var_dump($matches[0]);  // string(21) "<strong>html</strong>"

这会输出string(21) "<strong>html</strong>",这显然比您想要的 HTML 标签要多。

这要归咎于贪婪;*量词是贪婪的,试图找到最长的可能匹配。它返回强标签的开始<和结束标签的最后>之间的字符,这是最长的可能匹配。

相比之下,惰性搜索返回最短的可能匹配。可以通过给量词加一个问号(?)来修改量词,让它变懒。

<?php
$subject = "Some <strong>html</strong> text";
$pattern = "/<.*?>/";  // note the pattern has changed
$matches = [];
preg_match($pattern, $subject, $matches);
var_dump($matches[0]);  // string(8) "<strong>"

修改量词还有很多选择,但是它们超出了本书的范围。

获取所有匹配项

到目前为止,您的表达式只返回搜索字符串匹配部分的第一个匹配项。假设您想要查找字符串中的所有匹配项。

PCRE 有一个全局修饰符(后面会详细介绍),但是 PHP 使用一个名为preg_match_all()的独立函数来返回所有匹配。

<?php
$subject = "Some <strong>html</strong> text";
$pattern = "/<.*?>/";
$matches = [];
preg_match_all($pattern, $subject, $matches);
var_dump($matches);

/*
 array(1) {
                [0] =>
                        array(2) {
                        [0] => string(8) "<strong>"
                        [1] => string(9) "</strong>"
                }
        }
*/

命名组

您可以通过将?<name>添加到打开组的括号的开头来命名捕捉组。例如:

<?php
$subject = "test@example.com";
$pattern = "/^(?<username>\w+)@(?<domain>\w+).(?<tld>\w+)/";
$matches = [];
if (preg_match($pattern, $subject, $matches)) {
  var_dump($matches);
}

在这个例子中,我们将匹配模式的第一部分命名为username,第二部分命名为domain,第三部分命名为tld。这是一个有点幼稚的例子,因为它不适用于像test@example.co.uk这样的电子邮件地址,但是它确实可以显示语法。前面的示例输出如下:

array(7) {
              [0] => string(16) "test@example.com"
              'username' => string(4) "test"
              [1] => string(4) "test"
              'domain' => string(7) "example"
              [2] => string(7) "example"
              'tld' => string(3) "com"
              [3] => string(3) "com"
      }

所以您能够引用$matches['username']并接收"test"作为响应,这很方便。

图案修改器

您可以在表达式的结束分隔符后添加修饰符。下表列出了修饰符。

修饰语功能
i该表达式不区分大小写。
m多线模式。字符串可以跨多行,换行符被忽略。^$符号将匹配行的开始和结束,而不是匹配字符串的开始和结束。
s.元字符也将匹配换行符。
x忽略空白,除非你转义它。
e这会导致 PHP 代码被评估,这是非常不鼓励的。从 PHP 5.5 开始不推荐使用它,在 PHP 7 中将生成警告,因为它不再被支持。
U这使得量词在默认情况下是懒惰的,在它们后面使用?反而会将它们标记为贪婪的。
u这告诉 PHP 将模式和字符串视为 UTF-8 编码。这意味着匹配的是字符而不是字节。

Chapter 3 Quiz

Q1:你不能使用大于或小于运算符来比较字符串变量和整数变量。只能用等价运算符比较字符串和整数值。

| 真实的 |
| 错误的 |

Q2:你可以使用 ________ 函数在字符串之间进行二进制安全的不区分大小写的比较。

| <=> |
| strcmp |
| strcasecmp |
| stricmp |

Q3:搜索字符串的 PHP 函数总是有按什么顺序排列的参数。

| $haystack, $needle |
| $needle, $haystack |
| 这取决于功能 |

Q4:strspn($subject, $mask)函数是做什么的?

| 在字符串$subject中搜索子字符串$mask |
| 返回$subject中字符串的最大长度,该字符串只包含$mask中的字母 |
| 返回包含$mask中所有字母的$subject中字符串的最小长度 |
| 这是一种从$subject字符串中拼接出由$mask指定的字符串的二进制安全方式 |

q5:strstr($haystack, $needle)函数是做什么的?

| 这是比strpos()更快的选择 |
| 这是对strpos()的二进制安全替代 |
| 它返回出现在第一个$needle实例之后的那部分$haystack |
| 它返回字符串$needle第一次出现在$haystack中的位置 |

Q6:这段代码的输出是什么?

| 0 |
| Cats do nothing but sleep |
| Cats da nathint but sleep |
| 这会产生一个错误 |

<?php
$fact = "Dogs do nothing but sleep";
$fact = strtr($fact, "Dog", "Cat");
echo $fact;

问题 7:在下面的文本中,哪一个正则表达式将识别两个电子邮件地址(并且只识别电子邮件地址)?选择尽可能多的适用项。

“打翻圣诞树盯着 kittens@catsaregreat.com 墙,玩食物被灰尘弄糊涂或者今天要去抓红点今天要去抓红点。”。

| [a-z]。[a-z.]+ |
| \b[a-z]+@[a-z]+com\b |
| \b[a-z]+@[a-z.]+\b |
| (\b[a-z]
@\b)([a-zA-Z\d]+) |
| (\S*)@(\w*)。(\S*) |

Q8:这段代码的输出是什么?

| abcdefgh12345678 |
| 没有任何东西 |
| 一个警告 |
| 致命的错误 |

<?php
echo substr("abcdefgh12345678");

问题 9:如果运行这段代码,如何检索第一个电子邮件地址?

| echo $matches[0]; |
| echo $matches[0] . $matches[1] . $matches[2] |
| 你不能;有一个语法错误 |
| 不能;这不会匹配字符串中的任何内容 |
| 不能;这将产生一个错误,因为模式无效 |

<?php
$subject = "purr for no reason or eat prawns daintily with a claw then lick paws mycat@catsaregreat.com clean wash down prawns with a lap of carnation milk then retire to the warmest spot on the couch to claw";
$pattern = "#(\S*)@(\w*).(\S*)#";
$matches = [];
preg_match($pattern, $subject, $matches);
// how do I echo the full email address?

Q10:preg_replace_callback()函数用来做以下哪一项?

| 使用回调函数来提供替换字符串,而不是静态字符串 |
| 使用返回匹配列表的回调来替换 |
| 指定一个函数,在preg_replace()完成运行后调用 |
| 没有这个功能 |

Footnotes 1

https://php.net/manual/en/regexp.reference.escape.php

2

https://en.wikipedia.org/wiki/UTF-8

3

https://php.net/manual/en/function.substr.php

4

https://secure.php.net/manual/en/function.strpos.php

5

https://secure.php.net/manual/en/function.strspn.php

6

https://secure.php.net/manual/en/function.strcspn.php

7

https://php.net/manual/en/function.printf.php

8

https://secure.php.net/manual/en/function.sprintf.php

9

例如,网站 https://regex101.com/ 是一个玩正则表达式的好地方。

10

https://regex101.com/r/EXsPkY/2

四、数组

在这一章中,我们将会看到 PHP 数组。PHP 数组被实现为一个有序的映射,将值与键关联起来。PHP 中有三种类型的数组:索引数组、关联数组和多维数组。

PHP 有许多数组函数,涵盖了函数的许多常见用法。在编写函数来操作数组之前,应该先检查是否已经有一个函数。它们是用 C 实现的,所以要达到同样的效果,它们比用 PHP 编写的任何函数都要快得多。阵列手册页 1 在一个地方列出了它们,你要确保你学习了这个页面和每个函数的手册页。

这本书太长了,无法详尽地列出每一个功能。本章不是重复这些信息,而是集中在对这些函数的分组和解释上。

声明和引用数组

我们不会详细讨论什么是数组,而是直接讨论 PHP 中用于声明数组的语法。

数组被创建为一组用逗号分隔的值对。

<?php
// numeric index, auto assigned key
$arr = array(10, 'abc', 30);
// numeric index, key explicitly set
$arr = array(0 => 10, 1 => 'abc', 2 => 30 );
// associative
$arr = array('name' => 'foo', 'age' => 20);
// short syntax
$arr = ['name' => 'foo', 'age' => 20];

如果你没有指定一个键,那么 PHP 将分配一个自动递增的数字键。在这个例子中,前两个赋值是相同的,因为 PHP 会自动分配这个键。

密钥可以是数字或字符串。数组可能包含数字和字符串键的混合。

以数字为关键字的数组称为枚举型。前两个例子是列举性的。包含关键字字符串的数组称为关联数组。最后两个例子是关联数组。

声明数组有两种语法形式;选一个是编码风格的问题。

<?php
$shortForm = ['this', 'is', 'short'];
$longForm = array('this', 'is', 'short');

数组可以是嵌套的。换句话说,数组值本身可以是一个数组。这些被称为多维数组。

可以使用[]操作符引用单个数组元素,如下所示:

<?php
$arr = ['name' => 'foo', 'age' => 20];
echo $arr['age']; // 20

如果您没有在括号中指定一个键,PHP 会认为您正在尝试引用一个新元素。您可以使用它将元素添加到数组的末尾:

<?php
$arr = [0 => 'id', 'name' => 'foo', 'age' => 20];
$arr[] = 'example';
print_r($arr);

这将输出以下内容:

Array
(
[0] => id
[name] => foo
[age] => 20
[1] => example
)

注意,PHP 通过递增数组中最高的数字键来选择键。

创建数组的函数

有很多 PHP 函数返回一个数组,但是我将介绍几个与数组直接相关的函数。

函数explode()用于将一个字符串拆分成一个数组。举个例子解释最简单:

<?php
// The delimiter is a string of any length
$delimiter = ',';
// This string is broken up by the delimiter
$source = '1, abc, 2, def, 3, ghi';
// The limit determines how many elements explode will return
$limit = -2;
// create an array by splitting the source
$arr = explode($delimiter, $source, $limit);
print_r($arr);

该函数有三个参数。第一个是用作分隔符的字符串。通常,这只是单个字符(就像使用 CSV 时的逗号),但它可以是任意长度。

第二个参数是一个字符串,包含由分隔符分隔的元素列表。

第三个参数限制 PHP 将返回的项数。默认情况下,它被设置为PHP_INT_MAX,因此 PHP 将返回尽可能多的条目。如果是负数,PHP 将返回除最后一个$limit金额之外的所有元素。零限值被视为与 1 相同。

这个例子指定-2作为限制,所以 PHP 返回除最后两个元素之外的所有元素。

该示例的输出是:

Array
(
    [0] => 1
    [1] =>  abc
    [2] =>  2
    [3] =>  def
)

implode()功能 2 以相反的方式操作。它将数组中的元素连接到一个字符串中,该字符串由您提供的字符串分隔。

preg_split()是另一个将字符串拆分成数组的函数。它类似于explode(),但是它使用正则表达式来分隔字段,而不是使用文字字符串。它被记录在 PHP 手册中。

你可以使用str_split()函数 4 将一个字符串分解成一个组块数组。它有两个参数:要分割的字符串,以及用于数组中每个元素的块的长度。

<?php
$input = '12345678';
$arr = str_split($input, 3);
print_r($arr);

此示例将字符串分解为包含长度为 3 的元素的数组,如下所示:

Array
(
    [0] => 123
    [1] => 456
    [2] => 78
)

注意,字符串不能被块大小整除,所以最后一个元素只有两个字符长。如果块大小大于字符串的长度,则整个字符串作为数组的唯一元素返回。如果您尝试使用负的块长度,该函数将返回FALSE

数组运算符

PHP 数组可以测试等价性和同一性。我们在比较运算符一节中看到,如果数组具有相同的键和值对,那么它们就是等价的。如果它们具有相同的键和值对,顺序相同,并且键-值类型相同,则它们是相同的。

+运算符将产生两个数组的并集。

当使用+ union 操作符时,PHP 将操作符右边的数组附加到左边。如果一个键同时存在于两个数组中,那么左边的数组值将用于该键。

<?php
$a = ['a' => 'hello', 'b' => 'world'];
$b = ['a' => 'goodbye', 'c' => 'cruel'];
echo implode(' ', $a + $b);  // hello world cruel

在前面的例子中,两个数组都有键a。因此,数组的并集将具有这个键的来自$a的值,因为$a在并集操作符的左边。

例子名字结果
$a + $b联盟$b被追加到$a之后。如果一个键存在于两个数组中,那么来自$a的值被放入联合中。
$a == $b平等TRUE如果$a$b有相同的键值对
$a === $b身份TRUE如果$a$b有相同的键值对,相同的类型,相同的顺序。
$a != $b不平等TRUE如果$a不等于$b
$a <> $b不平等TRUE如果$a不等于$b
$a !== $b非同一性TRUE如果$a$b不相同。

让我们快速看一个例子:

<?php
$a = ['a', 'b', '1'];
$b = ['a', 'b', 1];
$c = ['1', 'b', 'a'];
$d = [2 => 1, 0 => 'a', 1 => 'b'];

var_dump($a == $b);     // true
var_dump($a === $b);    // false
var_dump($a == $c);     // false
var_dump($a == $d);     // true
var_dump($a === $d);    // false

我们可以看到$a等于$b,因为键值对是相同的。然而,它们并不等价,因为第三个元素的类型在$a中是一个字符串,在$b中是一个整数。

$a$c不相等,即使它们具有相同的值。如果数组具有相同的键值对,则认为它们是相等的。在这种情况下,我们没有指定一个键,所以 PHP 为每个值分配了一个自动递增的键。因此,即使值相同,它们的键值对也不匹配。

$a$d相等是因为键-值对相同,但不相同是因为它们不在同一顺序。

PHP 数组键的适当联系

PHP 数组是从零开始的。

PHP 数组键区分大小写:$arr['A']$arr['a']是不同的元素。

密钥只能是字符串或整数。其他变量类型在存储之前被转换为这些类型之一。

包含十进制有效整数的字符串将被转换为整数类型。

<?php
$a = [
"2" =>"hello",
    0x03 =>"world",
    0b100 => ' this is ',
"04" =>"PHP",
    8.7 =>"!!!!"
];
var_dump($a);
/*
array(5) {
  [2]=>
  string(5) "hello"
  [3]=>
  string(5) "world"
  [4]=>
  string(9) " this is "
  ["04"]=>
  string(3) "PHP"
  [8]=>
  string(4) "!!!!"
}
*/

在前面的例子中,我们看到字符串"2"被转换为整数 2。十六进制和二进制格式都转换成十进制。字符串"04"不会被转换为整数,因为它包含八进制表示,而不是十进制表示。

PHP 在将浮点数转换为整数时,会将浮点数舍入为零。另一种说法是,数字的小数部分被截断。例如,float 133.7 将转换为整数值 133(而不是向上舍入到 134)。

布尔值也可以转换为整数。布尔值true评估为整数 1,而false变为整数 0。

Null 被视为空字符串。所以空键将存储在键''下。

复合变量(对象和数组)和资源不能用作键。如果你试图这样做,PHP 会发出一个警告"illegal offset type"

键是唯一的;如果一个数组中的多个元素使用同一个键(如上转换后),那么 PHP 将使用最后一个键作为值,并覆盖所有前面的值。

Tip

这是一个很好的时间来回顾你的杂耍类型!

填充数组

您可以使用range()函数根据您指定的值范围将值添加到数组中。您可以指定范围的开始、结束和步长。

PHP 手册中有许多有用的例子,但这里有一个是基于其中一条评论的:

<?php
print_r(array_combine(range(1, 10, 2),range(1,5)));

这将输出以下内容:

/*
  Array
  (
    [1] => 1
    [3] => 2
    [5] => 3
    [7] => 4
    [9] => 5
  )
*/

另一个名为array_fill()的命令可以让你用一个值填充一个数组。它接受起始索引、要填充多少值以及要插入的值的参数。

<?php
print_r(array_fill(10, 5, 'five'));

该脚本输出:

Array
(
[10] => five
[11] => five
[12] => five
[13] => five
[14] => five
)

与此相关的是函数array_fill_keys()。这个函数将用一个特定的值填充一个数组,并让你指定使用什么键。

<?php
$keys = range(1, 10, 2);
$value = "PHP";
print_r(array_fill_keys($keys, $value));
/*
Array
(
    [1] => PHP
    [3] => PHP
    [5] => PHP
    [7] => PHP
    [9] => PHP
)
*/

推动、弹出、换档和取消换档(天啊!)

这四个命令用于在数组中添加或删除元素。

功能影响
array_shift()将一个元素从数组 5 的开头移开
array_unshift()将一个或多个元素添加到数组的开头 6
array_pop()从数组 7 的末尾弹出该元素
array_push()将一个或多个元素推到数组 8 的末尾

您可能会注意到,您可以用这些函数轻松地实现队列和堆栈。

从数组中删除元素的命令会将元素返回给您,并将所有元素下移。数字键被减少,直到它们从 0 开始计数,而文字键保持不变。

<?php
$stack = array("one", "two", "three", "four");
$fruit = array_shift($stack);
print_r($stack);

在输出中,您会注意到"two"现在的键是 0,而之前的键是 1:

/*
  Array
  (
    [0] => two
    [1] => three
    [2] => four
  )
*/

比较数组

你在本章的前面已经看到,可以使用等式运算符==和等式运算符===来比较数组。当应用于数组时,如果数组具有相同的键和值,则相等运算符返回true,而不管它们是什么类型。如果数组具有相同的键和值,相同的顺序,并且相同的变量类型,那么 identity 操作符将只返回true

<?php
$arr = ['1', '2', '3'];
$brr = [1, 2, 3];
var_dump($arr === $brr); // false
var_dump($arr == $brr);  // true

有专门用于数组比较的 PHP 函数,使得更复杂的比较成为可能。

array_diff()

array_diff()函数接受一组数组作为参数。它将返回一个数组,其中包含第一个数组中不存在于任何其他数组中的值。

这个例子使用array_diff()$_POST超全局中提供的输入参数与预定义的必需参数列表进行比较。

<?php
$requiredKeys = ['username', 'password', 'csrf_token'];
$missingKeys = array_diff($requiredKeys, array_keys($_POST));
if (count($missingKeys)) {
    throw new UnexpectedValueException('You need to provide [' . print_r($missingKeys, true) . ']');
}

这段代码查找所有在所需列表中但不在 post 数组中的键,并创建一个名为$missingKeys的数组来包含它们。这使您可以验证表单是否已完全填写。

array_diff_assoc()array_diff()的关联版本,考虑了数组键及其值。为了看出区别,我们可以用一个非常简单的例子:

<?php
$a = ['a' => 'apple', 'b' => 'banana'];
$b = ['a' => 'apple', 'd' => 'banana'];
print_r(array_diff($a, $b));
print_r(array_diff_assoc($a, $b));
/*
Array
(
)
Array
(
    [b] => banana
)
*/

array_diff()的结果是一个空数组,但是array_diff_assoc()返回一个由[b] => banana组成的数组,因为值banana的键是第一个数组中的b和第二个数组中的d

array_intersect()

函数array_intersect()也将数组列表作为参数。它计算第一个数组中的哪些值也存在于所有其他数组中。

<?php
$birds = ['duck', 'chicken', 'goose'];
$net = ['dog', 'cat', 'chicken', 'goose', 'hamster'];
print_r(array_intersect($net, $birds));

这将输出$net$birds中的元素:

Array
(
  [2] => chicken
  [3] => goose
)

请注意,键被保留。

array_intersect_assoc()包含匹配元素时的索引检查。如果将它应用于示例中的数组,它将返回一个空数组。返回值为空,因为尽管数组中的值匹配,但它们的索引不匹配。

用户定义的匹配函数

PHP 提供了允许您指定自己的比较函数的函数。

array_udiff()为例。它接受一个数组参数列表,后跟一个可调用的作为最后一个参数。

让我们考虑一个简单的例子,我们想要比较数组的小写值。更现实的用例可能涉及更复杂的操作,比如对对象的操作。

<?php
$birds = ['duck', 'chicken', 'goose'];
$net = ['Dog', 'Cat', 'Chicken', 'Goose', 'Hamster'];
$diff = array_udiff($net, $birds, function($a, $b){
    $a = strtolower($a);
    $b = strtolower($b);
    if ($a < $b) {
        return -1;
    } elseif ($a > $b) {
        return 1;
    } else {
        return 0;
    }
});
print_r($diff);

这段代码输出$net中的元素,这些元素在$birds的列表中没有匹配的动物。我们使用一个自定义函数来进行比较,首先将两个字符串都转换成小写。

Array
(
    [0] => Dog
    [1] => Cat
    [4] => Hamster
)

请注意以下几点:

  • 来自手册 9 :“如果第一个参数被认为分别小于、等于或大于第二个参数,比较函数必须返回小于、等于或大于零的整数。”
  • 对于任何将可调用对象作为参数的函数,都可以使用闭包作为可调用对象。
  • 您可以将 lambdas 用作可调用函数,也可以用于任何将可调用函数作为参数的函数。在这个例子中,我们使用了λ。
  • 比较函数采用两个参数作为要比较的值。

有一些 PHP 函数允许你指定自己的可调用函数来比较键、值或两者。

比较函数的快速列表

下表显示了用于执行不同功能的阵列。

有类似的功能来执行交集。它们有相同的命名约定和参数,所以我不在这里列出它们。

功能用于
array_diff计算数组的差 10
array_diff_assoc使用附加索引检查计算数组的差异
array_udiff使用数据比较回调函数 11 计算数组的差
array_udiff_assoc通过附加索引检查计算数组的差异,并通过回调函数 12 比较数据
array_udiff_uassoc使用附加索引检查计算数组的差异,并通过回调函数比较数据和索引

请注意,array_udiff_uassoc()将两个可调用函数作为参数,一个用于值,最后一个参数用于索引。查看手册页 13 确保你已经学习了它所有的相关功能。

组合数组

PHP 提供了一些有用的函数来帮助组合数组。

combine_array($keys, $values)函数通过使用一个键数组和另一个值数组来创建一个数组。如果数组中的元素数量不匹配,它将返回FALSE,否则将返回一个关联数组。

您可以使用array_replace($array1, $array2, ...)将一个数组中的值顺序替换为其他数组中的值。它接受两个或多个数组作为参数,并从左到右处理它们。

它遵循以下规则来确定最终结果:

  • 如果第一个数组有一个键不在第二个数组中,那么键-值对保持不变。
  • 如果第二个数组有一个不在第一个数组中的键,则将第二个数组中的键-值对插入到第一个数组中。
  • 如果第二个值有一个键也在第一个数组中,那么第二个数组中的值将替换第一个数组中的值。

让我们来看一个using array_replace()的例子:

<?php
$input = ['a', 'b', 'c'];
$replace = [3 => 'd', '1' => 'q'];
$replaceTwo = [2 => 1, 1.3 => 'Z'];
$output = array_replace($input, $replace, $replaceTwo);
echo implode(", ", $output); // a, Z, 1, d

我已经将这些信息放在一个表格中,这样您就可以看到这些规则是如何应用的。该函数从左到右工作,用前一个数组替换每个后续参数。

钥匙$输入$replace$replaceTwo$输出
Zeroaa
onebqZZ
Twoconeone
threedd

Note

字符串键1被转换为整数,浮点键1.3也被转换为整数。两者的值都为 1,因此将替换该位置的值。

函数将合并一个或多个数组。人们可能期望它在合并时遵循与+操作符相同的规则,但是在某些情况下它的行为完全不同。考虑这个例子:

<?php
$arrOne = [
  // integer
  0 => 'One 0',
  // string
  'a' => 'One a',
  // non-empty in One, but empty in Two
  'Overwrite' => 'Not empty',
];

$arrTwo = [
  0 => 'Two 0',
  1 => 'Two 1',
  'b' => 'Two b',
  'Overwrite' => '',
];

print_r($arrOne + $arrTwo);

print_r(array_merge($arrOne, $arrTwo));

一会儿我将向您展示这段代码的输出。在代码输出中,您应该注意两件事:

  • array_merge()函数重新索引数字键,但是操作符没有。
  • array_merge()函数不会用空值覆盖非空值,但操作符会。

正如所承诺的,下面是显示差异的输出:

Array
(
    [0] => One 0
    [a] => One a
    [1] => Two 1
    [b] => Two b
)
Array
(
    [0] => One 0
    [a] => One a
    [1] => Two 0
    [2] => Two 1
    [b] => Two b
)

拆分数组

有几个函数可以用来拆分数组。下表列出了它们。我们将在书中详细讨论一些,但是你应该确保你也阅读了手册。

功能习惯
array_chunk将数组分割成块。 14
array_column从输入数组中返回单个列,例如,数据库查询结果的数组。
array_slice提取数组中的一个数组。
array_splice返回数组的一部分,并用原始数组中的其他内容替换它(参数通过引用调用)。 十五
extract创建以数组的键命名的变量,这些变量包含数组中的值。使用这个函数会导致代码混乱,因为不清楚变量是在哪里定义的。
array_rand选择一个数组中的随机键。

在这些函数中,唯一可能比较棘手的是array_splice()。它不仅返回值(提取的切片),而且因为输入数组是通过引用传递的,所以它还会影响您调用它的数组。

更复杂的是,您可以选择用替换数组替换从输入数组中提取的切片。

让我们看一个例子:

<?php
$input = [1,2,3];
$replacement = ['hello', 'world'];
// $slice contains the piece we extract
$slice = array_splice($input, 1, 1, $replacement);
// $input is passed by reference and so is amended
print_r($input);

这个脚本查找输入数组中从位置 1 开始、长度为 1 的部分。我们知道数组是从零开始的,所以位置 1 的值是2array_splice()函数返回找到的棋子的数组。因此,$slice将是一个包含值2的单个元素的数组。

array_splice()的输入数组参数通过引用传递,因此将被函数修改。我们用替换数组替换提取的切片。

因此,该脚本的输出是:

Array
(
    [0] => 1
    [1] => hello
    [2] => world
    [3] => 3
)

解构数组

list()语言构造用于根据变量的索引给数组中的变量赋值。下面是其用法的一个基本示例:

<?php
$array = ['one', 'two', 'three'];
list($a, $b, $c) = $array;
echo $a; // one
echo $b; // two
echo $c; //three

PHP 7 为list()引入了一个语法变化,使得它在创建索引数组时表现得更加一致。在 PHP 7 中,变量是按照你写的顺序赋值的,而在 PHP 5 中,它们是按照相反的顺序赋值的。如果你看到一个例子,那就更有意义了:

<?php
$array = ['one', 'two', 'three'];
list($indexedArray[0], $indexedArray[1], $indexedArray[2]) = $array;
var_dump($indexedArray);

在 PHP 7 中,这将输出:

array(3) {
  [0]=>
  string(3) "one"
  [1]=>
  string(3) "two"
  [2]=>
  string(5) "three"
}

在 PHP 5 中,顺序相反,输出如下:

array(3) {
  [2]=>
  string(5) "three"
  [1]=>
  string(3) "two"
  [0]=>
  string(3) "one"
}

使用数组计算

PHP 提供了几个方便的函数,让您可以对数组执行数学计算,而不需要手动遍历它们。

功能返回
array_count_values数组中每个唯一值出现的次数
array_product数组中所有值的乘积
array_sum数组中所有值的总和
count数组中有多少个元素
sizeof这是count()的别名

Note

空数组的乘积是 1,而不是 0。 16

遍历数组

有两种方法可以循环访问数组,一种是使用游标,另一种是循环访问数组。

遍历数组

一个枚举 PHP 数组可以通过增加一个索引计数器来循环,但是这对关联数组不起作用。更好、更健壮的方法是使用foreach()构造。

它可以让你快速查看foreach()使用的两种可能的语法,然后继续。如果你正在考虑参加考试,你应该已经熟悉了它的用法,所以这是为了其他语言的程序员的利益。

<?php
$arr = [
    'a' => 'apple',
    'b' => 'banana',
    'c' => 'cherry'
];
foreach($arr as $value) {
    echo $value . PHP_EOL;
}
foreach($arr as $key => $value) {
    echo $key . ' = ' . $value . PHP_EOL;
}

第一个foreach()循环将遍历数组并将数组值传递给代码块。第二个foreach()循环遍历它并传递键和值。

默认情况下,PHP 将值传递给一个foreach()循环的代码块。如果您更改代码块中的值,它不会对代码块之外产生影响。但是,您可以通过在值前面加一个&符号来标记要通过引用传递的值。

Caution

通常人们会对你在foreach()循环中使用引用表示不满。

我们将在下面的代码示例中看到这一点,该示例还演示了在foreach块中声明的变量在包含范围中被定义。循环结束后,它将保存循环中的最后一个值。但是,依赖这个特性会使代码更难阅读。

<?php
$arr = [1,2,3];
foreach ($arr as $value) {
    $value += 1;
}
echo implode(', ', $arr) . PHP_EOL;   // 1, 2, 3
echo $value . PHP_EOL;                // 4
foreach ($arr as &$value) {
  $value += 1;
}
echo implode(', ', $arr) . PHP_EOL;   // 2, 3, 4
echo $value;

从 PHP 5.5 开始,list()构造可以用在foreach()循环中来解包嵌套数组。这在处理数据库结果时特别有用。

下面是一个使用列表的示例:

<?php
// assigning to scalars
list($animal, $food, $mood) = ['cat', 'cheeseburgers', 'grumpy'];
echo "{$animal}s eat $food except when they're $mood." . PHP_EOL;

// assigning to an array
$info = [];
list($info[0], $info[1], $info[2]) = ['cat', 'cheeseburgers', 'grumpy'];
var_dump($info);

/*
cats eat cheeseburgers except when they're grumpy.
array(3) {
  [0]=>
  string(3) "cat"
  [1]=>
  string(13) "cheeseburgers"
  [2]=>
  string(6) "grumpy"
}
*/

Note

关键字each也可以用于循环数组,但在 PHP 7.2.0 中不推荐使用(所以在 PHP 7.1 中也不要使用它)

使用数组游标

每个数组都有一个指向当前元素的光标或指针。许多 PHP 函数使用光标来决定对哪个元素进行操作。

以下是基本的光标功能:

功能表演
reset将光标移动到数组的开头 17
end将光标移动到数组的末尾
next推进光标 18
prev向前移动光标
current返回光标指向的元素的值
key返回光标指向的元素的键

可以使用相同的语法迭代对象,但是知道它们实现接口迭代器是很重要的。

游标的一个不太常见的用法如下:

<?php
$arr = [
  'a' => 'apple',
  'b' => 'banana',
  'c' => 'cherry'
];
while (list($var, $val) = each($arr)) {
  echo "$var is $val" . PHP_EOL;
}

list()是一种语言结构,从提供的数组中分配变量。each()函数从数组中返回当前的键和值对,并移动数组光标。

遍历数组

array_ walk()函数将一个用户可调用的函数应用于数组中的每个元素。它有两个参数——对数组的引用和可调用的。

这个可调用函数将被传递两个参数。第一个是数组中元素的值,第二个是它的索引。

一些内部函数,例如strtolower()如果接收到太多参数会抛出警告,因此不适合作为array_walk()的回调。

Note

如果需要回调函数来改变数组的值,应该确保通过引用传递第一个参数。

下面是一个将数组中的所有元素转换为大写的示例:

<?php
$arr = [
  'a' => 'apple',
  'b' => 'banana',
  'c' => 'cherry'
];
array_walk($arr, function(&$value, $key) {
  $value = strtoupper($value);
});
print_r($arr);

注意,我通过引用将值传递到我的 lambda 函数中,因此在 lambda 中更改它将影响到$arr变量。

如果我们使用strtoupper()作为回调,PHP 会产生警告。作为一个练习,试着找出为什么会这样。

排序数组

PHP 提供了几个排序函数。

它们遵循一种命名惯例,即基本的sort函数以r为前缀表示反向,以a为前缀表示关联。

所有排序函数都将数组引用作为其参数,并返回一个指示成功或失败的布尔值。

功能用于
sort按字母顺序排列数组
rsort反向字母排序
asort关联排序
arsort反向关联排序
ksort键短
krsort反向键排序
usort用于排序的用户定义的比较函数
shuffle伪随机排序

关联排序将按值排序,并维护索引关联。请看他们其中一个手册页 19 的例子。

所有函数(除了usort())都接受一个可选参数来指示sort标志。这些标志是预定义的常量:

意义
SORT_REGULAR正常比较项目;不要换类型。
SORT_NUMERIC将项目转换为数值,然后进行比较。
SORT_STRING将项转换为字符串,然后进行比较。
SORT_LOCALE_STRING使用区域设置将项转换为字符串。
SORT NATURAL使用自然顺序排序,像函数natsort()
SORT_FLAG_CASE可以与SORT_STRINGSORT_NATURAL结合使用,对字符串进行不区分大小写的排序。

自然顺序排序

自然排序是对人类有意义的排序顺序。这是一种字母排序顺序,但多个数字被视为一个字符。

功能natsort()不带标志,与设置了SORT_NATURAL标志的sort()相同。

作为一个例子,让我们从一个在人眼看来已经排序的字符串开始,对它进行洗牌,然后使用两种排序形式来看看它是如何排序的:

<?php
$a = $b = explode(' ', 'a1 a2 a10 a11 a12 a20 a21');
shuffle($a);
shuffle($b);
natsort($a);
sort($b);
print_r($a);
print_r($b);

注意,我使用了explode函数将一个字符串分解成一个数组。

这将输出:

Array
(
    [5] => a1
    [2] => a2
    [0] => a10
    [4] => a11
    [6] => a12
    [3] => a20
    [1] => a21
)
Array
(
    [0] => a1
    [1] => a10
    [2] => a11
    [3] => a12
    [4] => a2
    [5] => a20
    [6] => a21
)

标准 PHP 库(SPL) : ArrayObject 类

SPL 库包括ArrayObject类,允许你从数组中创建对象。这些对象可以使用手册页上列出的ArrayObject类的方法。

这让你可以把数组当作对象来处理,就像 PHP 手册 20 中的例子一样:

<?php
$fruits = array("d" =>"lemon", "a" =>"orange", "b" =>"banana", "c" =>"apple");
$fruitArrayObject = new ArrayObject($fruits);
$fruitArrayObject->ksort();
foreach ($fruitArrayObject as $key => $val) {
  echo "$key = $val\n";
}

当构造一个ArrayObject时,您传递一个输入,它可以是一个数组或者一个对象。

您还可以选择指定标志:

影响
ArrayObject::STD_PROP_LIST当作为列表(var_dumpforeach等)访问时,对象的属性具有其正常功能。).
ArrayObject::ARRAY_AS_PROPS条目可以作为属性来访问(readwrite)。 21

这些标志可以用setFlags()方法设置,如手册中的例子所示:

<?php
// Array of available fruits
$fruits = array("lemons" => 1, "oranges" => 4, "bananas" => 5, "apples" => 10);

$fruitsArrayObject = new ArrayObject($fruits);

// Try to use array key as property
var_dump($fruitsArrayObject->lemons);
// Set the flag so that the array keys can be used as properties of the ArrayObject
$fruitsArrayObject->setFlags(ArrayObject::ARRAY_AS_PROPS);
// Try it again
var_dump($fruitsArrayObject->lemons);

此示例将输出:

        NULL
        int(1)

Chapter 4 Quiz

Q1:PHP 键区分大小写吗?这个脚本的输出会是什么?

| 这会产生一个错误 |
| 2 |
| 4 |
| 以上都不是 |

<?php
$arr1 = ["A" => "apple", "B" => "banana"];
$arr2 = ["a" => "aardvark", "b" => "baboon"];
echo count($arr1 + $arr2);

Q2:这个脚本会输出什么?

| Found |
| Nothing |
| Warning: in_array() expects parameter 2 to be array |
| 以上都不是 |

<?php
$arr = [
  'a' => 'apple',
  'b' => 'banana',
  'c' => 'cherry'
];
$keys = array_keys($arr);
if (in_array($keys, 'a')) {
  echo "Found";
}

Q3:这个脚本会输出什么?

| 0 |
| 1 |
| 2 |
| 3 |
| 以上都不是 |

<?php
$birds = ['duck', 'chicken', 'goose'];
$net = ['dog', 'cat', 'chicken', 'goose', 'hamster'];
echo count(array_intersect_assoc($net, $birds));

Q4:这个脚本会输出什么?

| 这会产生一个错误 |
| int(1) |
| string(6) "lemons" |
| 以上都不是 |

<?php
// Array of available fruits
$fruits = array("lemons" => 1, "oranges" => 4, "bananas" => 5, "apples" => 10);
$fruitsArrayObject = new ArrayObject($fruits);
$fruitsArrayObject->setFlags(ArrayObject::ARRAY_AS_PROPS);
// Try to use array key as property
var_dump($fruitsArrayObject->lemons);

Q5:这个脚本会输出什么?

| 这会产生一个错误 |
| 2 |
| 3 |
| 5 |

<?php
$a = array('one','two');
$b = array('three','four','five');
echo count($a + $b);

Q6:这个脚本会输出什么?

| 这会产生一个错误 |
| 3 |
| 2 |
| 1 |

<?php
$a = array('three','four','five');
$b = array('one','two');
echo count($a - $b);

Q7:以下代码的输出是什么?

| 这会产生一个错误 |
| 2 |
| 3 |
| 4 |

<?php
$source = '12,23,34';
$arr = str_split($source, 2);
echo count($arr);

问题 8:这段代码会输出什么?

| 调用krsort()时出错 |
| 调用 array_ flip()时出错 |
| 您不能引用键'PHP',因为数组中有多个键 |
| 1 |
| 5 |
| 6 |

<?php
$keys = range(1, 6, 2);
$arr = array_fill_keys($keys, 'PHP');
krsort($arr);
$arr = array_flip($arr);
echo $arr['PHP'];

问题 9:这段代码会输出什么?

| A: 1; B: 2 A: 3; B: 4 |
| Notice: Undefined offset: 1 |
| Undefined variable $a |
| 以上都不是 |

<?php
$array = [
  [1, 2],
  [3, 4],
];
foreach ($array as list($a, $b)) {
  echo "A: $a; B: $b" . PHP_EOL;
}

Q10:这段代码会输出什么?

| 这会产生一个错误 |
| 1 |
| 3 |
| 5 |

<?php
$arr = [1,2,3,4,5];
$spliced = array_splice($arr, 2, 1);
$number = array_shift($arr);
echo $number;

Footnotes 1

https://php.net/manual/en/ref.array.php

2

https://php.net/manual/en/function.implode.php

3

https://php.net/manual/en/function.preg-split.php

4

https://php.net/manual/en/function.str-split.php

5

https://secure.php.net/manual/en/function.array-shift.php

6

https://secure.php.net/manual/en/function.array-unshift.php

7

https://secure.php.net/manual/en/function.array-pop.php

8

https://secure.php.net/manual/en/function.array-push.php

9

https://php.net/manual/en/function.array-udiff.php

10

https://php.net/manual/en/function.array-diff.php

11

https://php.net/manual/en/function.array-udiff.php

12

https://php.net/manual/en/function.array-udiff-assoc.php

13

https://php.net/manual/en/function.array-diff-uassoc.php

14

https://php.net/manual/en/function.array-chunk.php

15

https://php.net/manual/en/function.array-slice.php

16

https://en.wikipedia.org/wiki/Empty_product

17

https://secure.php.net/manual/en/function.reset.php

18

https://secure.php.net/manual/en/function.next.php

19

https://php.net/manual/en/function.asort.php

20

http://php.net/manual/en/class.arrayobject.php

21

https://secure.php.net/manual/en/class.arrayobject.php

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值