PHP与递归(Recursion):递归尾调用(Tail Call)

在程序设计中,递归(Recursion)是一个很常见的概念,合理使用递归,可以提升代码的可读性,但同时也可能会带来一些问题。

下面以阶乘(Factorial)为例来说明一下递归的用法,实现语言是PHP:

<?php
function factorial($n) {
    if ($n == 0) {
        return 1;
    }
    return factorial($n - 1) * $n;
}
var_dump(factorial(100));

如果安装了XDebug的话,可能会遇到如下错误:

Fatal error: Maximum function nesting level of ’100′ reached, aborting!

注:这是XDebug的一个保护机制,可以通过max_nesting_level选项来设置。

即便代码能正常运行,只要我们不断增大参数,程序迟早会报错:

Fatal error:  Allowed memory size of … bytes exhausted

为什么呢?简单点说就是递归造成了栈溢出。有几个方法可以用来规避这个问题,比如说利用尾调用(Tail Call)来消除递归对栈的影响。

让我们先来看看尾调用的定义:如果一个函数在执行了一次函数调用后,不再做别的事就称为尾调用。形象点说就是直接返回一个函数调用。尾调用不会返回原来的函数,所以不需要额外的栈保留调用函数的数据。上面代码改成尾调用后类似下面代码的样子:

我们用PHP来实现一个尾调用版本的阶乘:

<?php
function factorial($n, $accumulator = 1) {
    if ($n == 0) {
        return $accumulator;
    }
    return factorial($n - 1, $accumulator * $n);
}

var_dump(factorial(100));

可惜测试后才发现PHP根本不支持尾调用!好在天无绝人之路,仔细阅读维基百科中关于尾调用的介绍,你会发现里面提到了Trampoline的概念。简单点说就是利用高阶函数消除递归,依照这样的理论基础,我们可以把上面的尾调用代码改写成如下方式:

<?php

function factorial($n, $accumulator = 1) {
    if ($n == 0) {
        return $accumulator;
    }

    return function() use($n, $accumulator) {
        return factorial($n - 1, $accumulator * $n);
    };
}

function trampoline($callback, $params) {
    $result = call_user_func_array($callback, $params);

    while (is_callable($result)) {
        $result = $result();
    }

    return $result;
}

var_dump(trampoline('factorial', array(100)));

看上去不错,不过我不得不向大家道个歉,本文用递归实现阶乘其实是个玩笑,实际上只要用一个循环就行了,《代码大全》里专门提到了这一点:

<?php

function factorial($n) {
    $result = 1;

    for ($i = 1; $i <= $n; $i++) {
        $result *= $i;
    }

    return $result;
}

var_dump(factorial(100));

还有很多别的方法可以用来规避递归引起的栈溢出问题,比如说Python中可以通过装饰器和异常来消灭尾调用,让人有一种别有洞天的感觉

除非能提升代码可读性,否则没有必要使用递归;迫不得已之时,最好考虑使用Tail Call或Trampoline等技术来规避潜在的栈溢出问题。

(End)

CentOS使用YUM安装php运行环境,包含php,php-fpm,nginx,mysql-server

本文内所有内容均在CentOS 6.2 32位下验证。

我们要使用yum来安装php-fpm和比较新版本的php,nginx,mysql-server的话, 首先得给yum添加几个源,CentOS默认的源里面软件比较旧,有些软件、库还没有。

EPEL源:

Extra Packages for Enterprise Linux (or EPEL)或者叫企业版 Linux 附加软件包, 是一个由特别兴趣小组创建、维护并管理的,针对 红帽企业版 Linux(RHEL)及其衍生发行版 (比如 CentOS、 Scientific Linux)的一个高质量附加软件包项目。

EPEL 的软件包通常不会与企业版 Linux 官方源中的软件包发生冲突,或者互相替换文件。 EPEL 与 Fedora 项目基本一致,包含完整的构建系统、升级管理器、镜像管理器等等。

EPEL源官方网站:https://fedoraproject.org/wiki/EPEL

添加EPEL源:

32位CentOS,在命令行运行下面命令:

rpm -ivh http://mirrors.ustc.edu.cn/fedora/epel/6/i386/epel-release-6-7.noarch.rpm

64位CentOS,在命令行运行下面命令:

rpm -ivh http://mirrors.ustc.edu.cn/fedora/epel/6/x86_64/epel-release-6-7.noarch.rpm

EPEL源只能作为CentOS官方源的补充,里面还都是一些系统基础的软件包, 我们要安装的php,php-fpm,mysql等软件里面还是没有,所以接下来添加另一个软件源Remi。

Remi源中的软件几乎都是最新稳定版。或许您会怀疑稳定不?放心吧, 这些都是Linux骨灰级的玩家编译好放进源里的,他们对于系统环境和软件编译参数的熟悉程度毋庸置疑。

Remi官方网站:http://rpms.famillecollet.com/

添加Remi源,不管32位还是64位的系统,运行下面命令:

rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm

Remi源默认是没有启用的,我们来启用Remi源,修改 /etc/yum.repos.d/remi.repo 文件,把文件内的 enabled=0 改为 enabled=1 ,注意:改文件内有2个 enabled=0 我们修改 [remi]下面的,不要修改[remi-test]下面的。

到这里yum源的配置结束,下面安装软件就简单了。安装时候有询问y/n的时候都是y

安装php,php-fpm以及php扩展:

yum install php php-fpm php-bcmatch php-gd php-mbstring php-mcrypt php-mysql

安装mysql: yum install mysql-server

安装nginx: yum install nginx

OK,一切搞定,下面把所有涉及到的配置文件列一下(下面配置文件默认均已自动创建,不用我们自己创建):

Mysql配置文件my.cnf路径:/etc/my.cnf Nginx配置文件nginx.conf路径:/etc/nginx/nginx.conf PHP配置文件php.ini路径: /etc/php.ini php-fpm配置文件php-fpm.conf路径:/etc/php-fpm.conf

(End)

如何使用Snoopy获取https页面

使用Snoopy来获取https页面需要用到curl,您需要先检查一下服务器是否安装curl。 Snoopy默认的curl路径是 /usr/local/bin/curl 。在服务器上运行 which curl 可以找到curl 的安装路径,如果他和默认的不一样,您需要设置 $snoopy->curl_path 变量为curl的路径, 代码如下:

include "Snoopy.class.php";
$snoopy = new Snoopy;
$snoopy->curl_path="/usr/bin/curl";

(End)

Snoopy网络客户端类使用文档

Snoopy方法

  • fetch($URI) 获取一个网页HTML源代码,内容保存在$this->results中, 如果目标网页是一个frames(框架)页面,$this->results以数组形式保存每一个页面

    include "Snoopy.class.php";
    $snoopy = new Snoopy;
    
    $snoopy->fetch("http://www.zzphp.net/");
    var_dump($snoopy->results);
    
  • fetchtext($URI) 获取一个网页内容,不包含HTML代码和其他无关内容,只是文字内容

    include "Snoopy.class.php";
    $snoopy = new Snoopy;
    
    $snoopy->fetchtext("http://www.zzphp.net/");
    var_dump($snoopy->results);
    
  • fetchform($URI) 获取一个网页内的表单内容,只包含页面中的表单(form)代码

    include "Snoopy.class.php";
    $snoopy = new Snoopy;
    
    $snoopy->fetchform("http://www.zzphp.net/");
    var_dump($snoopy->results);
    
  • fetchlinks($URI) 获取一个网页内所有的链接,这些链接都是完整的带有域名的链接, 遇到不完整或者是相对路径的链接,Snoopy会自动补全链接地址

    include "Snoopy.class.php";
    $snoopy = new Snoopy;
    
    $snoopy->fetchlinks("http://www.zzphp.net/");
    var_dump($snoopy->results);
    
  • submit($URI,$formvars,$formfiles) 提交一个表单到指定的$URI,$formvars变量是需要提交的数据, 他需要一个数组,$formfiles和$formvars类是,是个数组,但是值需要是一个文件地址, 例如:$formfiles["var"] = “/dir/filename.ext”;

    include "Snoopy.class.php";
    $snoopy = new Snoopy;
    $submit_url = "http://www.zzphp.net/examples/snoopy_submit.php"; 
    $submit_vars["a"] = "a_value";
    $submit_vars["b"] = "b_value"; 
    $snoopy->submit($submit_url,$submit_vars);
    var_dump($snoopy->results);
    
  • submittext($URI,$formvars,$formfiles) 提交一个表单到指定的$URI,和submit方法类似, 不同的只是他返回文本内容,不包含HTML代码和其他无关数据

  • submitlinks($URI,$formvars,$formfiles) 这个方法和submit方法类似,但是返回的值是所有链接, 这点和fetchlinks方法类是

类变量 (括号中是默认值)

  • $host
    链接到的主机地址

  • $port
    链接到的主机的端口

  • $proxy_host
    使用的代理主机地址,如果有的话

  • $proxy_port
    使用的代理主机端口,如果有的话

  • $agent
    HTTP协议user-agent值,(Snoopy v0.1)

  • $referer
    来路信息,如果有的话

  • $cookies
    客户端cookies,如果有的话

  • $rawheaders
    其他的HTTP头信息

  • $maxredirs
    最大重定向次数。0=不允许重定向,默认值:(5)

  • $offsiteok
    是否允许重定向。(true)

  • $expandlinks
    是否将链接地址都自动补全为完整链接地址 (true)

  • $user
    认证中的用户名,如果有的话

  • $pass
    认证中的密码,如果有的话

  • $accept
    HTTP接受文件类型 (image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, /)

  • $error
    错误信息,如果有的话

  • $response_code
    服务器返回的HTTP状态码

  • $headers
    服务器返回的HTTP头信息

  • $maxlength
    从服务器返回数据的大小

  • $read_timeout
    读取数据的超时时间,设置为0禁用超时 (需要PHP 4以上版本)

  • $timed_out
    如果发生了读取数据超时的情况,此变量为真 (需要PHP 4以上版本)

  • $maxframes
    限制框架网页最大的框架层级

  • $status
    获取HTTP状态

  • $temp_dir
    保存获取网页内容的临时目录 (/tmp)

  • $curl_path
    cURL文件的系统地址,如故没有cURL设置为false

(End)

Snoopy模拟网页浏览器的PHP类

Snoopy是一个模拟网页浏览器的PHP类,他可以获取网页内容和发送表单。

  • 获取网页的内容
  • 获取网页的文字(去掉HTML代码)
  • 获取网页的链接
  • 支持代理
  • 支持基本的用户/密码认证模式
  • 支持自定义user_agent,referer,cookies和header内容
  • 支持浏览器转向,并能控制转向深度
  • 能把网页中的链接扩展成高质量的url(默认)
  • 提交表单数据并且获取返回值
  • 支持跟踪HTML框架(v0.92增加)
  • 支持跳转的时候传递cookies

Snoopy需要PHP PCRE扩展(Perl Compatible Regular Expressions),需要PHP 3.0.9以上版本,如果要支持read超时,需要PHP 4以上版本。Snoopy在PHP 3.0.12版本下面开发和测试。

虽然Snoopy最后一个版本1.2.4发布距离现在已经有4年之久(2008年10月发布),但是仍然不影响他的易用和强大。

下载地址:http://sourceforge.net/projects/snoopy/

Snoopy性能测试、速度测试

之前测试了curl, file_get_contents和fsockopen方法获取网页的速度,今天再测试下Snoopy,Snoopy底层还是使用fsockopen方法驱动的。按想这个测试结果应该不会比上次测试中的fsockopen要好,但结果却相反。

测试代码:

<?php
require 'Benchmark/Iterate.php';
include "Snoopy.class.php";

function snoopy_test(){
    $snoopy = new Snoopy;

    $snoopy->fetch("http://www.google.com.hk/");
    return $snoopy->results;
}

$benchmark = new Benchmark_Iterate();
$benchmark->run(10, 'snoopy_test');
$result = $benchmark->get();
var_dump($result);

测试结果:

array (size=12)
  1 => string '0.301730' (length=8)
  2 => string '0.242408' (length=8)
  3 => string '0.306462' (length=8)
  4 => string '0.242553' (length=8)
  5 => string '0.249154' (length=8)
  6 => string '0.316298' (length=8)
  7 => string '0.250106' (length=8)
  8 => string '0.248223' (length=8)
  9 => string '0.296550' (length=8)
  10 => string '0.240236' (length=8)
  'mean' => string '0.269372' (length=8)
  'iterations' => int 10

查看上次测试结果:curl, file_get_contents和fsockopen速度比较

为了测试结果有比较性,我又运行了上次的测试代码,结果和上面文章内的结果几乎一样。

(End)

PHP官方文档中preg_match_all函数有错误

在PHP官方文档中preg_match_all函数中看函数的定义第三个参数$matches参数为可选的,但是在实际使用中发现这个参数如何不传,这个preg_match_all函数是不会正常工作的,该函数的调用永远返回false。

下面为文档中对函数的说明:

int preg_match_all ( string $pattern , string $subject [, array &$matches [, int $flags = PREG_PATTERN_ORDER [, int $offset = 0 ]]] )

PHP中文手册(最新)

做了个PHP中文手册的镜像,地址:http://www.zzphp.net/doc/

为什么要做个这个手册呢:

  1. 现有的PHP中文手册都太旧,有些都好些年没更新过了。
  2. 官方的中文手册有些不太好用,特别是查看函数的时候,记得有这个函数,又想不起来这个函数名。

这个PHP中文手册要做的:

  1. 尽量保持最新的内容,每月更新一次应该没什么问题。
  2. 在这个PHP中文手册的基础上做一些东西,方便使用的,比如函数快速查找等等的一些实用东西。
  3. 也作为PHP官方中文手册的一个镜像。

当然,这些需要时间来做,目前就是原版的官方中文手册上线。

(End)

使用file_get_contents指定出口IP和端口

如果我们的机器有多个IP,可能是多个网卡,也可能是一个网卡绑定多个IP,那么我们使用file_get_contents获取网页内容的时候就可以指定使用某个IP和端口作为出口,让对方网站获取我们的IP时候可以做到我们预想的结果。

首先来准备一个显示IP和端口的页面:

getip.php
<?php
echo $_SERVER['REMOTE_ADDR'].':'.$_SERVER['REMOTE_PORT'];

假定我们现在有2个IP,一个127.0.0.1,一个192.168.1.2

<?php
$opts1 = array(
    'socket' => array(
        'bindto' =>'127.0.0.1:0'
    )
);
$opts2 = array(
    'socket' => array(
        'bindto' =>'192.168.1.2:0'
    )
);
$opts3 = array(
    'socket' => array(
        'bindto' =>'0:8888'
    )
);
$opts4 = array(
    'socket' => array(
        'bindto' =>'127.0.0.1:8888'
    )
);

$url = 'http://192.168.1.2/getip.php';

$context1 = stream_context_create($opts1);
$context2 = stream_context_create($opts2);
$context3 = stream_context_create($opts3);
$context4 = stream_context_create($opts4);

echo file_get_contents($url, false, $context1)."\r\n";
echo file_get_contents($url, false, $context2)."\r\n";
echo file_get_contents($url, false, $context3)."\r\n";
echo file_get_contents($url, false, $context4)."\r\n";

上面代码运行结果为:

127.0.0.1:54873
192.168.1.2:54874
192.168.1.2:8888
127.0.0.1:8888