持续更新,微信公众号文章批量采集系统的构建

我从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("&nbsp;", "", $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("&nbsp;","",$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。

相关文章
相关标签/搜索