抓取中国领先的开发者社区segment.com网站上问答及标签数据,侧面反映最新的技术潮流以及国内程序猿的关注焦点.javascript
注:抓取脚本纯属我的技术锻炼,非作任何商业用途.php
运行环境前端
CentOS Linux release 7.0.1406 (Core)java
PHP7.0.2node
Redis3.0.5mysql
Mysql5.5.46redis
Composer1.0-devsql
composer依赖json
symfony/dom-crawlersegmentfault
首先,先设计两张表: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_FOLLOWLOCATION
301跟踪抓取,由于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 helper\Spider; use helper\Db; $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
虽不是连续自增,可是一直递增的)