整体构思:
PHP爬取100万条数据,首先要思考这三个问题:
- 怎么爬取?
- 怎么提升爬取速度?
- 怎么存放爬取的数据?
- 怎么爬取一会再以代码说明,先说下怎么提升爬取速度。
第一个想到是不是分布式爬虫呢,主机多的话是可以这么张狂任性的,单机的话就要内敛些了。
不能分布式,那可以多线程啊,换个方向也是很有逼格的。
PHP多线程,我首选swoole了,不仅可以多线程,还可以多任务投递,还能异步执行,这跟爬虫不是很般配么。
- 解决爬取提速的问题,再来解决数据存放的问题。
先考虑下MySQL,毕竟平时用的多,总要给点面子。爬取到一条数据,就存入MySQL,如果这样执着的话,那这100万条数据估计不知道什么时候能爬完。存放在MySQL,本质上是存放在磁盘的,IO操作那就一个字呗,慢。
放磁盘上慢,那就考虑内存呗,是不是想到memcache和redis了。memcache不能持久化,电源断了数据就没了,这100万的数据也不是一两个小时能爬完,显然也不合适。再考虑redis,redis主打海量数据、高并发、可持久化,一听就知道这就是我们想要的了。
- 大体的架构确定了,回到第一个问题,怎么爬取?
爬取数据,首先要确定爬取什么数据,这里就简单点,爬取左边的这个博客信息。
这里有昵称、码龄、原创文章数、粉丝等十几个信息可以爬取。但是我们要爬取100万条数据,那就要找到这么多的用户信息。可以通过下面这种裂变方式找到更多的用户信息。
再来分析下怎么实现这种裂变:
博客主页,也就是获取昵称等信息的页面:
粉丝页面:
关注页面:
有没有发现这些地址都带着一个相同的字符串,这个就是用户的id,也就是说,通过这个id可以找到用户的博客首页、粉丝页面、关注页面,然后在粉丝和关注页面找到更多的用户id。每个uid都有两个作用:获取用户信息,获取用户的粉丝和关注列表。
这就是整体构思过程,下面来看戏按具体的实现。
具体实现:
这里采用面向对象的编程思想,分为Server(swoole服务器)、Spider(爬虫类)、RedisDB(redis数据库)这三个类,以下分别说明这三个类的职责。
Spider(爬虫类)
根据收到的uid,获取粉丝和关注列表里的所有uid(返回uid数组)、获取用户的昵称等信息(返回info数组);
注意:不使用swoole的话,也可以直接使用new Spider类爬取信息,Spider类与其他两个类无耦合。
RedisDB(redis数据库)
将爬取到的uid数组push入List;存储uid的状态,避免重复爬取;存储用户的信息
Server(swoole服务器)
Server类里面分为worker进程和taskworker进程:
worker进程负责调用Spider类的crawlList($cur_uid),获取粉丝和关注列表里的所有uid(返回uid数组),将这些uid放入RedisDB的List(队列)中,并设置$cur_uid的状态为已存在,避免重复爬取,并将$cur_uid投放到任务池中;
taskworker进程从任务池获取到uid,调用Spider爬虫类的crawlInfo($uid)(返回info数组),然后再将info存入redis中;
注意:生产任务的速度要小于消费任务的速度,不然任务池将会溢出。
由于篇幅,这里仅贴出Spider类的代码
<?php
class Spider
{
protected $fans = "https://me.youkuaiyun.com/fans/";
protected $follow = "https://me.youkuaiyun.com/follow/";
protected $info = "https://blog.youkuaiyun.com/";
public function __construct(){
}
/**
* @title 根据用户uid,爬取该用户的粉丝和关注的uid
* @param string uid 用户唯一id
* return array 返回爬取到的 用户粉丝和关注的uid组成的数组
*/
public function crawlList($uid)
{
$list = [];
$url_arr = array(
'url_fans' => $this->fans . $uid,
'url_follow' => $this->follow . $uid
);
foreach($url_arr as $key => $url){
$html = $this->request($url);
if($html === false)break;
$html = preg_replace("/\r|\n|\t/","",$html); // 去掉空白符和换行符
$reg = '#<p class="user_name">[\s]*?<a href="https://me.youkuaiyun.com/([^\"]*?)" target="_blank" class="fans">[^<]*?</a>#';
preg_match_all($reg, $html, $matchs); // $matchs[0]:全匹配数组, $matchs[1]:子匹配数组
$list = array_merge($list, $matchs[1]); // 如果匹配不到数据,$matchs[1]=[]
}
return $list;
}
/**
* @title 根据用户uid,爬取该用户的信息
* @param string uid 用户唯一id
* return array 返回用户信息
*/
public function crawlInfo($uid)
{
$info = []; // 用户信息
// 根据url获取页面
$info_url = $this->info . $uid;
$html = $this->request($info_url);
if($html === false){
return [];
}
// 匹配到了404页面
$extra_reg = '#<div class="new_404">#';
$count = preg_match($extra_reg, $html, $rs);
if(!empty($count)){
return [];
}
// 正则表达式数组
// 用户名 码龄 原创篇数 粉丝 获赞 评论 访问量 积分 收藏 周排名 总排名 博客等级
$reg_arr = array(
'username' => '#<span class="name " username=[\'|\"][^\'\"]*?[\'|\"]>([^<]*?)</span>#',
'code_age' => '#<span class="personal-home-page">码龄([\d]{1,})年</span>#',
'raw_count' => '#<dl class="text-center" title="([\d]{1,})">[\s]*?<dt>[\s]*?<a [^>]*?><span class="count">[^<]*?</span>[\s]*?</a>[\s]*?</dt>[\s]*?<dd><a [^>]*?>原创</a>#',
'fans_count' => '#<dl class="text-center" id="fanBox" title="([\d]{1,})">#',
'hot_count' => '#<dl class="text-center" title="([\d]{1,})">[\s]*?<dt>[\s]*?<span class="count">[^<]*?</span>[\s]*?</dt>[\s]*?<dd>获赞</dd>#',
'comment_count' => '#<dl class="text-center" title="([\d]*?)">[\s]*?<dt>[\s]*?<span class="count">[^<]*?</span>[\s]*?</dt>[\s]*?<dd>评论</dd>#',
'visit_count' => '#<dl class="text-center" style="min-width:58px" title="([\d]{1,})">[\s]*?<dt>[\s]*?<span class="count">[^<]*?</span>[\s]*?</dt>[\s]*?<dd>访问</dd>#',
'jifen_count' => '#<dl class="text-center" title="([\d]{1,})">[\s]*?<dt>[\s]*?<span class="count">[^<]*?</span>[\s]*?</dt>[\s]*?<dd>积分</dd>#',
'collect_count' => '#<dl class="text-center" title="([\d]{1,})">[\s]*?<dt>[\s]*?<span class="count">[^<]*?</span>[\s]*?</dt>[\s]*?<dd>收藏</dd>#',
'week_ranking' => '#<dl class="text-center" title="([\d]{1,})">[\s]*?<dt>[\s]*?<span class="count">[^<]*?</span>[\s]*?</dt>[\s]*?<dd><a [^>]*?>周排名</a>#',
'total_ranking' => '#<dl class="text-center" title="([\d]{1,})">[\s]*?<dt>[\s]*?<span class="count">[^<]*?</span>[\s]*?</dt>[\s]*?<dd>[\s]*?<a [^>]*?>总排名</a>#',
'blog_level' => '#<dl class="text-center" title="([\d]{1,})级,点击查看等级说明">#'
);
$info['uid'] = $uid;
foreach($reg_arr as $key => $reg){
preg_match($reg, $html, $res);
$info[$key] = isset($res[1]) ? trim($res[1]) : "";
unset($res);
}
return $info;
}
/**
* @title 根据url 请求并返回页面
* return false|string
*/
public function request($url){
// 初始化curl
$curl = curl_init();
$header = array(
"Content-type:application/json; text/html; charset=utf-8",
"Accept:application/json",
"Referer:https://me.youkuaiyun.com/fans/gshengod", // 伪造成站内请求
"User-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0"
);
$opt = array(
CURLOPT_URL => $url,
CURLOPT_HTTPHEADER => $header,
CURLOPT_RETURNTRANSFER => 1, // 返回数据,不显示页面内
CURLOPT_FOLLOWLOCATION => 1,
CURLOPT_SSL_VERIFYHOST => 0, // 跳过https验证
CURLOPT_SSL_VERIFYPEER => 0
);
// setopt
curl_setopt_array($curl, $opt);
// 发送请求
$html = curl_exec($curl); // 请求失败是返回false
// 输出错误
$error = curl_error($curl);
// 关闭会话
curl_close($curl);
return $html;
}
}
可以通过以下的方式看下效果:
$spider = new Spider();
// 所要爬取的uid
$cur_uid = "qq_36034503";
// 获取粉丝和关注里所有的uid
$list = $spider->crawlList($cur_uid);
// 获取用户信息
$info = $spider->crawlInfo($cur_uid);
// 输出看下效果
var_dump($list);
var_dump($info);
对swoole和redis有了解的可以尝试,用以上的思路实现下将每个uid当做任务,多任务投递、异步执行
swoole学习文档:https://wiki.swoole.com/#/
redis学习文档:https://www.redis.net.cn/tutorial/3501.html
对 Server类 和 RedisDB类 源码感兴趣可以下评论区留下邮箱,get到了别忘了给个star
注意:以上爬虫仅为学习使用,且要在夜间进行,不要给别人网站带来负载影响