HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
Let's Encrypt,是2016年4月12日成立的一家证书授权中心,提供免费的传输层安全(TLS)X.509证书,通过自动化的过程消除目前安全网站证书需要手工创建,加密,签名,安装以及更新的复杂性。
Let's Encrypt,是2016年4月12日成立的一家证书授权中心,提供免费的传输层安全(TLS)X.509证书,通过自动化的过程消除目前安全网站证书需要手工创建,加密,签名,安装以及更新的复杂性。
应用场景
某些情况下,我们需要持续的周期性的提供一些服务,比如监控内存或cpu的运行状况,这些应用与客户端是没有关系的,不是说客户端(如web界面,手机app等)关闭了,我们就不监控内存或cpu了,为了应对这种业务场景,后台守护进程就可以派上用场了。
某些情况下,我们需要持续的周期性的提供一些服务,比如监控内存或cpu的运行状况,这些应用与客户端是没有关系的,不是说客户端(如web界面,手机app等)关闭了,我们就不监控内存或cpu了,为了应对这种业务场景,后台守护进程就可以派上用场了。
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。php也可以实现守护进程的功能。
1、基本概念
进程
每个进程都有一个父进程,子进程退出,父进程能得到子进程退出的状态。
1、基本概念
进程
每个进程都有一个父进程,子进程退出,父进程能得到子进程退出的状态。
PHP curl实现多进程并发抓取数据我们经常的用到了,今天我们来看一篇关于PHP curl实现多进程并发高效率采集爬虫的例子,具体的细节如下。
主要封装函数
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中的绿色行)。
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");
主要封装函数
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中的绿色行)。
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");
安装说明
安装PHP扩展 trie_filter,安装教程 http://blog.41ms.com/post/39.html
安装PHP扩展 swoole,安装教程 http://www.swoole.com/
安装PHP扩展 trie_filter,安装教程 http://blog.41ms.com/post/39.html
安装PHP扩展 swoole,安装教程 http://www.swoole.com/
安装 libdatrie , 需要 libdatrie-0.2.4 或更新的版本
它依赖 libiconv .
安装:
================================
wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.14.tar.gz
tar -zxf libiconv-1.14.tar.gz
cd libiconv-1.14
./configure
make && make install
libdatrie 下载地址:
http://linux.thai.net/~thep/datrie/datrie.html#Download
安装:
================================
wget ftp://linux.thai.net/pub/ThaiLinux/software/libthai/libdatrie-0.2.4.tar.gz
tar -zxf libdatrie-0.2.4.tar.gz
cd libdatrie-0.2.4
./configure --prefix=/usr/local/libdatrie/
make ICONV_LIBS='/usr/local/lib/libiconv.so'
make install
编译出现错误 trietool.c:125: undefined reference to `libiconv'
解决办法为:./configure LDFLAGS=-L/usr/local/lib LIBS=-liconv
安装 PHP 扩展
================================
wget https://github.com/wulijun/php-ext-trie-filter/archive/master.zip
unzip master.zip
cd php-ext-trie-filter-master/
/usr/local/php/bin/phpize
./configure --with-php-config=/usr/local/php/bin/php-config --with-trie_filter=/usr/local/libdatrie/
make && make install
将生成的 trie_filter.so 文件复制到 php 扩展目录,并在 php.ini 中添加该文件
修改 php.ini 文件,添加 trie_filter 扩展:extension=trie_filter.so,重启PHP。
查看phpinfo发现trie_filter 扩展可用,如下图所示:
https://github.com/wulijun/php-ext-trie-filter
=============遇到的问题===============
安装libiconv如果遇到下面的问题:
In file included from progname.c:26:0:
./stdio.h:1010:1: error: 'gets' undeclared here (not in a function)
_GL_WARN_ON_USE (gets, "gets is a security hole - use fgets instead");
^
make[2]: *** [progname.o] Error 1
make[2]: Leaving directory `/usr/local/directadmin/custombuild/libiconv-1.14/srclib'
make[1]: *** [all] Error 2
make[1]: Leaving directory `/usr/local/directadmin/custombuild/libiconv-1.14/srclib'
make: *** [all] Error 2
解决方案:
cd /tmp/libiconv-1.14/srclib
vim stdio.in.h
找到这行内容:
_GL_WARN_ON_USE (gets, "gets is a security hole - use fgets instead");
替换成:
#if defined(__GLIBC__) && !defined(__UCLIBC__) && !__GLIBC_PREREQ(2, 16)
_GL_WARN_ON_USE (gets, "gets is a security hole - use fgets instead");
#endif
注意:结尾的#endif也要包括。
安装PHP如果遇到下面的问题:
/usr/include/stdio.h:325:14: note: expected ‘size_t *’ but argument is of type ‘zend_size_t *’
extern FILE *open_memstream (char **__bufloc, size_t *__sizeloc) __THROW __wur;
^
/root/soft/soft/php-ext-trie-filter/trie_filter.c:566:27: error: macro "RETVAL_STRINGL" requires 3 arguments, but only 2 given
RETVAL_STRINGL(p, plen);
^
/root/soft/soft/php-ext-trie-filter/trie_filter.c:566:5: error: ‘RETVAL_STRINGL’ undeclared (first use in this function)
RETVAL_STRINGL(p, plen);
^
解决方案:
vi php-ext-trie-filter/trie_filter.c
修改:
RETVAL_STRINGL(p, plen);
中PHP7这个宏定义只需要两个参数,
而PHP5需要三个
#if PHP_MAJOR_VERSION < 7
RETVAL_STRINGL(p, plen,0);
#else
RETVAL_STRINGL(p, plen);
#endif
解决
它依赖 libiconv .
安装:
================================
wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.14.tar.gz
tar -zxf libiconv-1.14.tar.gz
cd libiconv-1.14
./configure
make && make install
libdatrie 下载地址:
http://linux.thai.net/~thep/datrie/datrie.html#Download
安装:
================================
wget ftp://linux.thai.net/pub/ThaiLinux/software/libthai/libdatrie-0.2.4.tar.gz
tar -zxf libdatrie-0.2.4.tar.gz
cd libdatrie-0.2.4
./configure --prefix=/usr/local/libdatrie/
make ICONV_LIBS='/usr/local/lib/libiconv.so'
make install
编译出现错误 trietool.c:125: undefined reference to `libiconv'
解决办法为:./configure LDFLAGS=-L/usr/local/lib LIBS=-liconv
安装 PHP 扩展
================================
wget https://github.com/wulijun/php-ext-trie-filter/archive/master.zip
unzip master.zip
cd php-ext-trie-filter-master/
/usr/local/php/bin/phpize
./configure --with-php-config=/usr/local/php/bin/php-config --with-trie_filter=/usr/local/libdatrie/
make && make install
将生成的 trie_filter.so 文件复制到 php 扩展目录,并在 php.ini 中添加该文件
修改 php.ini 文件,添加 trie_filter 扩展:extension=trie_filter.so,重启PHP。
查看phpinfo发现trie_filter 扩展可用,如下图所示:
https://github.com/wulijun/php-ext-trie-filter
=============遇到的问题===============
安装libiconv如果遇到下面的问题:
In file included from progname.c:26:0:
./stdio.h:1010:1: error: 'gets' undeclared here (not in a function)
_GL_WARN_ON_USE (gets, "gets is a security hole - use fgets instead");
^
make[2]: *** [progname.o] Error 1
make[2]: Leaving directory `/usr/local/directadmin/custombuild/libiconv-1.14/srclib'
make[1]: *** [all] Error 2
make[1]: Leaving directory `/usr/local/directadmin/custombuild/libiconv-1.14/srclib'
make: *** [all] Error 2
解决方案:
cd /tmp/libiconv-1.14/srclib
vim stdio.in.h
找到这行内容:
_GL_WARN_ON_USE (gets, "gets is a security hole - use fgets instead");
替换成:
#if defined(__GLIBC__) && !defined(__UCLIBC__) && !__GLIBC_PREREQ(2, 16)
_GL_WARN_ON_USE (gets, "gets is a security hole - use fgets instead");
#endif
注意:结尾的#endif也要包括。
安装PHP如果遇到下面的问题:
/usr/include/stdio.h:325:14: note: expected ‘size_t *’ but argument is of type ‘zend_size_t *’
extern FILE *open_memstream (char **__bufloc, size_t *__sizeloc) __THROW __wur;
^
/root/soft/soft/php-ext-trie-filter/trie_filter.c:566:27: error: macro "RETVAL_STRINGL" requires 3 arguments, but only 2 given
RETVAL_STRINGL(p, plen);
^
/root/soft/soft/php-ext-trie-filter/trie_filter.c:566:5: error: ‘RETVAL_STRINGL’ undeclared (first use in this function)
RETVAL_STRINGL(p, plen);
^
解决方案:
vi php-ext-trie-filter/trie_filter.c
修改:
RETVAL_STRINGL(p, plen);
中PHP7这个宏定义只需要两个参数,
而PHP5需要三个
#if PHP_MAJOR_VERSION < 7
RETVAL_STRINGL(p, plen,0);
#else
RETVAL_STRINGL(p, plen);
#endif
解决
先写个结论,sphinx本身是可以支持中文搜索的,只是不支持中文分词,需要安装中文分词插件,coreseek就是一个打包了mmseg中文分词插件和sphinx源码的安装包。
概念讲解
Sphinx全文搜索引擎
一般用作大数据量的搜索支持
Sphinx特性
1:Sphinx支持高速建立索引(可达10MB/秒,而Lucene建立索引的速度是1.8MB/秒)
概念讲解
Sphinx全文搜索引擎
一般用作大数据量的搜索支持
Sphinx特性
1:Sphinx支持高速建立索引(可达10MB/秒,而Lucene建立索引的速度是1.8MB/秒)
1.安装libiconv,这个是libdatrie的依赖项
wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.14.tar.gz
tar zxvf libiconv-1.14.tar.gz
cd libiconv-1.14
./configure
make
make install
wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.14.tar.gz
tar zxvf libiconv-1.14.tar.gz
cd libiconv-1.14
./configure
make
make install
转自:http://flyer0126.iteye.com/blog/1931212
早就想研究下敏感词过滤问题,今天有些时间,正好实践一下。
1. 安装:libdatrie (http://linux.thai.net/~thep/datrie/datrie.html#Download)
早就想研究下敏感词过滤问题,今天有些时间,正好实践一下。
1. 安装:libdatrie (http://linux.thai.net/~thep/datrie/datrie.html#Download)
关键词过滤扩展,用于检查一段文本中是否出现敏感词,基于Double-Array Trie 树实现。
安装步骤
下面的$LIB_PATH为依赖库安装目录,$INSTALL_PHP_PATH为PHP5安装目录。
安装步骤
下面的$LIB_PATH为依赖库安装目录,$INSTALL_PHP_PATH为PHP5安装目录。