我從2014年就開始作微信公衆號內容的批量採集,最開始的目的是爲了作一個html5的垃圾內容網站。當時垃圾站採集到的微信公衆號的內容很容易在公衆號裏面傳播。當時批量採集特別好作,採集入口是公衆號的歷史消息頁。這個入口到如今也是同樣,只不過愈來愈難採集了。採集的方式也更新換代了好多個版本。後來在2015年html5垃圾站不作了,轉向將採集目標定位在本地新聞資訊類公衆號,前端顯示作成了app。因此就造成了一個能夠自動採集公衆號內容的新聞app。曾經我一直擔憂有一天微信技術升級以後沒法採集內容了,個人新聞app就失效了。但隨着微信不斷的技術升級,採集方法也隨之升級,反而使我愈來愈有信心。只要公衆號歷史消息頁存在,就能批量採集到內容。因此今天決定將採集方法整理以後寫下來。個人方法來源於許多同行的分享精神,因此我也會延續這個精神,將個人成果分享出來。php
本篇文章將持續更新,你所看到的內容將保證在看到的時間是可用的。html
首先咱們來看一個微信公衆號歷史消息頁面的連接地址:前端
http://mp.weixin.qq.com/mp/getmasssendmsg?__biz=MjM5MzczNjY2NA==&uin=NzM4MTk1ODgx&key=9ed31d4918c154c8e04cb95d0b28d07ae8eda2ba29a25f538d06adfa060e5d7d42a1427e8f9cfb6a4c3ecc0903a1a9ab87d1471e43705a8b04e1a796612405546f901ec1e4ea662122bb9235f4dfea4d&devicetype=android-17&version=26031c34&lang=zh_CN&nettype=WIFI&ascene=3&pass_ticket=iyVknv0cBEc1Z8oR4zVs%2BkLeRwYtW5bbtL4Tj9bm%2FwgjP%2BsobV6en3WohWUOllUU&wx_header=1
這裏面有幾個參數:html5
__biz;uin=;key=;devicetype=;version=;lang=;nettype=;ascene=;pass_ticket=;wx_header=;node
其中重要的參數是:__biz;uin=;key=;pass_ticket=;這4個參數。mysql
__biz是公衆號的一個相似id的參數,每一個公衆號擁有一個微信的biz,目前極小機率會發生公衆號的biz會變化的事件;android
剩下的3個參數是有關用戶的id和令牌票據之類的意思,這3個參數的值只能經過微信的客戶端產生。因此咱們想採集公衆號就必須經過一個微信客戶端app。在之前的微信版本中這3個參數還能夠獲取一次以後在有效期以內多個公衆號通用。如今的版本已是每次訪問一個公衆號都會更換參數值。ios
我如今所使用的方法只須要關注__biz這個參數就能夠了。web
個人採集系統由如下幾部分組成:
一、一個微信客戶端:能夠是一臺手機安裝了微信的app,或者是用電腦中的安卓模擬器。通過實測ios的微信客戶端在批量採集過程當中崩潰率高於安卓系統。爲了下降成本,我使用的是安卓模擬器。sql
二、一個微信我的號:爲了採集內容不只須要微信客戶端,還要有一個微信我的號專門用於採集,由於這個微信號就幹不了其它事情了。
三、本地代理服務器系統:目前使用的方法是經過Anyproxy代理服務器將公衆號歷史消息頁面中的文章列表發送到本身的服務器上。具體安裝設置方法在後面詳細介紹。
四、文章列表分析與入庫系統:我用的是php語言編寫的,後文將詳細介紹如何分析文章列表和創建採集隊列實現批量採集內容。
步驟
1、安裝模擬器或使用手機安裝微信客戶端app,申請微信我的號並登陸到app上面。這一點就不過多介紹了,你們都會。
2、代理服務器系統安裝
目前我使用的是Anyproxy,AnyProxy 。這個軟件的特色是能夠獲取到https連接的內容。在2016年年初的時候微信公衆號和微信文章開始使用https連接。而且Anyproxy能夠經過修改rule配置實現向公衆號的頁面中插入腳本代碼。下面開始介紹安裝與配置過程。
一、安裝 NodeJS
二、在命令行或者終端運行 npm install -g anyproxy,mac系統須要加上sudo;
三、生成RootCA,https須要這個證書:運行命令sudo anyproxy --root(windows可能不須要sudo);
四、啓動anyproxy運行命令:sudo anyproxy -i;參數-i是解析HTTPS的意思;
五、安裝證書,在手機或安卓模擬器中安裝證書:
方法一: 啓動anyproxy,瀏覽器打開 http://localhost:8002/fetchCr... ,能獲取rootCA.crt文件
方法二:啓動anyproxy,http://localhost:8002/qr_root 能夠獲取證書路徑的二維碼,移動端安裝時會比較便捷
建議經過二維碼將證書安裝到手機中。
六、設置代理:安卓模擬器的代理服務器地址是wifi連接的網關,能夠經過吧dhcp設置爲靜態後看到網關地址,看完後別忘了再設置爲自動。手機中的代理服務器地址就是運行anyproxy的電腦的ip地址。代理服務器默認端口是8001;
如今打開微信,點擊到任意一個公衆號歷史消息或文章中,在終端均可以看到響應的代碼滾動。若是沒有出現,請檢查手機的代理設置是否正確。
如今打開瀏覽器地址http://localhost:8002 能夠看到anyproxy的web界面。從微信中點開一個歷史消息頁面,而後再看瀏覽器的web界面,會滾動出現歷史消息頁面的地址。
以/mp/getmasssendmsg開頭的網址就是微信歷史消息頁面。左邊一個小鎖頭表示這個頁面是https加密的。如今咱們點擊一下這一行;
右邊若是出現了html的文件內容則表示解密成功。若是沒有內容,請檢查anyproxy的運行模式是否有參數i,是否生成了ca證書,手機是否正確安裝證書。
如今咱們的手機中的全部內容都已經能夠明文經過代理服務器了。下面咱們要修改配置代理服務器,使公衆號內容被獲取到。
1、找到配置文件:
mac系統中配置文件的位置在/usr/local/lib/node_modules/anyproxy/lib/;windows系統請原諒我暫時不知道。應該能夠根據相似mac的文件夾地址找到這個目錄。
2、修改文件rule_default.js
找到replaceServerResDataAsync: function(req,res,serverResData,callback) 函數
修改函數內容(請注意詳細閱讀註釋,這裏只是介紹原理,理解後根據本身的條件修改內容):
replaceServerResDataAsync: function(req,res,serverResData,callback){ if(/mp\/getmasssendmsg/i.test(req.url)){//當連接地址爲公衆號歷史消息頁面時 try {//防止報錯退出程序 var reg = /msgList = (.*?);\r\n/;//定義歷史消息正則匹配規則 var ret = reg.exec(serverResData.toString());//轉換變量爲string HttpPost(ret[1],req.url,"getMsgJson.php");//這個函數是後文定義的,將匹配到的歷史消息json發送到本身的服務器 var http = require('http'); http.get('http://xxx.com/getWxHis.php', function(res) {//這個地址是本身服務器上的一個程序,目的是爲了獲取到下一個連接地址,將地址放在一個js腳本中,將頁面自動跳轉到下一頁。後文將介紹getWxHis.php的原理。 res.on('data', function(chunk){ callback(chunk+serverResData);//將返回的代碼插入到歷史消息頁面中,並返回顯示出來 }) }); }catch(e){//若是上面的正則沒有匹配到,那麼這個頁面內容多是公衆號歷史消息頁面向下翻動的第二頁,由於歷史消息第一頁是html格式的,第二頁就是json格式的。 try { var json = JSON.parse(serverResData.toString()); if (json.general_msg_list != []) { HttpPost(json.general_msg_list,req.url,"getMsgJson.php");//這個函數和上面的同樣是後文定義的,將第二頁歷史消息的json發送到本身的服務器 } }catch(e){ console.log(e);//錯誤捕捉 } callback(serverResData);//直接返回第二頁json內容 } }else if(/mp\/getappmsgext/i.test(req.url)){//當連接地址爲公衆號文章閱讀量和點贊量時 try { HttpPost(serverResData,req.url,"getMsgExt.php");//函數是後文定義的,功能是將文章閱讀量點贊量的json發送到服務器 }catch(e){ } callback(serverResData); }else if(/s\?__biz/i.test(req.url) || /mp\/rumor/i.test(req.url)){//當連接地址爲公衆號文章時(rumor這個地址是公衆號文章被闢謠了) try { var http = require('http'); http.get('http://xxx.com/getWxPost.php', function(res) {//這個地址是本身服務器上的另外一個程序,目的是爲了獲取到下一個連接地址,將地址放在一個js腳本中,將頁面自動跳轉到下一頁。後文將介紹getWxPost.php的原理。 res.on('data', function(chunk){ callback(chunk+serverResData); }) }); }catch(e){ callback(serverResData); } }else{ callback(serverResData); } },
上面這段代碼是利用anyproxy能夠修改返回頁面內容的功能,向頁面注入腳本,和將頁面內容發送到服務器上。使用這個原理來批量採集公衆號內容和閱讀量。這段腳本中自定義了一個函數,下面詳細介紹:
在rule_default.js文件末尾添加如下代碼:
function HttpPost(str,url,path) {//將json發送到服務器,str爲json內容,url爲歷史消息頁面地址,path是接收程序的路徑和文件名 var http = require('http'); var data = { str: encodeURIComponent(str), url: encodeURIComponent(url) }; content = require('querystring').stringify(data); var options = { method: "POST", host: "www.xxx.com",//注意沒有http://,這是服務器的域名。 port: 80, path: path,//接收程序的路徑和文件名 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', "Content-Length": content.length } }; var req = http.request(options, function (res) { res.setEncoding('utf8'); res.on('data', function (chunk) { console.log('BODY: ' + chunk); }); }); req.on('error', function (e) { console.log('problem with request: ' + e.message); }); req.write(content); req.end(); }
上面就是rule規則修改的主要部分,須要將json內容發送到本身的服務器,還要從服務器獲取到下一頁的跳轉地址。這就涉及到了四個php文件:getMsgJson.php、getMsgExt.php、getWxHis.php、getWxPost.php
在詳細介紹這4個php文件以前,爲了提升採集系統性能和下降崩潰率,咱們還能夠進行一些修改:
安卓模擬器常常會訪問一些http://google.com的地址,這樣會致使anyproxy死機,找到函數replaceRequestOption : function(req,option),修改函數內容:
replaceRequestOption : function(req,option){ var newOption = option; if(/google/i.test(newOption.headers.host)){ newOption.hostname = "www.baidu.com"; newOption.port = "80"; } return newOption; },
以上就是針對anyproxy的rule文件的修改配置,配置修改完成以後,從新啓動anyproxy。mac系統裏按control+c中斷程序,再輸入命令sudo anyproxy -i啓動;若是啓動報錯,多是程序沒有退出乾淨,端口被佔用。這時輸入命令ps -a查看佔用的pid,再輸入命令「kill -9 pid」這裏將pid替換成查詢到的pid號碼。殺死進程以後就能夠啓動anyproxy了。仍是那句話windows的命令請原諒我不太熟悉。
接下來詳細介紹服務器上接收程序的設計原理:
(如下代碼並非直接能夠用的,只是介紹原理,其中一部分須要根據本身的服務器數據庫框架進行編寫)
一、getMsgJson.php:這個程序負責接收歷史消息的json並解析後存入數據庫
<? $str = $_POST['str']; $url = $_POST['url'];//先獲取到兩個POST變量 //先針對url參數進行操做 parse_str(parse_url(htmlspecialchars_decode(urldecode($url)),PHP_URL_QUERY ),$query);//解析url地址 $biz = $query['__biz'];//獲得公衆號的biz //接下來進行如下操做 //從數據庫中查詢biz是否已經存在,若是不存在則插入,這表明着咱們新添加了一個採集目標公衆號。 //再解析str變量 $json = json_decode($str,true);//首先進行json_decode if(!$json){ $json = json_decode(htmlspecialchars_decode($result),true);//若是不成功,就增長一步htmlspecialchars_decode } foreach($json['list'] as $k=>$v){ $type = $v['comm_msg_info']['type']; if($type==49){//type=49表明是圖文消息 $content_url = str_replace("\\", "", htmlspecialchars_decode($v['app_msg_ext_info']['content_url']));//得到圖文消息的連接地址 $is_multi = $v['app_msg_ext_info']['is_multi'];//是不是多圖文消息 $datetime = $v['comm_msg_info']['datetime'];//圖文消息發送時間 //在這裏將圖文消息連接地址插入到採集隊列庫中(隊列庫將在後文介紹,主要目的是創建一個批量採集隊列,另外一個程序將根據隊列安排下一個採集的公衆號或者文章內容) //在這裏根據$content_url從數據庫中判斷一下是否重複 if('數據庫中不存在相同的$content_url') { $field_id = $v['app_msg_ext_info']['fileid'];//一個微信給的id,每條文章惟一不重複 $title = $v['app_msg_ext_info']['title'];//文章標題 $title_encode = urlencode(str_replace(" ", "", $title));//建議將標題進行編碼,這樣就能夠存儲emoji特殊符號了 $digest = $v['app_msg_ext_info']['digest'];//文章摘要 $source_url = str_replace("\\", "", htmlspecialchars_decode($v['app_msg_ext_info']['source_url']));//閱讀原文的連接 $cover = str_replace("\\", "", htmlspecialchars_decode($v['app_msg_ext_info']['cover']));//封面圖片 $is_top = 1;//標記一下是頭條內容 //如今存入數據庫 echo "頭條標題:".$title.$lastId."\n";//這個echo能夠顯示在anyproxy的終端裏 } if($is_multi==1){//若是是多圖文消息 foreach($v['app_msg_ext_info']['multi_app_msg_item_list'] as $kk=>$vv){//循環後面的圖文消息 $content_url = str_replace("\\","",htmlspecialchars_decode($vv['content_url']));//圖文消息連接地址 //這裏再次根據$content_url判斷一下數據庫中是否重複以避免出錯 if('數據庫中不存在相同的$content_url'){ //在這裏將圖文消息連接地址插入到採集隊列庫中(隊列庫將在後文介紹,主要目的是創建一個批量採集隊列,另外一個程序將根據隊列安排下一個採集的公衆號或者文章內容) $title = $vv['title'];//文章標題 $field_id = $vv['fileid'];//一個微信給的id,每條文章惟一不重複 $title_encode = urlencode(str_replace(" ","",$title));//建議將標題進行編碼,這樣就能夠存儲emoji特殊符號了 $digest = htmlspecialchars($vv['digest']);//文章摘要 $source_url = str_replace("\\","",htmlspecialchars_decode($vv['source_url']));//閱讀原文的連接 //$cover = getCover(str_replace("\\","",htmlspecialchars_decode($vv['cover']))); $cover = str_replace("\\","",htmlspecialchars_decode($vv['cover']));//封面圖片 //如今存入數據庫 echo "標題:".$title.$lastId."\n"; } } } } } ?>
再次強調代碼只是原理,其中一部分注視的代碼要本身編寫。
二、getMsgExt.php獲取文章閱讀量和點贊量的程序
<? $str = $_POST['str']; $url = $_POST['url'];//先獲取到兩個POST變量 //先針對url參數進行操做 parse_str(parse_url(htmlspecialchars_decode(urldecode($url)),PHP_URL_QUERY ),$query);//解析url地址 $biz = $query['__biz'];//獲得公衆號的biz $sn = $query['sn']; //再解析str變量 $json = json_decode($str,true);//進行json_decode //$sql = "select * from `文章表` where `biz`='".$biz."' and `content_url` like '%".$sn."%'" limit 0,1; //根據biz和sn找到對應的文章 $read_num = $json['appmsgstat']['read_num'];//閱讀量 $like_num = $json['appmsgstat']['like_num'];//點贊量 //在這裏一樣根據biz和sn在採集隊列表中刪除對應的文章,表明這篇文章能夠移出採集隊列了 //而後將閱讀量和點贊量更新到文章表中。 exit(json_encode($msg));//能夠顯示在anyproxy的終端裏 ?>
三、getWxHis.php、getWxPost.php兩個程序比較相似,一塊兒介紹
<? //getWxHis.php 當前頁面爲公衆號歷史消息時,讀取這個程序 //在採集隊列表中有一個load字段,當值等於1時表明正在被讀取 //首先刪除採集隊列表中load=1的行 //而後從隊列表中任意select一行 if('隊列表爲空'){ //隊列表若是空了,就從存儲公衆號biz的表中取得一個biz,這裏我在公衆號表中設置了一個採集時間的time字段,按照正序排列以後,就獲得時間戳最小的一個公衆號記錄,並取得它的biz $url = "http://mp.weixin.qq.com/mp/getmasssendmsg?__biz=".$biz."#wechat_webview_type=1&wechat_redirect";//拼接公衆號歷史消息url地址 //更新剛纔提到的公衆號表中的採集時間time字段爲當前時間戳。 }else{ //取得當前這一行的content_url字段 $url = $content_url; //將load字段update爲1 } echo "<script>setTimeout(function(){window.location.href='".$url."';},2000);</script>";//將下一個將要跳轉的$url變成js腳本,由anyproxy注入到微信頁面中。 ?> <? //getWxPost.php 當前頁面爲公衆號文章頁面時,讀取這個程序 //首先刪除採集隊列表中load=1的行 //而後從隊列表中按照「order by id asc」選擇多行(注意這一行和上面的程序不同) if(!empty('隊列表') && count('隊列表中的行數')>1){//(注意這一行和上面的程序不同) //取得第0行的content_url字段 $url = $content_url; //將第0行的load字段update爲1 }else{ //隊列表還剩下最後一條時,就從存儲公衆號biz的表中取得一個biz,這裏我在公衆號表中設置了一個採集時間的time字段,按照正序排列以後,就獲得時間戳最小的一個公衆號記錄,並取得它的biz $url = "http://mp.weixin.qq.com/mp/getmasssendmsg?__biz=".$biz."#wechat_webview_type=1&wechat_redirect";//拼接公衆號歷史消息url地址 //更新剛纔提到的公衆號表中的採集時間time字段爲當前時間戳。 } echo "<script>setTimeout(function(){window.location.href='".$url."';},2000);</script>";//將下一個將要跳轉的$url變成js腳本,由anyproxy注入到微信頁面中。 ?>
這兩個程序的微小差異是由於當讀取公衆號歷史消息頁面時,anyproxy會同時作兩件事,第一是將歷史消息的json發送到服務器,第二是獲取到下一頁的連接地址。可是這兩個操做是存在時間差的,第一次讀取下一頁地址時候原本應該是獲得當前這個公衆號文章的第一條連接地址,可是這時候歷史消息的json尚未發送到服務器,因此只能獲得第二個公衆號的歷史消息頁面。在讀取第二個公衆號歷史消息頁面以後獲得的下一頁地址則是第一個公衆號的第一篇文章的地址。當隊列還剩下一條記錄時,就須要再去取得下一個公衆號的連接地址,不然若是當隊列空了再去取得下一個公衆號的連接地址,就會循環到上面提到的第一次讀取時的狀況,這樣就會出現兩個公衆號歷史消息列表和文章採集穿插進行的狀況。
剛纔這4個PHP程序提到了幾個數據表,下面再講一下數據表如何設計。這裏只介紹一些主要字段,現實應用中還會根據本身程序的不一樣添加上其它有必要的字段。
一、微信公衆號表
CREATE TABLE `weixin` ( `id` int(11) NOT NULL AUTO_INCREMENT, `biz` varchar(255) DEFAULT '' COMMENT '公衆號惟一標識biz', `collect` int(11) DEFAULT '1' COMMENT '記錄採集時間的時間戳', PRIMARY KEY (`id`) ) ;
二、微信文章表
CREATE TABLE `post` ( `id` int(11) NOT NULL AUTO_INCREMENT, `biz` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '文章對應的公衆號biz', `field_id` int(11) NOT NULL COMMENT '微信定義的一個id,每條文章惟一', `title` varchar(255) NOT NULL DEFAULT '' COMMENT '文章標題', `title_encode` text CHARACTER SET utf8 NOT NULL COMMENT '文章編碼,防止文章出現emoji', `digest` varchar(500) NOT NULL DEFAULT '' COMMENT '文章摘要', `content_url` varchar(500) CHARACTER SET utf8 NOT NULL COMMENT '文章地址', `source_url` varchar(500) CHARACTER SET utf8 NOT NULL COMMENT '閱讀原文地址', `cover` varchar(500) CHARACTER SET utf8 NOT NULL COMMENT '封面圖片', `is_multi` int(11) NOT NULL COMMENT '是否多圖文', `is_top` int(11) NOT NULL COMMENT '是否頭條', `datetime` int(11) NOT NULL COMMENT '文章時間戳', `readNum` int(11) NOT NULL DEFAULT '1' COMMENT '文章閱讀量', `likeNum` int(11) NOT NULL DEFAULT '0' COMMENT '文章點贊量', PRIMARY KEY (`id`) ) ;
三、採集隊列表
CREATE TABLE `tmplist` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `content_url` varchar(255) DEFAULT NULL COMMENT '文章地址', `load` int(11) DEFAULT '0' COMMENT '讀取中標記', PRIMARY KEY (`id`), UNIQUE KEY `content_url` (`content_url`) ) ;
以上就是由微信客戶端、微信號、anyproxy代理服務器、PHP程序、mysql數據庫共同組成的微信公衆號文章批量自動採集系統。
在接下來的文章中,還會再進一步詳細介紹如何保存文章內容,如何提升採集系統的穩定性,以及其它個人系統運行過程當中獲得的經驗。
很是但願你們能給予意見和交流,歡迎騷擾微信號cuijin。