摘要:爬蟲抓取問答一需求概述抓取中國領先的開發者社區網站上問答及標簽數據側面反映最新的技術潮流以及國內程序猿的關注焦點注抓取腳本純屬個人技術鍛煉非做任何商業用途二開發環境及包依賴運行環境依賴三流程與實踐首先先設計兩張表文章發布用戶文章標題瀏覽
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雖不是連續自增,但是一直遞增的)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/21348.html
摘要:學了天的,寫了一個爬蟲開源項目。現在把所有的筆記放到記錄下來,算是一個紀念。定義抓取下載的檔案對目標檔案建立一個網絡連接。 學了7天的PHP/CURL,寫了一個爬蟲開源項目。 現在把所有的筆記放到Segmentfault記錄下來,算是一個紀念。 https://github.com/hosinoruri/Omoikane $target=http://www.WebbotsSp...
摘要:比如分鐘破譯朋友圈測試小游戲文章里用的方法但有些根本就沒有提供網頁端,比如今年火得不行的抖音。所以常用的方式就是通過在電腦上裝一些抓包軟件,將手機上的網絡請求全部顯示出來。總結下,重點是的抓取,關鍵是配置代理證書,難點是對請求的分析。 爬蟲的案例我們已講得太多。不過幾乎都是 網頁爬蟲 。即使有些手機才能訪問的網站,我們也可以通過 Chrome 開發者工具 的 手機模擬 功能來訪問,以便...
摘要:組件引擎負責控制數據流在系統中所有組件中流動,并在相應動作發生時觸發事件。下載器下載器負責獲取頁面數據并提供給引擎,而后提供給。下載器中間件下載器中間件是在引擎及下載器之間的特定鉤子,處理傳遞給引擎的。 Scrapy 是用Python實現一個為爬取網站數據、提取結構性數據而編寫的應用框架。 一、Scrapy框架簡介 Scrapy是一個為了爬取網站數據,提取結構性數據而編寫的應用框架。 ...
閱讀 1772·2021-10-11 10:57
閱讀 2356·2021-10-08 10:14
閱讀 3400·2019-08-29 17:26
閱讀 3356·2019-08-28 17:54
閱讀 3029·2019-08-26 13:38
閱讀 2904·2019-08-26 12:19
閱讀 3613·2019-08-23 18:05
閱讀 1282·2019-08-23 17:04