一个简单的开源PHP爬虫框架『Phpfetcher』

一个简单的开源PHP爬虫框架『Phpfetcher』
13 votes, 4.92 avg. rating (98% score)
转载请注明: 吹水小镇 | reetsee.com
原文链接地址: http://blog.reetsee.com/archives/366

好久不见了!我终于又写一篇日志了,本来有很多流水帐想发但是感觉没营养,就作罢了。

今天我主要分享一个简单的PHP爬虫框架,名字叫:Phpfetcher

项目的地址是:https://github.com/fanfank/phpfetcher

这个框架的作者是:reetsee.xu,即吹水。

关于这个框架的任何问题,可以到贴吧的『phpfetcher』吧提问,或者到QQ群『413401717』提问。两个地方我每天都会定期看,希望大家不要在里面发水帖或者水消息。

把整个项目下载下来后,在Linux下的终端直接执行demo文件夹下的single_page.php即可看到效果。不过在执行demo文件前,先设置一下你的终端编码为UTF-8以免显示乱码:

export LANG=en_US.UTF-8

————————————————————————————————

0 背景

背景是这样的目前吹水新闻http://news.reetsee.com)下的内容全部由Python的爬虫抓取,使用的框架是Python的Scrapy,而吹水新闻目前是运行在BAE(百度应用引擎)下的,每个月还需要交钱。目前我的想法是把吹水新闻完全迁移到目前这台阿里云主机上,并且原本的新闻我每天都手动执行一次脚本来抓取再更新到网站,等迁移到这里后就能直接使用Crontab定时脚本自动更新新闻了!

最近工作都在用PHP,开发网站的新页面要PHP,直接读写数据库也能用PHP,那么就直接用PHP重构新闻网站好了。

准备开干的时候却发现没找到一个好的PHP爬虫框架(可能是我没仔细找),于是就打算自己写一个,因此就有了这个Phpfetcher。

名字起得略好……但是代码写得略搓……不管怎么样,目前基本可以用,而且应该能满足不少简单的需求,下面就是使用示例。

 1 基本概念

在Phpfetcher中有四个主要的对象,依次是:Dom,Page,Crawler,Manager。

  • Dom对象用来解析html,能够访问html里的dom
  • Page对象对应到一个具体的html页面,能够取得整个网页的内容,Page对象中有一个Dom对象的成员;
  • Crawler对象可以理解为就是爬虫对象,用来设置要爬取页面的规则;
  • Manager对象原本是用来管理Crawler对象的,以后或许能用来在多进程环境下使用,但目前没有实现,所以暂时没有用;

大致概念就是这样了,实际使用主要是操作Crawler对象。在Phpfetcher中,你可以实现自己的Dom,Page和Crawler,只要符合基类的要求即可。

要说明的是Phpfetcher的默认Page对象中的Dom对象使用的是simple_html_dom,没有使用PHP提供的DOMDocument类,因为我发现DOMDocument对HTML格式的内容兼容性比较差,有时网页中混入其它内容时可能解析不出dom。

下面这张是图是Phpfetcher的目录结构:

phpfetcher_目录结构

你可以根据自己的需要定制想要的Crawler,Page,Dom类,默认情况下我提供了Crawler的默认类是Phpfetcher_Crawler_Default,Page的默认类是Phpfetcher_Page_Default,Dom的默认类是Phpfetcher_Dom_SimpleHtmlDom。类名和它们所在的路径有对应关系。要注意的是,在使用默认的Page对象时需要PHP的curl库,使用默认的Crawler对象时需要使用PHP的mb_string库,没有的需要装一下。

为了便于理解,我画了几张图,第一张是Phpfetcher的三个主要对象之间的关系:

phpfetcher_类结构

图里表示的是Crawler里面有Page的对象,Page里面有Dom的对象。

在使用Phpfetcher时,最重要的是完成下图中两个绿色矩形框要求的事情:

使用phpfetcher

 

即你要写一个类继承Phpfetcher提供的Crawler类,然后在你自己的类中实现一个名为handlePage($page)的函数。其中$page参数是一个Phpfetcher的Page类对象。

最后这里给出一个基本的流程图:

phpfetcher_工作流程图

 

上面说的东西有点虚,那还是直接看实例吧!

 

2 简单例子

****** 实例1:single_page.php ******

例如我们要抓取这个网站的内容:http://news.qq.com/a/20140927/026557.htm

里面有很多超链接,有标题,有新闻详细内容,或者其它我们关心的内容。

先看一下下面的例子:

<?php
require_once('phpfetcher.php');
class mycrawler extends Phpfetcher_Crawler_Default {
    public function handlePage($page) {
        //打印处当前页面的title
        $res = $page->sel('//title');
        for ($i = 0; $i < count($res); ++$i) {
            echo $res[$i]->plaintext;
            echo "\n";
        }
    }
}

$crawler = new mycrawler();
$arrJobs = array(
    //任务的名字随便起,这里把名字叫qqnews
    //the key is the name of a job, here names it qqnews
    'qqnews' => array( 
        'start_page' => 'http://news.qq.com/a/20140927/026557.htm', //起始网页
        'link_rules' => array(
            /*
             * 所有在这里列出的正则规则,只要能匹配到超链接,那么那条爬虫就会爬到那条超链接
             * Regex rules are listed here, the crawler will follow any hyperlinks once the regex matches
             */
        ),
        //爬虫从开始页面算起,最多爬取的深度,设置为1表示只爬取起始页面
        //Crawler's max following depth, 1 stands for only crawl the start page
        'max_depth' => 1, 
        
    ) ,   
);

//$crawler->setFetchJobs($arrJobs)->run(); 这一行的效果和下面两行的效果一样
$crawler->setFetchJobs($arrJobs);
$crawler->run();

将这个脚本和“phpfetcher.php”以及“Phpfetcher”文件夹放在同一个目录下(或者将“phpfetcher.php”和“Phpfetcher”放到你的PHP环境默认include的查找路径),执行这个脚本,得到的输出如下:

[root@reetsee demo]# php single_page.php 
王思聪回应遭警方调查:带弓箭不犯法 我是绿箭侠_新闻_腾讯网

查看一下我们抓取的网页源代码,可以发现是下面这几行中的title标签内容提取出来了:

<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=gb2312"></meta>
        <meta charset="gb2312"></meta>
        <title>
            王思聪回应遭警方调查:带弓箭不犯法 我是绿箭侠_新闻_腾讯网
        </title>

上面就是一个最简单的例子。

 

****** 实例2:multi_page.php ******

接下来就是另外一个简单的例子,例如说腾讯新闻的主页,上面有各种新闻,我们这次的目标是把腾讯新闻主页(http://news.qq.com)显示的部分新闻标题抓下来,直接先上例程:

<?php
//下面两行使得这个项目被下载下来后本文件能直接运行
$demo_include_path = dirname(__FILE__) . '/../';
set_include_path(get_include_path() . PATH_SEPARATOR . $demo_include_path);

require_once('phpfetcher.php');
class mycrawler extends Phpfetcher_Crawler_Default {
    public function handlePage($page) {
        //打印处当前页面的第1个h1标题内荣(下标从0开始)
        $strFirstH1 = trim($page->sel('//h1', 0)->plaintext);
        if (!empty($strFirstH1)) {
            echo $page->sel('//h1', 0)->plaintext;
            echo "\n";
        }
    }
}

$crawler = new mycrawler();
$arrJobs = array(
    //任务的名字随便起,这里把名字叫qqnews
    //the key is the name of a job, here names it qqnews
    'qqnews' => array( 
        'start_page' => 'http://news.qq.com', //起始网页
        'link_rules' => array(
            /*
             * 所有在这里列出的正则规则,只要能匹配到超链接,那么那条爬虫就会爬到那条超链接
             * Regex rules are listed here, the crawler will follow any hyperlinks once the regex matches
             */
            '#news\.qq\.com/a/\d+/\d+\.htm$#',
        ),
        //爬虫从开始页面算起,最多爬取的深度,设置为2表示爬取深度为1
        //Crawler's max following depth, 1 stands for only crawl the start page
        'max_depth' => 2, 
        
    ) ,   
);

$crawler->setFetchJobs($arrJobs)->run(); //这一行的效果和下面两行的效果一样
//$crawler->setFetchJobs($arrJobs);
//$crawler->run();

相比于第1个例子,变化的地方有几个:首先这次我们增加了一条爬虫跟踪的规则“#news\.qq\.com/a/\d+/\d+\.htm$#”(注:PHP使用pcre正则表达式,可以到PHP关于正则表达式的页面看一下),这是一个正则表达式,例如这种超链接“news.qq.com/a/12345678/00234.htm”那么爬虫就会跟踪;然后是我们把爬虫的最大跟踪深度设置为2,这样爬虫会跟踪1次起始页面上符合要求的超级链接;最后是我把原本的Dom选择从“//title”改为了“//h1”,意思就是抓取h1标签的内容而不是像之前那样抓取title标签,想知道这种Dom选择器的选择规则,需要了解一下xpath

运行这个文件,能够看到大致效果如下:

phpfetcher_multipage

 

 

这样第二个例子就结束了。

暂时我就介绍这两个例子吧,Phpfetcher的源代码在这里:https://github.com/fanfank/phpfetcher

把代码下载下来后,demo内的东西就可以直接运行了(当然你需要一个有curl和mb_string扩展的php,可以使用“php -m”命令来看一下你的PHP有没有装这两个扩展)。

3 后话

实际上这个phpfetcher目前还有很多问题,性能应该是比较差的,不过毕竟也是我写的第一个框架。

另外是关于phpfetcher我有很多东西还没有提到,例如Page对象的一些设置,Crawler对象的设置等,主要是目前太过懒不想写文档,也不知道有没有必要写。我感觉这个框架还是蛮简单的,里面主要的函数我都做了详细的注释,欢迎阅读批评指正给建议!

最后就是,如果你想写个爬虫,又想用PHP来写,不妨试一下phpfetcher。

祝大家国庆节快乐~!

 

 

转载请注明: 吹水小镇 | reetsee.com
原文链接地址: http://blog.reetsee.com/archives/366


咦,怎么会有个支付宝链接?
这些Money用来写新网站好了


一个简单的开源PHP爬虫框架『Phpfetcher』
13 votes, 4.92 avg. rating (98% score)

53 条思考于 “一个简单的开源PHP爬虫框架『Phpfetcher』

  1. kuhool

    Fatal error: Declaration of Phpfetcher_Crawler_Default::setFetchJobs() must be compatible with that of Phpfetcher_Crawler_Abstract::setFetchJobs() in D:\wamp\www\phpfetcher\Phpfetcher\Crawler\Default.php on line 8

    php报错请指点一下,php哪个版本???

    1. fanfank 文章作者

      你的意思是指这样用吗:

      $arrSel = array(array(“title”), array(“//h1”, 0));
      $arrSelRes = $page->multiSel($arrSel);
      $arrSelRes[0] …
      $arrSelRes[1] …

      如果是这样的话,我觉得你可以自己写一个函数封装一下实现你的需求就可以。
      如果不是这样的话,要不你详细描述一下?(或者用伪代码写出来你想怎么使用)

        1. fanfank 文章作者

          嗯,你要考虑清楚自己想怎么用这个东西。
          不过我看你的这个需求可以通过自己写一个函数封装一下来实现。
          没啥菜不菜的,码能写出来就行

  2. 坚慧

    fanfank您好,这个软件很厉害,一直在找这样的软件。

    有几个问题想请教一下,有的文章列表页有几十页,每页有几十篇文章的标题和超链接,从demo中,好像没看到抓取多个文章列表页的方法,请问有办法支持吗?

    还有,有的文章是分页阅读的,请问有办法支持吗?

    第三个问题,有的文章内嵌了多个iframe,请问有办法支持吗?

    第四个问题,抓取每篇文章时,希望中间有个10秒钟的间隔。

    第五个问题,开始抓取之前,希望能登陆页面获取cookie,请问如何处理?

    这些问题比较多,如果不支持,希望能指点一下修改代码哪里。非常感谢!

    1. fanfank 文章作者

      哈喽,感谢厚爱,我一一回答吧。
      第一和第二个问题:文章分页本质上或者列表翻页本质上就是打开一个新页面(或者发一个请求到指定的URL),你知道对应URL的规则就可以。不过有一个问题,就是如果这些翻页的链接返回的内容不是HTML的(例如它直接返回10条文本格式的链接),那框架没法识别出这10条链接,并且目前框架没有提供在爬虫执行过程中在handlePage函数添加链接的功能,但这个比较好改,你可以在这个文件『https://github.com/fanfank/phpfetcher/blob/master/Phpfetcher/Crawler/Default.php』中看一下函数『run』的源码,目前(2015年4月6号更新的版本)是在243行添加要继续派去的链接的;

      第三个问题:这个和第一、二的问题是一样的,因为你在爬取时才能知道iframe对应的URL源地址,所以相当于是边爬取边在handlePage函数中添加链接,这个暂时还是得按照第一、第二个问题里提到的那样改;

      第四个问题:这个需求太合理了!因为我知道爬虫有时候可能被封,所以不能一次爬取太多链接,但是也很抱歉目前框架没有提供设置的方法,你可以在这个文件『https://github.com/fanfank/phpfetcher/blob/master/Phpfetcher/Crawler/Default.php』(2015年4月6号commit更新的版本)的251行『$intPageNum += 1;』后面加上一个sleep,让它停10秒;

      第五个问题:关于cookie目前也不支持。而且这个的支持比第1~第4个问题我可能要研究更长时间。

      你提的都很好啊,想问下这个你着急吗?我觉得我近期可以先把第1~第4里提到的功能从框架层面先支持了,而且我来写的话应该会更快点。
      如果你不是特别着急,那么在本周内第1~4的功能我应该可以完成,如果你特别着急,你可以先自己改改源码。

    2. fanfank 文章作者

      针对翻页的另外一个问题,如果它是通过js来根据onclick事件触发的翻页,也可能暂时没法实现,还是得你知道它onclick后触发的URL的规则才行。
      补充一下那个每次爬一个页面就睡眠的,你可以直接在handlePage这个用户函数的最后一行写上,可以不用修改源码。
      另外是python也有一个爬虫工具叫Scrapy,能处理更复杂的需求,你可以看看,不过感觉学习成本相对也高一点

  3. steady_pace

    博主,我在BAE上部署个简单的python爬虫。出现了问题,调了好几天了,还是解决不了。希望楼主能帮忙看一下,实在感谢了。

    问题如下:
    BAE上部署了python-worker类型,想部署个爬虫在上面呢。可是偏偏就是不成功。代码在本地是可以正确的爬取页面,解析页面后,提取想要的内容的。

    部署到BAE后,发现,页面可以抓取到,可是返回的html的长度不对:如:某个网页本来是98541,可是在BAE中抓取后,html长度就变成了28831

    re,正则表达式抽取模块是对的,可以按照模式正常抽取。说明解析这部分,是正确的

    数据库操作部分也是:可以连接,插入。
    可是,当我把整个htmly页面插入到 longtext字段时,发现程序可以运行,但是该字段 为空! 是html整个页面太长了么?为什么插入的时候不报错,只是个warning???id自动增长

    所以,我觉得,现在问题是不是归咎于,请求html页面的时候,返回的html页面压缩了???

    1. fanfank 文章作者

      主要是因为这个框架的内容相对少点,所以暂时还没写详细文档的打算。
      不过基本的功能其实就和上面的example是一样的
      如果有啥功能疑问可以随时留言给我。

    2. fanfank 文章作者

      可以啊,那和上面示例代码中列举的功能是一样的。
      你可以先看一下xpath,因为那个是用来选择html元素的,代码里面使用了xpath的规则。
      知道xpath后,就可以随意抽取出你想要的html元素了

  4. aa

    protected ‘_dom’ => null
    为啥我更换了一个网址就不能爬呢 比如http://blog.csdn.net/linmin811/article/details/7583788
    报dom为空

      1. aa

        //下面两行使得这个项目被下载下来后本文件能直接运行
        $demo_include_path = dirname(__FILE__) . ‘/../’;
        set_include_path(get_include_path() . PATH_SEPARATOR . $demo_include_path);

        require_once(‘phpfetcher.php’);
        class mycrawler extends Phpfetcher_Crawler_Default {
        public function handlePage($page) {
        var_dump($page);
        //打印处当前页面的第1个h1标题内荣(下标从0开始)
        $strFirstH1 = trim($page->sel(‘//title’, 0)->plaintext);
        if (!empty($strFirstH1)) {
        echo $page->sel(‘//title’, 0)->plaintext;
        echo “\n”;
        }
        }
        }

        $crawler = new mycrawler();
        $arrJobs = array(
        //任务的名字随便起,这里把名字叫qqnews
        //the key is the name of a job, here names it qqnews
        ‘qqnews’ => array(
        ‘start_page’ => ‘http://blog.csdn.net/linmin811/article/details/7583788’, //起始网页
        ‘link_rules’ => array(
        /*
        * 所有在这里列出的正则规则,只要能匹配到超链接,那么那条爬虫就会爬到那条超链接
        * Regex rules are listed here, the crawler will follow any hyperlinks once the regex matches
        */
        //’#news\.qq\.com/a/\d+/\d+\.htm$#’,
        ),
        //爬虫从开始页面算起,最多爬取的深度,设置为2表示爬取深度为1
        //Crawler’s max following depth, 1 stands for only crawl the start page
        ‘max_depth’ => 1,

        ) ,
        );

        $crawler->setFetchJobs($arrJobs)->run(); //这一行的效果和下面两行的效果一样

        其他的没变

      2. aa

        public function read() {
        $this->_strContent = curl_exec($this->_curlHandle);

        是_strContent取到了false导致的 这个是啥原因呢Page default.php

    1. fanfank 文章作者

      我这里返回的是403 forbidden,查了一下知道原因了,因为user_agent的问题csdn把爬虫给禁了。你可以这样修改:找到文件Phpfetcher/Page/Default.php,然后搜『user_agent』,把里面改掉,我改成『firefox』就可以了,当然你可以可以改得更真实一点,例如什么『Mozilla/5.0 AppleWebKit』之类的
      有些网站会根据UA来屏蔽请求,可能是因为某些UA有恶意攻击的特征,或者一些爬虫之类的,之前百度有一段时间屏蔽360浏览器就是通过360浏览器里一些特定的UA来做到的,当然后来360浏览器把UA给修改嗯,就需要根据其它特征屏蔽了。
      所以你这里先改一下user_agent吧。

    2. fanfank 文章作者

      另外是,你贴的代码里面,标点符号不对啊,你的start_page对应那一行的标点,怎么是中文的单引号?后面的单引号貌似也不是个单引号吧?要全部用英文的单引号才行。

  5. joke

    为什么匹配的内容都一样?

    代码:
    sel(‘//span[@id=”text110″]’, 0)->plaintext);
    if (!empty($strFirstH1)) {
    echo “”;
    echo $page->sel(‘//span[@id=”text110″]’, 0)->plaintext;
    echo “”;
    echo “\n”;
    }
    }
    }
    $crawler = new mycrawler();
    $arrJobs = array(
    ‘joke’ => array(
    ‘start_page’ => ‘http://www.jokeji.cn/JokeHtml/mj/2015102123095331.htm’,
    ‘link_rules’ => array(
    ‘#/\woke\wtml/\w+/20151021\d+\.htm$#’,
    ),
    ‘max_depth’ => 2,

    ) ,
    );

    $crawler->setFetchJobs($arrJobs)->run();

    1. fanfank 文章作者

      已经修复了。之前的问题是爬虫不认识站内链接,例如有的超链接是『/entry』这样的,而不是『http://news.reetsee.com/entry』。现在最新的Phpfetcher已经能够识别站内链接,可以试一下

    1. fanfank 文章作者

      提交post参数,那感觉场景挺特殊的,因为这个就不是单纯地根据链接爬取网页内容了,而且如果真的提供这个功能,针对什么样的链接什么样的参数,怎么提交,然后返回的内容是怎么处理这些,目前我感觉好像不太适合爬虫做。或者你在微博私信我,告诉我你的使用场景是什么,我看看是不是考虑找时间加进去

      1. modejun

        场景就是有一翻页时用ajax post提交的page参数,如果是get就很easy。还有顺便问问,如果翻页我昨天试了要解决的话就是调节深度,但是好像最大是20,还有就是修改正则循环调用setFetchJobs这个方法,总是感觉不是那么完美,有什么好的思路解决翻页这个问题吗,现在公司在定方案我想多了解把这个框架的优势发挥出来,感谢了。

        1. fanfank 文章作者

          如果像你说的是个post请求,那么它返回的应该不是一个HTML格式的文档,通常都是json格式的,然后由当前页面将异步返回的内容加载显示出来。
          你们的post请求应该是有类似pn,rn等参数,如果你们仅仅是想拿到post请求的所有内容,可以直接写一个for循环,然后使用php的curl来直接发送post请求获取每一个页面内容,可以不使用爬虫,因为这个爬虫基本原理是针对GET请求返回的HTML页面的,然后自动抽取HTML的标签

          最大深度可以修改类『Phpfetcher_Crawler_Default』中的『MAX_DEPTH』变量,把20改成-1就没有限制了,不过建议还是设一个上限比较好
          可以不需要循环修改正则呀,设置正则规则的可以是一个数组,把里面的所有你觉得合适的正则都列上就可以,除非说你的正则表达式还得根据页面的某个参数或者内容不同而修改,那这个情况还是相对特殊了一点···
          翻页的解决,如果是GET就用爬虫,如果是POST,那么直接for循环然后调用curl会更好。

  6. jeremy

    博主。。为什么https的页面没办法请求呢?

    返回:2016-01-21 12:20:14 Default.php Phpfetcher_Page_Default sel 116 Warning: $this->_dom is NULL! 2016-01-21 12:20:14 single_page.php Phpfetcher_Page_Default sel 10 Warning: $this->_dom is NULL!

    1. fanfank 文章作者

      从这个错误来看是没有获取到网页。我这边爬https的是没有问题的,你可以参照这个文件『demo/crawl_baidu_page.php』,把里面的http改成https:『http://www.baidu.com/s?wd=facebook』—>『https://www.baidu.com/s?wd=facebook』。然后运行一下,如果还是报错,我感觉也许是你那边的网络设置有点问题?实在不行你在微博私信我,把你运行的代码发我看一下

  7. 程序周

    最近也在看,php爬虫的东西,学了一些python,发现这个框架,可以实现基本的爬虫功能,感谢吹水的分享。
    像其中一个小伙伴,有一些问题,比如ajax类型的翻页,页码没有固定规律的如何进行爬取。
    也遇到一些技术问题,希望大家可以快速的交流起来,在QQ群里面搜了phpfetcher无果,斗胆建立了一个QQ群,促进大家的交流,也跪请博主加入。如果有冒犯,我可以把群解散,或者转给博主。只是为了效率更高的,完善这个框架。

    QQ群名称:Phpfetcher-吹水不吹牛
    QQ群 号:413401717

    1. fanfank 文章作者

      Ajax类型的翻页会有异步请求的地址以及参数。正常来说所有的页面都会有规律,得我们自己去找。多谢你建这个群啊,我会加入的,主要是群的形式太过松散了,容易讨论着讨论着跑偏,我正在申请贴吧的phpfetcher吧,等2~3个工作日会有审核结果。在这之前我先加到群里。

  8. tjb

    博主你看看你那这个能抓取波
    ‘start_page’ =>’http://jianli.58.com/resume/92955882188554/’,
    我的报 “Default.php Phpfetcher_Page_Default sel 116 Warning: $this->_dom is NULL!”
    user_agent 尝试改过不顶用

    1. fanfank 文章作者

      有可能它网站的内容是动态加载的,你要从Chrome的开发者工具里面选择“网络”,然后看它请求的是哪个接口,你再直接调用接口获取数据

发表评论

电子邮件地址不会被公开。 必填项已用*标注