PHP curl实现多进程并发高效率采集爬虫

 更新时间:2016年11月25日 15:31  点击:1582
PHP curl实现多进程并发抓取数据我们经常的用到了,今天我们来看一篇关于PHP curl实现多进程并发高效率采集爬虫的例子,具体的细节如下。

演示代码

使用PHP编写多进程并发高效率采集爬虫 (1)

运行效果(图1)

使用PHP编写多进程并发高效率采集爬虫 (2)

运行效果(图2)

使用PHP编写多进程并发高效率采集爬虫 (3)

主要封装函数

multi_process();
根据参数,创建指点数目的子进程。
亮点功能1:子进程各种异常退出,比如segment fault, Allowed memory size exhausted等,中断一个子进程后,父进程会重新fork一个新进程顶上去,保持子进程数量。如果子进程里完成任务(比如判断tid达到10000),可以在子进程里exit(9),父进程会收到这个退出状态(9),然后等待所有子进程退出,最后退出自身进程。
亮点功能2:与curl封装函数一起实现了一统计功能,在程序关闭后会显示出一些主要的统计信息(图2的底部)。
mp_counter();
在父进程以及所有子进程之间通信,负责协调分配各子进程的任务,使用了锁机制。可以设置’init’参数重置计数,可以设置每次更新计数的值。
curl_get();
对curl相关函数的封装,加入了大量的错误机制,支持POST,GET,Cookie,Proxy,下载。
mp_msg();
实现规范之一就是,每条任务处理完,只输出一行信息。
亮点功能:这个函数会判断终端的高度和宽度,实现每一屏内容会显示一条统计信息(图1的紫色行),便于观察程序的执行情况,控制每一行输出的长度,保持一条信息不会超过一行。
rand_exit();
众所周知,PHP存在内在泄露的问题,所以每一个子进程里执行一定次数的任务后就退出,由multi_process()负责自动建立新的子进程(如图1中的绿色行)。

程序效率

本次测试使用的是Vultr的最低配置机器,1 CPU(3.6GHz),768MB RAM,美国LA机房(一定程度上影响了抓取速度)。
执行了十多分钟后,统计信息如下:
使用PHP编写多进程并发高效率采集爬虫 (4)

运行期间内存占用统计(while true; do psmem | grep php;sleep 10; done)如下:

使用PHP编写多进程并发高效率采集爬虫 (5)

vmstat 1命令结果如下
使用PHP编写多进程并发高效率采集爬虫 (6)

iftop带宽监控如下:
使用PHP编写多进程并发高效率采集爬虫 (7)

简单点解释下:
50个子进程,执行11分55秒,抓取50951次,按这个速度计算,一天可以抓取615万次。
所有进程(1父进程+50子进程)共占用内存约60MB,占用CPU约20%(1核心),带宽占用约7-8Mbps。
按上面的性能参数来看,本机再开5倍的子进程数量是可以承受的,但是目标机器承受不了这么大的压力。

不同进程数量的抓取速度对比:
1个进程

使用PHP编写多进程并发高效率采集爬虫 (8)

10个进程
使用PHP编写多进程并发高效率采集爬虫 (9)

100个进程
使用PHP编写多进程并发高效率采集爬虫 (10)

多进程的封装几乎完美,但curl由于它的功能太过于丰富和强大,可能永远也无法达到完美

代码如下

curl.lib.php

 代码如下 复制代码

<?php

// 命令行颜色输出
$colors['red']         = "\33[31m";
$colors['green']       = "\33[32m";
$colors['yellow']      = "\33[33m";
$colors['end']         = "\33[0m";
$colors['reverse']     = "\33[7m";
$colors['purple']      = "\33[35m";

/*
 默认参数设置
*/
$curl_default_config['ua'] = 'Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)';
$curl_default_config['referer'] = '';
$curl_default_config['retry'] = 5;
$curl_default_config['conntimeout'] = 30;
$curl_default_config['fetchtimeout'] = 30;
$curl_default_config['downtimeout'] = 60;

/*
 针对指定域名设置referer(通常是用于下载图片),优先于$curl_default_config
 默认使用空referer,一般不会有问题
 eg: $referer_config = array(
          'img_domain'=>'web_domain',
          'e.hiphotos.baidu.com'=>'http://hi.baidu.com/');
*/
$referer_config = array('img1.51cto.com'=>'blog.51cto.com',
    '360doc.com'=>'www.360doc.com');

/*
针对指定域名设置User-agent,优先于$curl_default_config
 默认使用百度蜘蛛的UA,拒绝百度UA的网站极少
 eg: $useragent_config = array(
          'web_domain'=>'user agent',
          'www.xxx.com'=>'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)');
*/
$useragent_config = array('hiphotos.baidu.com'=>'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)';

/*
 * 如果机器有多个IP地址,可以改变默认的出口IP,每次调用会在数组中随机选择一个。考虑到可能会有需要排除的IP,所以这里不自动配置为所有的IP。
 * eg: $curl_ip_config = array('11.11.11.11', '22.22.22.22');
 */
$local_ip_config = array();

// cookie和临时文件目录
if((@file_exists('/dev/shm/') && @is_writable('/dev/shm/'))){
 $cookie_dir = $tmpfile_dir = '/dev/shm/';
}else{
 $cookie_dir = $tmpfile_dir = '/tmp/';
}

// 清除过期的cookie文件和下载临时文件
if(php_sapi_name() == 'cli'){
 clear_curl_file();
}

/**
 * GET方式抓取网页
 *
 * @param string $url    网页URL地址
 * @param string $encode 返回的页面编码,默认为GBK,设置为空值则不转换
 * @return string        网页HTML内容
 */
function curl_get($url, $encode='gbk'){
    return curl_func($url, 'GET', null, null, null, $encode);
}

/**
 * POST方式请求网页
 *
 * @param string $url       请求的URL地址
 * @param array  $data      发送的POST数据
 * @param string $encode    返回的页面编码,默认为GBK,设置为空值则不转换
 * @return bool
 */
function curl_post($url, $data, $encode='gbk'){
    return curl_func($url, 'POST', $data, null, null, $encode);
}

/**
 * 获取页面的HEADER信息
 *
 * HTTP状态码并不是以“名称:值”的形式返回,这里以http_code作为它的名称,其他的值都有固定的名称,并且转成小写
 *
 * @param string $url URL地址
 * @return array      返回HEADER数组
 */
function curl_header($url, $follow=true){
    $header_text = curl_func($url, 'HEADER');

    if(!$header_text){
        // 获取HTTP头失败
        return FALSE;
    }
    $header_array =explode("\r\n\r\n", trim($header_text));
 if($follow){
  $last_header = array_pop($header_array);
 }else{
  $last_header = array_shift($header_array);
 }
   
    $lines = explode("\n", trim($last_header));

    // 处理状态码
    $status_line = trim(array_shift($lines));
    preg_match("/(\d\d\d)/", $status_line, $preg);
    if(!empty($preg[1])){
        $header['http_code'] = $preg[1];
    }else{
        $header['http_code'] = 0;
    }
    foreach ($lines as $line) {
        list($key, $val) = explode(':', $line, 2);
        $key = str_replace('-', '_', strtolower(trim($key)));
        $header[$key] = trim($val);
    }
    return $header;
}

/**
 * 下载文件
 *
 * @param $url      文件地址
 * @param $path     保存到的本地路径
 * @return bool     下载是否成功
 */
function curl_down($url, $path, $data=null, $proxy=null){
    if(empty($data)){
        $method = 'GET';
    }else{
        $method = 'POST';
    }

    return curl_func($url, $method, $data, $path, $proxy);
}

/**
 * 使用代理发起GET请求
 *
 * @param string $url       请求的URL地址
 * @param string $proxy     代理地址
 * @param string $encode    返回编码
 *
 * @return string           网页内容
 */
function curl_get_by_proxy($url, $proxy, $encode='gbk'){
    return curl_func($url, 'GET', null, null, $proxy, $encode);
}


/**
 * 使用代理发起POST请求
 *
 * @param string $url       请求的URL地址
 * @param string $proxy     代理地址
 * @param string $encode    返回编码
 *
 * @return string           网页内容
 */
function curl_post_by_proxy($url, $data, $proxy, $encode='gbk'){
    return curl_func($url, 'POST', $data, null, $proxy, $encode);
}

/**
 * @param string $url       请求的URL地址
 * @param string $encode    返回编码
 *
 * @return string           网页内容
 */

function img_down($url, $path_pre){
    $img_tmp = '/tmp/curl_imgtmp_pid_'.getmypid();
    $res = curl_down($url, $img_tmp);
    if(empty($res)){
        return $res;
    }
    $ext = get_img_ext($img_tmp);
    if(empty($ext)){
        return NULL;
    }
    $path = "{$path_pre}.{$ext}";
    @mkdir(dirname($path), 0777, TRUE);
    // 转移临时的文件路径
    rename($img_tmp, $path);
    return $path;
}

function get_img_ext($path){
    $types = array(
        1 => 'gif',
        2 => 'jpg',
        3 => 'png',
        6 => 'bmp'
    );
    $info = @getimagesize($path);
    if(isset($types[$info[2]])){
        $ext = $info['type'] = $types[$info[2]];
        $ext == 'jpeg' && $ext = 'jpg';
    } else{
        $ext = FALSE;
    }
    return $ext;
}

/**
 * 获取文件类型
 *
 * @param string $filepath 文件路径
 * @return array           返回数组,格式为array($type, $ext)
 */
function get_file_type($filepath){

}

/**
 * 返回文件的大小,用于下载文件后判断与本地文件大小是否相同
 * curl_getinfo()方式获得的size_download并不一定是文件的真实大小
 *
 * @param  string $url    URL地址
 * @return string         网络文件的大小
 */
function get_file_size($url){
    $header = curl_header($url);
    if(!empty($header['content_length'])){
        return $header['content_length'];
    }else{
        return FALSE;
    }
}

/**
 * 获取状态码
 *
 * @param  string $url URL地址
 * @return string      状态码
 */
function get_http_code($url, $follow=true){
    $header = curl_header($url, $follow);
    if(!empty($header['http_code'])){
        return $header['http_code'];
    }else{
        return FALSE;
    }
}

/**
 * 获取URL文件后缀
 *
 * @param  string $url URL地址
 * @return array      文件类型的后缀
 */
function curl_get_ext($url){
    $header = curl_header($url);
    if(!empty($header['content_type'])){
        @list($type, $ext) = @explode('/', $header['content_type']);
        if(!empty($type) && !empty($ext)){
            return array($type, $ext);
        }else{
            return array('', '');
        }
    }else{
        return array('', '');
    }
}

/**
 * 封装curl操作
 *
 * @param string $url            请求的URL地址
 * @param string $method         请求的方法(POST, GET, HEADER, DOWN)
 * @param mix    $arg            POST方式为POST数据,DOWN方式时为下载保存的路径
 * @param string $return_encode  网页返回的编码
 * @param string $proxy          代理
 * @return mix                   返回内容。4xx序列错误和空白页面会返回NULL,curl抓取错误返回False。结果正常则返回页面内容。
 */
// 待改进,下载到临时文件,下载成功后再转移(已经有文件则覆盖),下载失败则删除。
// 待改进,参数形式改成curl_func($url, $method, $data=null, savepath=null, $proxy=null, $return_encode='gbk')
function curl_func($url, $method, $data=null, $savepath=null, $proxy=null, $return_encode=null){
    global $colors, $cookie_dir, $tmpfile_dir, $referer_config, $useragent_config, $local_ip_config, $curl_config;

    // 控制台输出颜色
    extract($colors);

    // 去除URL中的/../
    $url = get_absolute_path($url);

    // 去除实体转码
    $url = htmlspecialchars_decode($url);

    // 统计数据
    if(function_exists('mp_counter')){
        if(!empty($savepath)){
            mp_counter('down_total');   // 下载次数计数
        }elseif($method == 'HEADER'){
            mp_counter('header_total'); // 抓取HTTP头次数计数
        }else{
            mp_counter('fetch_total');  // 抓取网页次数计数
        }
    }

    for($i = 0; $i < curl_config_get('retry'); $i++){

        // 初始化
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);

        // 设置超时
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, curl_config_get('conntimeout'));      // 连接超时
        if(empty($savepath)){
            curl_setopt($ch, CURLOPT_TIMEOUT, curl_config_get('fetchtimeout'));           // 抓取网页(包括HEADER)超时
        }else{
            curl_setopt($ch, CURLOPT_TIMEOUT, curl_config_get('downtimeout'));        // 下载文件超时
        }

        // 接收网页内容到变量
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);

        // 忽略SSL验证
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);

        // 设置referer, 在文件里配置的优先级最高
        foreach($referer_config as $domain=>$ref){
            if(stripos($url, $domain) !== FALSE){
                $referer = $ref;
                break;
            }
        }
  // 检查是否有通过curl_set_referer()设置referer
        if(empty($referer) && !empty($curl_config[getmypid()]['referer'])){
            $referer = $curl_config[getmypid()]['referer'];
        }
  if(!empty($referer)){
   curl_setopt($ch, CURLOPT_REFERER, $referer);
  }
  

        // 设置HTTP请求标识,在文件里配置的优先级最高
        foreach($useragent_config as $domain=>$ua){
            if(stripos($url, $domain) !== FALSE){
                $useragent = $ua;
                break;
            }
        }
  // 检查是否有通过curl_set_ua()设置useragent
        if(empty($useragent)){
            $useragent = curl_config_get('ua');
        }

        curl_setopt($ch, CURLOPT_USERAGENT, $useragent);

        // 出口IP
        if(!empty($local_ip_config)){
            curl_setopt($ch, CURLOPT_INTERFACE, $local_ip_config[array_rand($local_ip_config)]);
        }

        // 设置代理
        if(!empty($proxy)){
            curl_setopt($ch, CURLOPT_PROXY, $proxy);
            curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
        }

        // 设置允许接收gzip压缩数据,以及解压,抓取HEADER时不使用(获取不到正确的文件大小,影响判断下载成功)
        if($method != 'HEADER') {
            curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept-Encoding: gzip, deflate'));
            curl_setopt($ch, CURLOPT_ENCODING, "");
        }

        // 遇到301和302转向自动跳转继续抓取,如果用于WEB程序并且设置了open_basedir,这个选项无效
        @curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
        // 最大转向次数,避免进入到死循环
        curl_setopt($ch, CURLOPT_MAXREDIRS, 5);

        // 启用cookie
        $cookie_path = $cookie_dir . 'curl_cookie_pid_' . get_ppid();
        curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_path);
        curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_path);

        // 设置post参数内容
        if($method == 'POST'){
            curl_setopt($ch, CURLOPT_HEADER, 0);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }

        // 设置用于下载的参数
        if(!empty($savepath)){
            $tmpfile = $tmpfile_dir . '/curl_tmpfile_pid_'.getmypid();
            file_exists($tmpfile) && unlink($tmpfile);
            $fp = fopen($tmpfile, 'w');
            curl_setopt($ch, CURLOPT_FILE, $fp);
        }

        // 仅获取header
        if($method == 'HEADER'){
            curl_setopt($ch, CURLOPT_NOBODY, TRUE);
            curl_setopt($ch, CURLOPT_HEADER, TRUE);
        }

        // 抓取结果
        $curl_res = curl_exec($ch);
        // curl info
        $info = curl_getinfo($ch);

        // 调试curl时间,记录连接时间,等待时间,传输时间,总时间。
        // 测试方法,任何输出前设置sleep,输出中间设置sleep
        /*
        foreach($info as $key=>$val){
            echo "$key:$val\n";
        }
        exit(9);
        */
        // 错误信息
        $error_msg = curl_error($ch);
        $error_no = curl_errno($ch);

        // 关闭CURL句柄
        curl_close($ch);

        // 如果CURL有错误信息则判断为抓取失败,重试
        if(!empty($error_no) || !empty($error_msg)){
            $error_msg = "{$error_msg}($error_no)";
            curl_msg($error_msg, $method, $url, 'yellow');
            continue;
        }

        // 统计流量
        if(function_exists('mp_counter')){
            if(!empty($info['size_download']) && $info['size_download'] > 0){
                mp_counter('download_total', $info['size_download']);
            }
        }

        // 对结果进行处理
        if($method == 'HEADER'){
            // 返回header信息
            return $curl_res;
        }else{
            // 最终的状态码
            $status_code = $info['http_code'];

            if(in_array($status_code, array_merge(range(400, 417), array(500, 444)))){
                // 非服务器故障性的错误,直接退出,返回NULL
                $error_msg = $status_code;
                if(!empty($savepath)){
                    $method = "{$method}|DOWN";
                }
                curl_msg($error_msg, $method, $url, 'red');
                return NULL;
            }if($status_code != 200){
                // 防止网站502等临时错误,排除了上面的情况后,非200就重试。这一条规则需要后续根据情况来改进。
                // curl执行过程中会自动跳转,这里不会出现301和302,除非跳转次数超过CURLOPT_MAXREDIRS的值
                $error_msg = $status_code;
                curl_msg($error_msg, $method, $url, 'yellow');
                continue;
            }

            if(empty($savepath)){
                // 抓取页面
                if(empty($curl_res)){
                    // 空白页面
                    $error_msg = "blank page";
                    // 返回NULL值,调用处注意判断
                    return NULL;
                }else{
                    // 默认将页面以GBK编码返回

                    // 分析页面编码
                    preg_match_all("/<meta.*?charset=(\"|'?)(.*?)(;|\"|'|\s)/is", $curl_res, $matches);

                    // 转码条件:1)匹配到编码, 2)返回编码不为空, 3)匹配到的编码和返回编码不相同
                    if(!empty($matches[2][0]) && !empty($return_encode)
                        && str_replace('-', '', strtolower($matches[2][0]))
                        != str_replace('-', '', strtolower($return_encode))){
                        $curl_res = @iconv($matches[2][0], "{$return_encode}//IGNORE", $curl_res);
                        // 替换网页标明的编码
                        $curl_res = str_ireplace($matches[2][0], $return_encode, $curl_res);
                    }

                    // iconv如果失败则返回空白页
                    if(empty($curl_res)){
                        return NULL;
                    }else{
                        // 将相对路径转换为绝对路径
                        $curl_res = relative_to_absolute($curl_res, $url);
                        return $curl_res;
                    }
                }
            }else{
                // 下载文件
                if(@filesize($tmpfile) == 0){
                    $error_msg = 'Emtpy Content';
                    continue;
                }

                // 统计下载文件量
                if(function_exists('mp_counter')){
                    mp_counter('download_size', filesize($tmpfile));
                }
                // 创建目录
                @mkdir(dirname($savepath), 0777, TRUE);
                // 转移临时的文件路径
                rename($tmpfile, $savepath);

                return TRUE;
            }
        }
    }

    // 如果是下载或者抓取header,并且错误代码为6(域名无法解析),则不输出错误。失效的图片引用太多了。
    // 域名不合法的时候也无法输出错误了,需要改进,在前面判断URL的合法性
    if(!(($method == 'HEADER' || !empty($savepath)) && !empty($error_no) && $error_no == 6)){
        if(!empty($savepath)){
            $method = "{$method}|DOWN";
        }
        curl_msg($error_msg, $method, $url, 'red');
    }

    // 统计数据
    if(function_exists('mp_counter')){
        if(!empty($savepath)){
            mp_counter('down_failed');
        }elseif($method == 'HEADER'){
            mp_counter('header_failed');
        }else{
            mp_counter('fetch_failed');
        }
    }

    return FALSE;
}

/**
 * 输出错误信息
 *
 * @param string $msg     错误信息
 * @param string $method  请求方式
 * @param string $url     URL地址
 * @param string $color   颜色
 */
function curl_msg($msg, $method, $url, $color){
    global $colors;
    extract($colors);

    // 多并发下建议关闭黄色错误输出
    //$available_msg[] = 'yellow';
    $available_msg[] = 'red';

    if(php_sapi_name() != 'cli'){
        return;
    }

    if(!in_array($color, $available_msg)){
        return;
    }

    echo "{$reverse}".$colors[$color]."({$method})[cURL ERROR: {$msg}] {$url}{$end}\n";
}

/**
 * 将URL地址转换为绝对路径
 * URL地址有可能会遇到包含'/../'构成的相对路径,curl不会自动转换
 * echo get_absolute_path("http://www.a.com/a/../b/../c/../././index.php");
 * 结果为:http://www.a.com/index.php
 *
 * @param  string $path 需要处理的URL
 * @return string       返回URL的绝对路径
 */
function get_absolute_path($path) {
    $parts = array_filter(explode('/', $path), 'strlen');
    $absolutes = array();
    foreach ($parts as $part) {
        if ('.' == $part) continue;
        if ('..' == $part) {
            array_pop($absolutes);
        } else {
            $absolutes[] = $part;
        }
    }
    return str_replace(':/', '://', implode('/', $absolutes));
}

/**
 * 使用图片URL的md5值作为路径,并且分级目录
 * 深度为e时,伪静态规则为rewrite ^/(.)(.)(.)(.*)$ /$1/$2/$3/$4 break;
 * 平均1篇文章1张图片,三千万文章,三千万图片,3级目录最终4096子目录,平均每目录7324个图片
 *
 * @param string $str 原图片地址
 * @param int $deep   目录深度
 * @return string     返回分级目录
 */
function md5_path($str, $deep = 3){
    $md5 = substr(md5($str), 0, 16);
    preg_match_all('/./', $md5, $preg);
    $res = '';
    for($i = 0; $i < count($preg[0]); $i++){
        $res .= $preg[0][$i];
        if($i < $deep){
            $res .= '/';
        }
    }
    return $res;
}

function relative_to_absolute($content, $url) {
    $content = preg_replace("/src\s*=\s*\"\s*/", 'src="', $content);
    $content = preg_replace("/href\s*=\s*\"\s*/", 'href="', $content);

    preg_match("/(http|https|ftp):\/\/[^\/]*/", $url, $preg_base);
    if(!empty($preg_base[0])){
        // $preg_base[0]内容如http://www.yundaiwei.com
        // 这里处理掉以/开头的链接,也就是相对于网站根目录的路径
        $content = preg_replace('/href=\s*"\//i', 'href="'.$preg_base[0].'/', $content);
        $content = preg_replace('/src=\s*"\//ims', 'src="'.$preg_base[0].'/', $content);
    }

    preg_match("/(http|https|ftp):\/\/.*\//", $url, $preg_full);
    if(!empty($preg_full[0])){
        // 这里处理掉相对于目录的路径,如src="../../images/jobs/lippman.gif"
        // 排除掉file://开头的本地文件链接,排除掉data:image方式的BASE64图片
        $content = preg_replace('/href=\s*"\s*(?!http|file:\/\/|data:image|javascript)/i', 'href="'.$preg_full[0], $content);
        $content = preg_replace('/src=\s*"\s*(?!http|file:\/\/|data:image|javascript)/i', 'src="'.$preg_full[0], $content);
    }

    return $content;
}

/**
 * 清除过期的cookie文件和下载临时文件
 */
function clear_curl_file(){
    global $cookie_dir;

    $cookie_files = glob("{$cookie_dir}curl_*_pid_*");
    $tmp_files = glob("/tmp/curl_*_pid_*");
    $files = array_merge($cookie_files, $tmp_files);

    foreach($files as $file){
        preg_match("/pid_(\d*)/", $file, $preg);
        $pid = $preg[1];
        $exe_path = "/proc/{$pid}/exe";
        // 如果文件不存在则说明进程不存在,判断是否为PHP进程,排除php-fpm进程
        if(!file_exists($exe_path)
            || stripos(readlink($exe_path), 'php') === FALSE
            || stripos(readlink($exe_path), 'php-fpm') === TRUE){
   $sem = @sem_get(@ftok($file, 'a'));
   if($sem){
    @sem_remove($sem);
   }
            unlink($file);
        }
    }
}


/**
 * 如果是在子进程中,获取父进程PID,否则获取自身PID
 * @return int
 */
if(!function_exists('get_ppid')){
    function get_ppid(){
  
  if(php_sapi_name() != 'cli'){
   // 如果是web方式调用,返回PHP执行进程PID,如APACHE或者PHP-FPM
   getmypid();
  }else{
   // 命令行执行进入到这里
   // 这里需要识别出是在子进程中调用还是在父进程中调用,不同的形式,保存的变量内容的文件位置需要保持一致
   $ppid = posix_getppid();
   // 理论上,这种判断方式可能会出坑。但在实际使用中,除了fork出的子进程外,不太可能让PHP进程的父进程的程序名中出现php字样。
   if(strpos(readlink("/proc/{$ppid}/exe"), 'php') === FALSE){
    $pid = getmypid();
   }else{
    $pid = $ppid;
   }
   return $pid;
  }

    }
}


// UTF-8转GBK
if(!function_exists('u2g')){
    function u2g($string){
        return @iconv("UTF-8", "GBK//IGNORE", $string);
    }
}


// GBK转UTF-8
if(!function_exists('g2u')) {
    function g2u($string)
    {
        return @iconv("GBK", "UTF-8//IGNORE", $string);
    }
}

function curl_rand_ua_pc(){
    $ua = 'Mozilla/5.0 (compatible; MSIE '.rand(7, 9).
        '.0; Windows NT 6.1; WOW64; Trident/'.rand(4, 5).'.0)';
    return $ua;
}

function curl_rand_ua_mobile(){
    $op = 'Mozilla/5.0 (Linux; U; Android '.rand(4,5).'.'.rand(1,5).'.'.rand(1,5).'; zh-cn; MI '.rand(3, 5).');';
    $browser = 'AppleWebKit/'.rand(500, 700).'.'.rand(1,100).'.'.rand(1,100)
        .' (KHTML, like Gecko) Version/'.rand(5,10)
        .'.0 Mobile Safari/537.36 XiaoMi/MiuiBrowser/'.rand(1,5).'.'.rand(1,5).'.'.rand(1,5);
    return $op.$browser;
}

function curl_config_get($key){
 global $curl_config, $curl_default_config;

 if(!empty($curl_config[getmypid()][$key])){
  return $curl_config[getmypid()][$key];
 }elseif(!empty($curl_default_config[$key])){
  return $curl_default_config[$key];
 }else{
  echo '$curl_default_config'."[$key] Not Found!\n";
  exit(9);
 }
}

function curl_config_set($key, $val){
 global $curl_config;
 $curl_config[getmypid()][$key] = $val;
}

function curl_set_ua($ua){
 curl_config_set('ua', $ua);
}

function curl_set_referer($referer){
 curl_config_set('referer', $referer);
}

function curl_set_retry($retry){
 curl_config_set('retry', $retry);
}

function curl_set_conntimeout($conntimeout){
 curl_config_set('conntimeout', $conntimeout);
}

function curl_set_fetchtimeout($fetchtimeout){
 curl_config_set('fetchtimeout', $fetchtimeout);
}

function curl_set_downtimeout($downtimeout){
 curl_config_set('downtimeout', $downtimeout);
}

process.lib.php

 代码如下 复制代码

<?php
if(php_sapi_name() != 'cli'){
 return;
}

declare(ticks = 1);

// 中断信号
$signals = array(
    SIGINT  => "SIGINT",
    SIGHUP  => "SIGHUP",
    SIGQUIT => "SIGQUIT"
);

// 命令行颜色输出
$colors['red']         = "\33[31m";
$colors['green']       = "\33[32m";
$colors['yellow']      = "\33[33m";
$colors['end']         = "\33[0m";
$colors['reverse']     = "\33[7m";
$colors['purple']      = "\33[35m";
$colors['cyan']        = "\33[36m";

// 程序开始运行时间
$start_time = time();

// 父进程PID
$fpid = getmypid();

// 文件保存目录,/dev/shm/是内存空间映射到硬盘上,IO速度快。
// 有些环境上可能会没有这个目录,比如OpenVZ的VPS,这个路径实际是在硬盘上
if(file_exists('/dev/shm/') && is_dir('/dev/shm/')){
    $process_file_dir = '/dev/shm/';
}else{
    $process_file_dir = '/tmp/';
}

// 清理过期资源(文件和SEM信号锁),每次程序执行都需要调用,清除掉之前执行时的残留文件。
clear_process_resource();

// 判断是否在子进程中
function is_subprocess(){
 global $fpid;
 if(getmypid() != $fpid){
  return true;
 }else{
  return false;
 }
}

/**
 * 多进程计数
 *
 * 1,用于多进程运行时的任务分配与计数,比如要采集某DZ论坛的帖子,则可以将计数器用于/thread-tid-1-1.html中
 * 的tid,实现进程间的协调工作
 * 2,由于shm_*系列函数的操作不够灵活,所以这里主要用于/proc/和/dev/shm/这二个目录来实现数据的读写(内存操
 * 作,不受硬盘IO性能影响),用semaphore信号来实现锁定和互斥机制
 * 3,编译PHP时需要使用参数--enable-sysvmsg安装所需的模块
 *
 * @param   string  $countername    计数器名称
 * @param   mix     $update         计数器的更新值,如果是'init',计数器则被初始化为0
 * @return int                      返回计数
 */
function mp_counter($countername, $update=1){
    global $process_file_dir;
    $time = date('Y-m-d H:i:s');

    // 父进程PID或者自身PID
    $top_pid = get_ppid();

    // 系统启动时间
    $sysuptime = get_sysuptime();

    // 进程启动时间
    $ppuptime = get_ppuptime($top_pid);

    // 由父进程ID确定变量文件路径前缀
    $path_pre = "{$process_file_dir}mp_counter_{$countername}_pid_{$top_pid}_";

    // 由于系统启动时间和当前父进程启动时间(jiffies格式)确定计数使用的文件
    $cur_path = "{$path_pre}btime_{$sysuptime}_ptime_{$ppuptime}";

    // 更新计数,先锁定
    $lock = sem_lock();

    if(!file_exists($cur_path)){
        // 调试代码。个别系统上启动时间会变化,造成文件路径跟随变化,最终导致计数归0。
        // $log = "[{$time}] - {$countername}($cur_path) - init\n";
        // file_put_contents('/tmp/process.log', $log, FILE_APPEND);

        $counter = 0;
    }else{
        // 理论上在这里,文件是一定存在的
        $counter = file_get_contents($cur_path);
    }

    // 更新记数, 继续研究下判断init不能用==
    if($update === 'init'){
        // 如果接收到更新值为init,或者变量文件不存在,则将计数初始化为0。
        $new_counter = 0;
    }else{
        $new_counter = $counter + $update;
    }

    // 写入计数,解锁
    file_put_contents($cur_path, $new_counter);
    sem_unlock($lock);

    return $new_counter;
}

/**
 * 创建多进程
 *
 * 1,通过mp_counter()函数实现进程间的任务协调
 * 2,由于PHP进程可能会由于异常而退出(主要是segment fault),并且由于处理内存泄露的问题需要子进程主动退出,本函数可以实现自动建立
 * 新的进程,使子进程数量始终保持在$num的数量
 * 3,编译PHP时需要使用参数--enable-pcntl安装所需的模块
 * 4,如果在子进程中调用了exit(9),那么主进程和所有子进程都将退出
 *
 * @param int       $num            进程数量
 * @param bool      $stat           结束后是否输出统计信息
 */
function multi_process($num, $stat=FALSE){
    global $colors, $signals;
    extract($colors);

    if(empty($num)){
        $num = 1;
    }

    // 记录进程数量,统计用
 mp_counter('process_num', 'init');
    mp_counter('process_num', $num);

    // 子进程数量
    $child = 0;

    // 任务完成标识
    $task_finish = FALSE;

    while(TRUE) {

        // 清空子进程退出状态
        unset($status);

        // 如果任务未完成,并且子进程数量没有达到最高,则创建
        if ($task_finish == FALSE && $child < $num) {
            $pid = pcntl_fork();
            if ($pid) {
                // 有PID,这里是父进程
                $child++;

                // 注册父进程的信号处理函数
                if($stat){
                    foreach ($signals as $signal => $name) {
                        if (!pcntl_signal($signal, "signal_handler")) {
                            die("Install signal handler for {$name} failed");
                        }
                    }
                }

                //$stat && pcntl_signal(SIGINT, "signal_handler");

                echo "{$reverse}{$green}[+]New Process Forked: {$pid}{$end}\n";
                mp_counter('t_lines', -1);
            } else {
                // fork后,子进程将进入到这里

                // 1,注册一个信号,处理函数直接exit(),目的是让子进程不进行任何处理,仅由主进程处理这个信号
                // 2,貌似不单独为子进程注册信号的话,子进程将使用父进程的处理函数
                $stat && pcntl_signal(SIGINT, "sub_process_exit");

                // 注册信号后直接返回,继续处理主程序的后续部分。
                return;
            }
        }

        // 子进程管理部分
        if($task_finish){
            // 如果任务已经完成
            if ($child > 0) {
                // 如果还有子进程未退出,则等待,否则退出
                pcntl_wait($status);
                $child--;
            } else {
                // 所有子进程退出,父进程退出

                // 统计信息
                $stat && final_stat();
    
    // 这里修改,父进程不退出,改为返回,继续处理后续任务,如删除文件
                //exit();
    return;
            }
        }else{
            // 如果任务未完成
            if($child >= $num){
                // 子进程已经达到数量,等待子进程退出
                pcntl_wait($status);
                $child--;
            }else{
                // 子进程没有达到数量,下一循环继续创建
            }
        }

        // 子进程退出状态码为9时,则判断为所有任务完成,然后等待所有子进程退出
        if(!empty($status) && pcntl_wexitstatus($status) == 9){
            $task_finish = TRUE;
        }
    }
}


/**
 * 检查同一脚本是否已经在运行,确保只有一个实例运行
 * @return bool
 */
function single_process(){
    if(get_ppid() !== getmypid()){
        echo "Fatal Error: Can't call single_process() in child process!\n";
        exit(9);
    }
    $self = get_path();
    $files = glob("/proc/*/exe");
    foreach($files as $exe_path){
        if(stripos(@readlink($exe_path), 'php') !== FALSE
            && stripos(readlink($exe_path), 'php-fpm') === FALSE){
            // 如果是PHP进程,进入到这里
            preg_match("/\/proc\/(\d+)\/exe/", $exe_path, $preg);
            if(!empty($preg[1]) && get_path($preg[1]) == $self && $preg[1] != getmypid()){
                exit("Fatal Error: This script is already running!\n");
            }
        }
    }
    return TRUE;
}


/**
 * 获取脚本自身的绝对路径,要求必须以php foo.php的方式运行
 * @param int $pid
 * @return string
 */
function get_path($pid=0){
    if($pid == 0){
        $pid = get_ppid();
    }
    $cwd = @readlink("/proc/{$pid}/cwd");
    $cmdline = @file_get_contents("/proc/{$pid}/cmdline");
    preg_match("/php(.*?\.php)/", $cmdline, $preg);
 if(empty($preg[1])){
  return FALSE;
 }else{
  $script = $preg[1];
 }
   

    if(strpos($script, '/') === FALSE || strpos($script, '..') !== FALSE){
        $path = "{$cwd}/{$script}";
    }else{
        $path = $script;
    }
    $path =  realpath(strval(str_replace("\0", "", $path)));
    if(!file_exists($path)){
        exit("Fatal Error: Can't located php script path!\n");
    }

    return $path;
}

function final_stat(){
    global $colors;
    extract($colors);

    // 时间统计
    global $start_time;
    $usetime = time() - $start_time;
    $usetime < 1 && $usetime = 1;
    $H = floor($usetime / 3600);
    $i = ($usetime / 60) % 60;
    $s = $usetime % 60;
    $str_usetime = sprintf("%02d hours, %02d minutes, %02d seconds", $H, $i, $s);
    echo "\n{$green}========================================================================\n";
    echo " All Task Done! Used Time: {$str_usetime}({$usetime}s).\n";

    // curl抓取统计
    $fetch_total = mp_counter('fetch_total', 0);
    $fetch_success = $fetch_total - mp_counter('fetch_failed', 0);

    $down_total = mp_counter('down_total', 0);
    $down_success = $down_total - mp_counter('down_failed', 0);

    $header_total = mp_counter('header_total', 0);
    $header_success = $header_total - mp_counter('header_failed', 0);

    $download_size = hs(mp_counter('download_size', 0));

    echo " Request Stat: Fetch({$fetch_success}/{$fetch_total}), Header({$header_success}/{$header_total}), ";
    echo "Download({$down_success}/{$down_total}, {$download_size}).\n";

    // curl流量统计
    $bw_in =  hs(mp_counter('download_total', 0));
    $rate_down = hbw(mp_counter('download_total', 0) / $usetime);
    echo " Bandwidth Stat(rough): Total({$bw_in}), Rate($rate_down).\n";

    // 效率统计
    $process_num = mp_counter('process_num', 0);
    $fetch_rps = hnum($fetch_success / $usetime);
    $fetch_rph = hnum($fetch_success * 3600 / $usetime);
    $fetch_rpd = hnum($fetch_success * 3600 * 24 / $usetime);
    echo " Efficiency: Process({$reverse}{$process_num}{$end}{$green}), Second({$fetch_rps}), ";
    echo "Hour({$fetch_rph}), Day({$reverse}{$fetch_rpd}{$end}{$green}).\n";

    echo "========================================================================{$end}\n";
}

/**
 * @param $signal
 */
function signal_handler($signal) {
    global $colors, $signals;
    extract($colors);
    if(array_key_exists($signal, $signals)){
        kill_all_child();
        echo "\n{$cyan}Ctrl + C caught, quit!{$end}\n";
        final_stat();
        exit();
    }
}

function sub_process_exit(){
    exit(9);
}

function hnum($num){
    if($num < 10){
        $res = round($num, 1);
    }elseif($num < 10000){
        $res = floor($num);
    }elseif($num < 100000){
        $res = round($num/10000, 1) . 'w';
    }else{
        $res = floor($num/10000) . 'w';
    }
    return $res;
}

/**
 * 人性化显示带宽速率
 *
 * @param $size   byte字节数
 * @return string
 */
function hbw($size) {
    $size *= 8;
    if($size > 1024 * 1024 * 1024) {
        $rate = round($size / 1073741824 * 100) / 100 . ' Gbps';
    } elseif($size > 1024 * 1024) {
        $rate = round($size / 1048576 * 100) / 100 . ' Mbps';
    } elseif($size > 1024) {
        $rate = round($size / 1024 * 100) / 100 . ' Kbps';
    } else {
        $rate = round($size) . ' Bbps';
    }
    return $rate;
}


/**
 * 人性化显示数据量
 *
 * @param $size
 * @return string
 */
function hs($size) {
    if($size > 1024 * 1024 * 1024) {
        $size = round($size / 1073741824 * 100) / 100 . ' GB';
    } elseif($size > 1024 * 1024) {
        $size = round($size / 1048576 * 100) / 100 . ' MB';
    } elseif($size > 1024) {
        $size = round($size / 1024 * 100) / 100 . ' KB';
    } else {
        $size = round($size) . ' Bytes';
    }
    return $size;
}

/**
 * 杀死所有子进程
 */
function kill_all_child(){
    $ppid = getmypid();
    $files = glob("/proc/*/stat");
    foreach($files as $file){
        if(is_file($file)){
            $sections = explode(' ', file_get_contents($file));
            if($sections[3] == $ppid){
                posix_kill($sections[0], SIGTERM);
            }
        }
    }
}

if(!function_exists('get_ppid')){
    function get_ppid(){
        // 这里需要识别出是在子进程中调用还是在父进程中调用,不同的形式,保存的变量内容的文件位置需要保持一致
        $ppid = posix_getppid();
        // 理论上,这种判断方式可能会出坑。但在实际使用中,除了fork出的子进程外,不太可能让PHP进程的父进程的程序名中出现php字样。
        if(strpos(readlink("/proc/{$ppid}/exe"), 'php') === FALSE){
            $pid = getmypid();
        }else{
            $pid = $ppid;
        }
        return $pid;
    }
}

// 以进程(多进程运行时,使用父进程)为单位,每个进程使用一个锁。
function sem_lock($lock_name=NULL){
    global $process_file_dir;
    $pid = get_ppid();
    if(empty($lock_name)){
        $lockfile = "{$process_file_dir}sem_keyfile_main_pid_{$pid}";
    }else{
        $lockfile = "{$process_file_dir}sem_keyfile_{$lock_name}_pid_{$pid}";
    }
    if(!file_exists($lockfile)){
        touch($lockfile);
    }
    $shm_id = sem_get(ftok($lockfile, 'a'), 1, 0600, true);
    if(sem_acquire($shm_id)){
        return $shm_id;
    }else{
        return FALSE;
    }
}

// 解除锁
function sem_unlock($shm_id){
    sem_release($shm_id);
}

// 清理资源(文件和SEM信号锁)
function clear_process_resource(){
    global $process_file_dir;

    // 清除sem的文件和信号量
    $files = glob("{$process_file_dir}sem_keyfile*pid_*");
    foreach($files as $file){
        preg_match("/pid_(\d*)/", $file, $preg);
        $pid = $preg[1];
        $exe_path = "/proc/{$pid}/exe";
        // 如果文件不存在则说明进程不存在,判断是否为PHP进程,排除php-fpm进程
        if(!file_exists($exe_path)
            || stripos(readlink($exe_path), 'php') === FALSE
            || stripos(readlink($exe_path), 'php-fpm') === TRUE){
   $sem = @sem_get(@ftok($file, 'a'));
   if($sem){
    @sem_remove($sem);
   }
            @unlink($file);
        }
    }

    // 清除mp_counter的文件(仅此类型文件不可重用,所以严格处理,匹配系统启动时间和进程启动时间)
    $files = glob("{$process_file_dir}mp_counter*");
    foreach($files as $file){
        preg_match("/pid_(\d*)_btime_(\d*)_ptime_(\d*)/", $file, $preg);
        $pid = $preg[1];
        $btime = $preg[2];
        $ptime = $preg[3];
        $exe_path = "/proc/{$pid}/exe";

        // 清除文件
        if(!file_exists($exe_path)
            || stripos(readlink($exe_path), 'php') === FALSE
            || stripos(readlink($exe_path), 'php-fpm') === TRUE
            || $btime != get_sysuptime()
            || $ptime != get_ppuptime($pid)){
            @unlink($file);
        }
    }
}

// 系统启动时间
function get_sysuptime(){
    preg_match("/btime (\d+)/", file_get_contents("/proc/stat"), $preg);
    return $preg[1];
}

// 如果是在子进程中调用,则取父进程的启动时间。如果不是在子进程中调用,则取自身启动时间。时间都是jiffies格式。
function get_ppuptime($pid){
    $stat_sections = explode(' ', file_get_contents("/proc/{$pid}/stat"));
    return $stat_sections[21];
}

// 防止PHP进程内存泄露,每个子进程执行完一定数量的任务就退出。
function rand_exit($num=100){
    if(rand(floor($num*0.5), floor($num*1.5)) === $num){
        exit();
    }
}

// 单次的任务结果输出函数
function mp_msg(){
    global $start_time, $colors;
    extract($colors);

    // 整理统计信息
    $msg = date('[H:i:s]');
    $max = 0;
    $msg_array = func_get_args();
    foreach($msg_array as $key=>$val){
        $val = preg_replace("/\s{2,}/", ' ', $val);
        $msg_array[$key] = $val;
        if(is_int($key)){
            $msg .= " $val";
        }else{
            $msg .= " {$key}:$val";
        }
        if(strlen($val) > strlen($msg_array[$max])){
            $max = $key;
        }
    }

    // cron方式运行
    if(empty($_SERVER['SSH_TTY'])){
        $msg = preg_replace("/\\\33\[\d\dm/", '', $msg);
        echo "{$msg}\n";
        return;
    }

    $lock = sem_lock('mp_msg');
    $t_lines = mp_counter('t_lines', -1);
    if($t_lines <= 1){
        mp_counter('t_lines', 'init');
        mp_counter('t_lines', shell_exec('tput lines'));
        mp_counter('t_cols', 'init');
        mp_counter('t_cols', shell_exec('tput cols'));
    }
    sem_unlock($lock);

    $t_cols = mp_counter('t_cols', 0);
    $msg_len = strlen($msg);
    if($msg_len > $t_cols){
        $cut_len = strlen($msg_array[$max]) - ($msg_len - $t_cols);
        $msg = str_replace($msg_array[$max], substr($msg_array[$max], 0, $cut_len), $msg);
    }
    echo "{$msg}\n";

    if($t_lines <= 1){
        $usetime = time() - $start_time;
        $usetime < 1 && $usetime = 1;
        $H = floor($usetime / 3600);
        $i = ($usetime / 60) % 60;
        $s = $usetime % 60;
        $str_usetime = sprintf("%02d:%02d:%02d", $H, $i, $s);

        $process_num = mp_counter('process_num', 0);

        $fetch_total = mp_counter('fetch_total', 0);
        $fetch_success = $fetch_total - mp_counter('fetch_failed', 0);
        $fetch = hnum($fetch_success);
        $fetch_all = hnum($fetch_total);
        $fetch_rpd = hnum($fetch_success * 3600 * 24 / $usetime);

        echo "{$reverse}{$purple}";
        echo "Stat: Time({$str_usetime}) Process({$process_num}) Fetch({$fetch}/{$fetch_all}) Day({$fetch_rpd})";
        echo "{$end}\n";
        flush();
    }

}

 

日志切割我们很少使用php来开发了,因为php处理这些来非常的慢了,不过日志小的话或直接在网站上查看日志时这个就起到作用了,下面来看一个php系统日志切割的实例,具体如下。

我习惯设置的日志路径是这样
/home/www/logs/域名.log
比如
/home/www/logs/www.yundaiwei.com.log

为了方便管理,日志需要按天保存在一个文件中,并且保留指定天数的日志,超过时间的就删除。

分享一下脚本

#!/usr/bin/php
<?php
$logdir = '/home/www/logs/';
// 保留天数含当天
$log_save_day = 7;

$files = glob("{$logdir}/*");

foreach($files as $path){
    $filename = basename($path);
    preg_match("/(\d{8})\.log/", $filename, $preg);

    $date = @$preg[1];
   
    if(empty($date)){
        // 当天日志,更改文件名
        $newpath = $logdir . '/' . str_replace('log', date('Ymd',strtotime("-1 day")).'.log', $filename);
        rename($path, $newpath);
        echo "$path >>> $newpath\n";
    }else{
        // 超过保留天数,删除
        if(time()+10 - strtotime($date) > 3600*24*$log_save_day){
            unlink($path);
            echo "$path delete!\n";
        }
    }
}

shell_exec('/etc/init.d/nginx reload &> /dev/null');

下面我们来看一篇关于 提交PHP组件到Packagist 发布自己的Composer包的教程,希望这篇文章能够帮助到各位朋友哦。

Composer是PHP的一个依赖管理工具,它使得PHP焕发新的生机,有了现代化的WEB开发规范,Packagist是PHP组件的库,也有其他的镜像。

在Packagist上提交了一个自己开发的PHP组件,这样其他开发者就可以使用Composer使用这个包了。这个组件并没什么功能,主要是看看提交PHP组件的流程,并记录了过程中遇到的问题及解决方法,以供参考。


提交PHP组件步骤:

1.新建一个项目目录,创建一个composer.json文件,格式如下:

PHP

{
    "name": "your-vendor-name/package-name",
    "description": "A short description of what your package does",
    "require": {
        "php": "^5.3.3 || ^7.0",
        "another-vendor/package": "1.*"
    }
}


这个json格式的文件中包含组件的基本信息,这里还差自动加载的方式,要根据具体开发模式指定自动加载方式,这里require可以指定这个组件依赖的其他组件,composer都会自动解决依赖。

附上完整的composer.json内容作为示例:

JavaScript

{
    "name": "tanteng/laravel-page-loaded-time",
    "description": "Display page loaded time",
    "keywords": ["laravel","performance"],
    "homepage": "https://github.com/tanteng/laravel-page-loaded-time",
    "license": "MIT",
    "authors": [
      {
        "name": "tanteng",
        "email": "tanteng@qq.com",
        "homepage": "http://www.tanteng.me",
        "role": "Developer"
      }
    ],
    "require": {
        "php": "^5.3.3 || ^7.0",
        "laravel/framework": "~4.0"
    },
    "autoload": {
      "psr-4": { "Loading\\": "" }
    }
}

2.开发组件功能

要注意遵循psr规范,使用命名空间。

3.把组件提交到Github上

提交组件到Packagist之前需要先把代码提交到github上,在Packagist只需填写组件的github地址。

4.提交组件地址到Packagist

这样就完成的PHP组件提交到Packagist的过程,具体请参见Packagist官网。

问题:使用composer require找不到组件

组件提交到Packagist上,提示发布成功了,但是使用composer命令却找不到组件:

PHP

composer require tanteng/laravel-page-loaded-time

如图:

composer-not-found

由于我的composer使用的国内镜像,猜测可能是没有同步的原因,使用这个命令把“源”改回去还是不行。

PHP

composer config -g repo.packagist composer https://packagist.org

原来我的组件还没有在github上发布正式,这个时候还是开发版本dev-master.应该加上dev-master版本。

PHP

composer require tanteng/laravel-page-loaded-time:dev-master

果然指定了dev-master版本可以成功更新组件,但是这样不行,我们要有一个正式版本。

composer-dev-master

github发布版本

进入组件的github主页,找到导航上“releases”,点击进去如图页面,就可以创建一个版本,填写好信息之后即可发布版本。

create_release

按照页面上的提示填写内容,完成后发布。发布版本后,通过composer require发现还是找不到包。

设置自动更新版本

auto-update

原来还要在Github上配置一下自动更新。具体步骤参考:https://packagist.org/about#how-to-update-packages

我直接通过手动的方式发送curl请求来设置,这样还简单一点,不过这样每次发新的版本都需要这样请求一下:

PHP

curl -XPOST -H'content-type:application/json' 'https://packagist.org/api/update-package?username=tanteng&apiToken=secret' -d'{"repository":{"url":"https://github.com/tanteng/laravel-page-loaded-time"}}'

返回{“status”:”success”}表示成功。

再打开https://packagist.org/packages/tanteng/laravel-page-loaded-time,发现已经是正式版本了。

我用的是composer国内镜像,因为众所周知的原因,连代码仓库也要被墙,我服!等了几个小时再试,这个时候镜像同步更新了,再次输入:composer require tanteng/laravel-page-loaded-time,这个时候可以成功更新了。如图,vendor文件夹下也自动装载了依赖的其他组件

命名规则其实还是有一点的要求会比较好,否则不统计也难看了,下面整理了一篇关于PHP变量命名规则详解,希望下面的文章对各位有用。

PHP变量命名规则

1、变量以美元符号$开头。如$name,$age。

2、美元符号$后面的第一个字符不可以是数字,只能是下划线_或者字母。如$1_1这样的变量是错误的。

3、除了下划线_外,变量不允许出现任何空格或标点符号。也就是说变量名只能包含:a-z、A-Z、0-9 以及下划线_。

4、PHP变量名是区分大小写的。如$name与$Name是两个不同的变量。

PHP变量命名规则其余注意事项

1、当用两个或两个以上的单词命名变量时,可以将除第一个单词以外的所有单词的首字母大写。如$myName、$yourFamilyName。

2、以下划线_开始命名的变量通常代表特殊的变量。如在类中创建受保护的属性、PHP预定义变量($_GET)、全局数组等。

3、定义变量的时候,不要贪图简短,而应该使用具有描述性的名称定义变量。

学而不思则罔,思而不学则殆。希望您在学习PHP的道路上一帆风顺,并成为PHP编程艺术大师。


以上便是PHP变量命名规则的全部内容。以下是废话,如果您有时间,不妨粗略阅读,也许您能产生共鸣。

PHP变量命名规则

从大学开始到现在,陆陆续续接触过各种编程语言。如客户端语言Javascript,Actionscript;服务器端语言C++,Java,ASP.net,PHP等等,不一而足。在所有这些语言中,我最想学的是PHP。不为别的,就因为PHP的读法:屁H屁,说起来琅琅上口,韵味十足。今天看到PHP变量命名规则这一节,索性就随便写一点东西,聊以打发无聊的光阴。

对PHP的学习,一直时断时续。年轻时候要学好PHP的雄心壮志如今差不多荡然无存了。

有人说,活在这样一个社会里,没有人能一尘不染地活着。这是个怎样的社会?我不太清楚。不过我听人说,这是个聋子听见哑巴说瞎子看见了爱情的扯淡时代。不完全信,但也无法完全不信。

不管怎样,既然活着是不容易的,那么就且活且珍惜吧。

PHP是我想珍惜的。

于是,今天翻了一翻从图书馆借来的新书——PHP动态网页设计(第二版,作者David Powers)。

看到PHP变量命名规则一节。这些非常基础的内容,在时隔数年之后重新温习,倒是真有温故而知新的感觉。

为了使印象更为深刻,也为了实践,好记性不如烂笔头的真理,在这不太安分的夜里,我决定写这篇文章,献给曾经想成为PHP高手的自己。

1、变量以美元符号$开头。如$name,$age;

2、美元符号$后面的第一个字符不可以是数字;

3、除了下划线_外,变量不允许出现任何空格或者标点符号;

4、PHP变量名是区分大小写的。如$name与$Name是两个不同的变量。

其余注意事项

1、当用两个或两个以上的单词命名变量时,可以将除第一个单词以外的所有单词的首字母大写。如$myName;$yourFamilyName;

2、以下划线_开始命名的变量通常留给特殊的情况使用。如在类中创建受保护的属性;PHP预定义变量:全局数组等;

3、定义变量的时候,不要贪图简短,而应该使用具有描述性的名称定义变量。


例子

面是分别用骆驼式命名法和下划线法命名的同一个函数:

printEmployeePaychecks();
print_employee_paychecks();

第一个函数名使用了骆驼式命名法——函数名中的每一个逻辑断点都有一个大写字母来标记;第二个函数名使用了下划线法----函数名中的每一个逻辑断点都有一个下划线来标记。
骆驼式命名法近年来越来越流行了,在许多新的函数库和Microsoft Windows这样的环境中,它使用得相当多。另一方面,下划线法是c出现后开始流行起来的,在许多旧的程序和UNIX这样的环境中,它的使用非常普遍。

编辑本段应用概述

骆驼式命名法(Camel-Case)是电脑程式编写时的一套命名规则(惯例)。
骆驼式命名法就是当变量名或函式名是由一个或多个单字连结在一起,而构成的唯一识别字时,第一个单词以小写字母开始;第二个单词的首字母大写或每一个单词的首字母都采用大写字母,例如:myFirstName、myLastName,这样的变量名看上去就像骆驼峰一样此起彼伏,故得名。
骆驼式命名法(Camel-Case)一词来自 Perl 语言中普遍使用的大小写混合格式,而 Larry Wall 等人所著的畅销书《Programming Perl》(O'Reilly 出版)的封面图片正是一匹骆驼。
骆驼式命名法的命名规则可视为一种惯例,并无绝对与强制,为的是增加识别和可读性。

驼峰法(小驼峰法)

变量一般用小驼峰法标识。驼峰法的意思是:除第一个单词之外,其他单词首字母大写。譬如
int myStudentCount;
变量myStudentCount第一个单词是全部小写,后面的单词首字母大写。

Pascal法(大驼峰法)

相比小驼峰法,大驼峰法把第一个单词的首字母也大写了。常用于类名,函数名,属性,命名空间。譬如
publicclass DataBaseUser;

[!--infotagslink--]

相关文章

  • Mysql效率优化定位较低sql的两种方式

    关于mysql效率优化一般通过以下两种方式定位执行效率较低的sql语句。通过慢查询日志定位那些执行效率较低的 SQL 语句,用 --log-slow-queries[=file_name] 选项启动时, mysqld 会 写一个包含所有执行时间超过 long_quer...2015-11-08
  • 如何用Node.js编写内存效率高的应用程序

    这篇文章主要介绍了如何用Node.js编写内存效率高的应用程序,对Node.js感兴趣的同学,可以参考下...2021-05-01
  • python基于opencv检测程序运行效率

    这篇文章主要介绍了python基于opencv检测程序运行效率,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-05-09
  • mysql事务对效率的影响分析总结

    在本篇文章里小编给大家分享的是一篇关于mysql事务对效率的影响分析总结内容,有需要的朋友们可以跟着学习下。...2021-10-24
  • JavaScript数组去重的几种方法效率测试

    JavaScript数组去重是前端面试酷爱的问题,问题简单而又能看出程序员对计算机程序执行过程的理解如何。数组去重的方法有很多,到底哪种是最理想的我不清楚。于是我测试了下数组去重的效率。测试二十万个数据,随着数据越多效率很明显的就体验了出来。下面来一起看看吧。...2016-10-25
  • php安装pcntl扩展实现多进程

    pcntl中的php必须要安装pcntl才可以实现多线程了,在网上找到许多的关于pcntl安装教程,下面整理了一篇比较完整的关于php pcntl安装与使用方法。 pcntl中php实现多进...2016-11-25
  • Redis高效率原因及数据结构分析

    这篇文章主要为大家详细的介绍了Redis高效的原因以及分析了Redis高效的数据结构,有需要的朋友可以借鉴参考下,希望能够有所帮助...2021-09-27
  • python 详解如何使用GPU大幅提高效率

    CuPy是一个开源矩阵库,使用NVIDIA CUDA加速。CuPy使用Python提供GPU加速计算。CUPY使用CUDA相关库,包括 CuBLAS、CUDNN、Curand、CuoSver、CuPaSeSE、Cufft和NCCL,以充分利用GPU架构...2021-11-10
  • MySql数据库单表查询与多表连接查询效率对比

    在遇到数据之间的联系很复杂,建表就很纠结,到底该怎么去处理这些复杂的数据呢,是单表查询,然后在业务层去处理数据间的关系,还是直接通过多表连接查询来处理数据关系呢...2021-09-27
  • 使用C# CefSharp Python采集某网站简历并且自动发送邀请短信的方法

    这篇文章主要给大家介绍了关于如何使用C# CefSharp Python采集某网站简历并且自动发送邀请短信的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起看看吧...2020-06-25
  • php中 strtr 和 str_replace 的效率问题

    下面小编就为大家带来一篇关于php中 strtr 和 str_replace 的效率问题。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧 在网上看了...2017-07-06
  • 音乐采集程序

    <?php header("Content-Type: text/html; charset=gb2312"); session_start(); set_time_limit(0); $str = file_get_contents('http://music.soso.com/'); pr...2016-11-25
  • 如何采集搜狗微信搜索的内容(SogouEncrypt版) -- hzw

    在搜狗微信搜索中,之前微信的内容是用 http://weixin.sogou.com/gzhjs?cb=sogou.weixin.gzhcb&openid=oIWsFt1OaL2XHiDx6809O8q2KZ5A&page=1 这类格式来调用数据的,openid...2016-05-19
  • PHP CLI模式下PCNTL扩展实现多进程服务的例子

    我们一起来看一个PHP CLI模式下PCNTL扩展实现多进程服务的例子,有面要了解这个问题的朋友可以和小编一起来看看。 PHP可通过PCNTL扩展实现进程控制,如进程创建,信...2016-11-25
  • 几点提高PHP序运行效率的方法

    1、用i =1代替i=i 1。符合c/c 的习惯,效率还高。 2、尽可能的使用PHP内部函数。自己编写函数之前要具体查阅手册,看有没有相关的函数,否则费力不讨好。 3、能使用单引号...2016-11-25
  • asp.net采集网页图片的具体方法

    采集网页上图片的主要关键是在怎么解析出页面代码里那些img标签的src属性...2021-09-22
  • 利用fopen函数采集新闻页面内容保存到本地函数

     <? / / PHP的新闻抓取由Neil Moomey,。 / /你可以自由的使用此代码作为您的愿望。 / /请确保您可以从任何网站,你抓从标题许可。 / /你可能需要写上您的服务器上...2016-11-25
  • pytorch 限制GPU使用效率详解(计算效率)

    这篇文章主要介绍了pytorch 限制GPU使用效率详解(计算效率),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-06-28
  • 多线程如何解决for循环效率的问题

    这篇文章主要介绍了多线程如何解决for循环效率的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-06-17
  • IDEA提高开发效率的7个插件(推荐)

    这篇文章主要介绍了IDEA提高开发效率的7个插件(推荐),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-07-10