资讯专栏INFORMATION COLUMN

PHP爬虫抓取segmentfault问答

zorro / 2174人阅读

摘要:爬虫抓取问答一需求概述抓取中国领先的开发者社区网站上问答及标签数据侧面反映最新的技术潮流以及国内程序猿的关注焦点注抓取脚本纯属个人技术锻炼非做任何商业用途二开发环境及包依赖运行环境依赖三流程与实践首先先设计两张表文章发布用户文章标题浏览

PHP爬虫抓取segmentfault问答 一 需求概述

抓取中国领先的开发者社区segment.com网站上问答及标签数据,侧面反映最新的技术潮流以及国内程序猿的关注焦点.

注:抓取脚本纯属个人技术锻炼,非做任何商业用途.

二 开发环境及包依赖

运行环境

CentOS Linux release 7.0.1406 (Core)

PHP7.0.2

Redis3.0.5

Mysql5.5.46

Composer1.0-dev

composer依赖

symfony/dom-crawler

三 流程与实践

首先,先设计两张表:post,post_tag

CREATE TABLE `post` (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT "pk",
 `post_id` varchar(32) NOT NULL COMMENT "文章id",
 `author` varchar(64) NOT NULL COMMENT "发布用户",
 `title` varchar(512) NOT NULL COMMENT "文章标题",
 `view_num` int(11) NOT NULL COMMENT "浏览次数",
 `reply_num` int(11) NOT NULL COMMENT "回复次数",
 `collect_num` int(11) NOT NULL COMMENT "收藏次数",
 `tag_num` int(11) NOT NULL COMMENT "标签个数",
 `vote_num` int(11) NOT NULL COMMENT "投票次数",
 `post_time` date NOT NULL COMMENT "发布日期",
 `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT "抓取时间",
 PRIMARY KEY (`id`),
 KEY `idx_post_id` (`post_id`)
) ENGINE=MyISAM AUTO_INCREMENT=7108 DEFAULT CHARSET=utf8 COMMENT="帖子";
CREATE TABLE `post_tag` (
 `id` int(11) NOT NULL AUTO_INCREMENT COMMENT "PK",
 `post_id` varchar(32) NOT NULL COMMENT "帖子ID",
 `tag_name` varchar(128) NOT NULL COMMENT "标签名称",
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15349 DEFAULT CHARSET=utf8 COMMENT="帖子-标签关联表";

当然有同学说,这么设计不对,标签是个独立的主体,应该设计post,tag,post_tag三张表,文档和标签之间再建立联系,这样不仅清晰明了,而且查询也很方便.
这里简单处理是因为首先不是很正式的开发需求,自娱自乐,越简单搞起来越快,另外三张表抓取入库时就要多一张表,更重要的判断标签重复性,导致抓取速度减慢.

整个项目工程文件如下:

app/config/config.php  /*配置文件*/
app/helper/Db.php  /*入库脚本*/
app/helper/Redis.php /*缓存服务*/
app/helper/Spider.php /*抓取解析服务*/
app/helper/Util.php /*工具*/
app/vendor/composer/ /*composer自动加载*/
app/vendor/symfony/ /*第三方抓取服务包*/
app/vendor/autoload.php /*自动加载*/
app/composer.json /*项目配置*/
app/composer.lock /*项目配置*/
app/run.php /*入口脚本*/

因为功能很简单,所以没有必要引用第三方开源的PHP框架

基本配置

class Config
{
    public static $spider = [
        "base_url"  => "http://segmentfault.com/questions?",
        "from_page" => 1,
        "timeout"   => 5,
    ];

    public static $redis = [
        "host"    => "127.0.0.1",
        "port"    => 10000,
        "timeout" => 5,
    ];

    public static $mysql = [
        "host"     => "127.0.0.1",
        "port"     => "3306",
        "dbname"   => "segmentfault",
        "dbuser"     => "user",
        "dbpwd" => "user",
        "charset"  => "utf8",
    ];
}

curl抓取页面的函数

public function getUrlContent($url)
{
    if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) {
        return false;
    }

    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($curl, CURLOPT_TIMEOUT, Config::$spider["timeout"]);
    curl_setopt($curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36");
    $content = curl_exec($curl);
    curl_close($curl);

    return $content;
}

这里要有两点要注意:

第一,要开启CURLOPT_FOLLOWLOCATION301跟踪抓取,因为segmentfautl官方会做域名跳转,比如http://www.segmentfault.com/会跳转到到"http://segmentfault.com"等等.
第二,指定UserAgent,否则会出现301重定向到浏览器升级页面.

crawler解析处理

public function craw()
{
    $content = $this->getUrlContent($this->getUrl());
    $crawler = new Crawler();
    $crawler->addHtmlContent($content);

    $found = $crawler->filter(".stream-list__item");

    //判断是否页面已经结束
    if ($found->count()) {
        $data = $found->each(function (Crawler $node, $i) {
            //问答ID
            $href    = trim($node->filter(".author li a")->eq(1)->attr("href"));
            $a       = explode("/", $href);
            $post_id = isset($a[2]) ? $a[2] : 0;

            //检查该问答是否已经抓取过
            if ($post_id == 0 || !(new Redis())->checkPostExists($post_id)) {
                return $this->getPostData($node, $post_id, $href);
            }

            return false;
        });
        //去除空的数据
        foreach ($data as $i => $v) {
            if (!$v) {
                unset($data[$i]);
            }
        }
        $data = array_values($data);
        $this->incrementPage();

        $continue = true;
    } else {
        $data     = [];
        $continue = false;
    }


    return [$data, $continue];
}

private function getPostData(Crawler $node, $post_id, $href)
{
    $tmp            = [];
    $tmp["post_id"] = $post_id;
    //标题
    $tmp["title"] = trim($node->filter(".summary h2.title a")->text());

    //回答数
    $tmp["reply_num"] = intval(trim($node->filter(".qa-rank .answers")->text()));

    //浏览数
    $tmp["view_num"] = intval(trim($node->filter(".qa-rank .views")->text()));

    //投票数
    $tmp["vote_num"] = intval(trim($node->filter(".qa-rank .votes")->text()));

    //发布者
    $tmp["author"] = trim($node->filter(".author li a")->eq(0)->text());

    //发布时间
    $origin_time = trim($node->filter(".author li a")->eq(1)->text());
    if (mb_substr($origin_time, -2, 2, "utf-8") == "提问") {
        $tmp["post_time"] = Util::parseDate($origin_time);
    } else {
        $tmp["post_time"] = Util::parseDate($this->getPostDateByDetail($href));
    }

    //收藏数
    $collect = $node->filter(".author .pull-right");
    if ($collect->count()) {
        $tmp["collect_num"] = intval(trim($collect->text()));
    } else {
        $tmp["collect_num"] = 0;
    }

    $tmp["tags"] = [];
    //标签列表
    $tags = $node->filter(".taglist--inline");
    if ($tags->count()) {
        $tmp["tags"] = $tags->filter(".tagPopup")->each(function (Crawler $node, $i) {
            return $node->filter(".tag")->text();
        });
    }

    $tmp["tag_num"] = count($tmp["tags"]);

    return $tmp;
}

通过crawler将抓取的列表解析成待入库的二维数据,每次抓完,分页参数递增.
这里要注意几点:
1.有些问答已经抓取过了,入库时需要排除,因此此处加入了redis缓存判断.
2.问答的创建时间需要根据"提问","解答","更新"状态来动态解析.
3.需要把类似"5分钟前","12小时前","3天前"解析成标准的Y-m-d格式

入库操作

public function multiInsert($post)
{
    if (!$post || !is_array($post)) {
        return false;
    }

    $this->beginTransaction();
    try {
        //问答入库
        if (!$this->multiInsertPost($post)) {
            throw new Exception("failed(insert post)");
        }
        //标签入库
        if (!$this->multiInsertTag($post)) {
            throw new Exception("failed(insert tag)");
        }
        $this->commit();
        $this->pushPostIdToCache($post);

        $ret = true;
    } catch (Exception $e) {
        $this->rollBack();
        $ret = false;
    }

    return $ret;
}

采用事务+批量方式的一次提交入库,入库完成后将post_id加入redis缓存

启动作业

require "./vendor/autoload.php";

use helperSpider;
use helperDb;

$spider = new Spider();
while (true) {
    echo "crawling from page:" . $spider->getUrl() . PHP_EOL;
    list($data, $ret) = $data = $spider->craw();
    if ($data) {
        $ret = (new Db)->multiInsert($data);
        echo count($data) . " new post crawled " . ($ret ? "success" : "failed") . PHP_EOL;
    } else {
        echo "no new post crawled".PHP_EOL;
    }
    echo PHP_EOL;

    if (!$ret) {
        exit("work done");
    }
};

运用while无限循环的方式执行抓取,遇到抓取失败时,自动退出,中途可以按Ctrl + C中断执行.

四 效果展示

抓取执行中

问答截图

标签截图

五 总结

以上的设计思路和脚本基本上可以完成简单的抓取和统计分析任务了.
我们先看下TOP25标签统计结果:

可以看出segmentfault站点里,讨论最热的前三名是javascript,php,java,而且前25个标签里跟前端相关的(这里不包含移动APP端)居然有13个,占比50%以上了.

每月标签统计一次标签,就可以很方便的掌握最新的技术潮流,哪些技术的关注度有所下降,又有哪些在上升.

有待完善或不足之处
1.单进程抓取,速度有些慢,如果开启多进程的,则需要考虑进程间避免重复抓取的问题
2.暂不支持增量更新,每次抓取到从配置项的指定页码开始一直到结束,可以根据已抓取的post_id做终止判断(post_id虽不是连续自增,但是一直递增的)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/36152.html

相关文章

  • PHP爬虫抓取segmentfault问答

    摘要:爬虫抓取问答一需求概述抓取中国领先的开发者社区网站上问答及标签数据侧面反映最新的技术潮流以及国内程序猿的关注焦点注抓取脚本纯属个人技术锻炼非做任何商业用途二开发环境及包依赖运行环境依赖三流程与实践首先先设计两张表文章发布用户文章标题浏览 PHP爬虫抓取segmentfault问答 一 需求概述 抓取中国领先的开发者社区segment.com网站上问答及标签数据,侧面反映最新的技术潮流以...

    fevin 评论0 收藏0
  • PHP爬虫抓取segmentfault问答

    摘要:爬虫抓取问答一需求概述抓取中国领先的开发者社区网站上问答及标签数据侧面反映最新的技术潮流以及国内程序猿的关注焦点注抓取脚本纯属个人技术锻炼非做任何商业用途二开发环境及包依赖运行环境依赖三流程与实践首先先设计两张表文章发布用户文章标题浏览 PHP爬虫抓取segmentfault问答 一 需求概述 抓取中国领先的开发者社区segment.com网站上问答及标签数据,侧面反映最新的技术潮流以...

    The question 评论0 收藏0
  • 基于PHP/CURL/codeIgniter的Spider Webbot爬虫[0]-使用原生PHP

    摘要:学了天的,写了一个爬虫开源项目。现在把所有的笔记放到记录下来,算是一个纪念。定义抓取下载的档案对目标档案建立一个网络连接。 学了7天的PHP/CURL,写了一个爬虫开源项目。 现在把所有的笔记放到Segmentfault记录下来,算是一个纪念。 https://github.com/hosinoruri/Omoikane $target=http://www.WebbotsSp...

    masturbator 评论0 收藏0
  • 如何用Python抓抖音上的小姐姐

    摘要:比如分钟破译朋友圈测试小游戏文章里用的方法但有些根本就没有提供网页端,比如今年火得不行的抖音。所以常用的方式就是通过在电脑上装一些抓包软件,将手机上的网络请求全部显示出来。总结下,重点是的抓取,关键是配置代理证书,难点是对请求的分析。 爬虫的案例我们已讲得太多。不过几乎都是 网页爬虫 。即使有些手机才能访问的网站,我们也可以通过 Chrome 开发者工具 的 手机模拟 功能来访问,以便...

    FingerLiu 评论0 收藏0
  • Python Scrapy爬虫框架学习

    摘要:组件引擎负责控制数据流在系统中所有组件中流动,并在相应动作发生时触发事件。下载器下载器负责获取页面数据并提供给引擎,而后提供给。下载器中间件下载器中间件是在引擎及下载器之间的特定钩子,处理传递给引擎的。 Scrapy 是用Python实现一个为爬取网站数据、提取结构性数据而编写的应用框架。 一、Scrapy框架简介 Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 ...

    harriszh 评论0 收藏0

发表评论

0条评论

zorro

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<