PHP爬虫-100万条数据其实不难

整体构思:

PHP爬取100万条数据,首先要思考这三个问题:

  1. 怎么爬取?
  2. 怎么提升爬取速度?
  3. 怎么存放爬取的数据?
  • 怎么爬取一会再以代码说明,先说下怎么提升爬取速度

        第一个想到是不是分布式爬虫呢,主机多的话是可以这么张狂任性的,单机的话就要内敛些了。

        不能分布式,那可以多线程啊,换个方向也是很有逼格的。

        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.csdn.net/fans/";
    protected $follow = "https://me.csdn.net/follow/";
    protected $info = "https://blog.csdn.net/";

    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.csdn.net/([^\"]*?)" 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.csdn.net/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

注意:以上爬虫仅为学习使用,且要在夜间进行,不要给别人网站带来负载影响

 

 

 

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值