2014年的第一缕阳光终于在2月27日早上照在了北京的街道上, 心情豁然开朗, 写个博客 以记录此重大事件.
说并发之前,先说防*吧.
近期工作中需要采集一些其它网站的数据(对同行兄弟表示歉意).
1. 先用了 file_get_contents ,结果 根本得不到任何内容.
2.换curl ,一点问题都没有,内容获取成功. 但 很快就被对方屏蔽了, 导致 整个公司 的同事都无法访问 目标网站. 由此可见,目标网站是根据访问者的IP进行的*.
3.使用代理,这个CURL支持. 上网搜了一堆代理服务器的地址(几千个),
4.程序中轮换使用随机代理, 但有些代理不好用, 也没办法手工剔除. 解决方法: 所有代理地址写到数据库的表中, 根据抓取成功与失败, 剔除掉不好用的服务器.
5.随机轮换UserAgent.
6.完成以上后,采集是没问题了,但速度 不满意, 主要时间都在网络上(从代理访问目标网站). 开始研究并发.
7.查了一下资料,也不难.
8.并发数是个问题, 根据个人的电脑和网络情况吧.
说那么多,开始贴代码
class LSynCatch {
//默认的超时设置 10 秒
private static $timeout=10;
//构造一个连接句柄
public static function curlInit($url){
$ch = curl_init ( $url );
//随机取一个用户代理, 此变量请自行添加
$agent=self::$agents[rand(0,count(self::$agents)-1)];
//设置CURL参数
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER=>true, //要求返回结果
CURLOPT_TIMEOUT=>self::$timeout,//超时
CURLOPT_USERAGENT=> $agent, //用户代理
CURLOPT_REFERER=>‘‘, //上一次页面
CURLOPT_COOKIE=>‘‘, //COOKIE 无
CURLOPT_FOLLOWLOCATION=>false, //不自动 跳转
//以下是Header,用FireBug之类的抓取一个正常请求的Header数据就可以
CURLOPT_HTTPHEADER=>array (
//.....略
),
));
return $ch;
}
//全部请求地址的栈
private $urlStack=array();
//入栈
public function push(array $info){
array_push($this->urlStack, $info);
}
//全部入栈完成后,开始爬行
public function run(){
$chs = curl_multi_init();
$map = array();
//初始20个并发, 根据网络情况自行修改
while(count($map)<20){
$info=array_pop($this->urlStack);
$url=$info[‘url‘];
$ch=self::curlInit($url);
//代理的类,请根据上述原则行开发
$info[‘proxy‘]=LProxy::get();
curl_setopt($ch, CURLOPT_PROXY, $info[‘proxy‘]);
curl_multi_add_handle($chs, $ch);
$map[strval($ch)] = $info;
}
//同时发起网络请求,持续查看运行状态
do{
$status = curl_multi_exec($chs, $active);
if ($status == CURLM_CALL_MULTI_PERFORM) {
continue;
}
if ($status != CURLM_OK) {
continue;
} //如果没有准备就绪,就再次调用curl_multi_exec
//终于有请求完成的
while ($done = curl_multi_info_read($chs)) {
$ch=$done[‘handle‘];
$params = $map[strval($ch)];
$params[‘info‘] = curl_getinfo($ch);
$params[‘errno‘]=curl_errno($ch);
$params[‘error‘] = curl_error($ch);
$params[‘code‘]=curl_getinfo($ch,CURLINFO_HTTP_CODE);
$params[‘result‘] = curl_multi_getcontent($ch);
//请求出错了,应该是代理服务器的错,换代理
if($params[‘errno‘]){
LProxy::failure($params[‘proxy‘]);
if($params[‘errno‘]==28){
$error=‘timeout of ‘.self::$timeout.‘ s‘;
}else{
$error=$params[‘error‘];
}
echo ‘URL : ‘.$params[‘url‘]."\r\n";
echo ‘Curl errno:‘.$params[‘errno‘].‘ Curl error: ‘ . $error."\r\n";
continue;
}
//目标网站出错,如: 302,504,404之类
if($params[‘code‘]==0){
echo ‘URL : ‘.$params[‘url‘]."\r\n";
echo ‘Code : 0 Length : ‘.strlen($params[‘result‘])."\r\n";
continue;
}
//本次抓取成功
LProxy::success($params[‘proxy‘]);
echo ‘URL : ‘.$params[‘url‘]."\r\n";
echo ‘Http Code : ‘.$params[‘code‘]. " \tUsed : ".round($params[‘info‘][‘total_time‘],2)." \tLength : ".strlen($params[‘result‘] )."\r\n";
//调用 回调方法,对采集的内容进行处理
if ($params[‘callback‘]) {
$params[‘callback‘]($params);
}
//从并发中去除此句柄
curl_multi_remove_handle($chs, $ch);
curl_close($ch);
//从栈里再拿一个放到并发中
$info=array_pop($this->urlStack);
if($info){
$ch=self::curlInit($info[‘url‘]);
$info[‘proxy‘]=LProxy::get();
curl_setopt($ch, CURLOPT_PROXY, $info[‘proxy‘]);
curl_multi_add_handle($chs, $ch);
$map[strval($ch)] = $info;
}
//如果仍然有未处理完毕的句柄,那么就select
if ($active > 0) {
curl_multi_select($chs, 0.5); //此处会导致阻塞大概0.5秒。
}
}
}while($active > 0); //还有句柄处理还在进行中
//全部结束
curl_multi_close($chs);
}
以上是并发采集类的主体代码,其它无关代码我就不贴了.
以下是调用 时的 代码
$syn=new LSynCatch();
foreach ( $articles as $k => $v ) {
// 分析文章内容
$syn->push(array(‘url‘=>$v,‘callback‘=>function(array $info){
self::process($info);
}));
}
$syn->run();