微信公众号数据采集历史页、文章阅读量点赞量
由于在微信之外无法访问到阅读量和点赞量还有历史页面,所以需要研究怎么在此之外来获取到数据。
最终实现接口:http://www.xiaoxiangzi.com/68.html
之前看过几篇前辈的文章,主要利用Anyproxy作为中间人代理来获取微信公众号的文章数据,开始用了一段时间,发现并不是很理想。
使用Anyproxy有几个不是很好的地方:
一,采集时间慢;
二、需要注入JS跳转页面,翻页;
三、不稳定。
后来我想出了另外一种方案,因为我是iOS开发,也有前后端开发经验,后面又学了iOS逆向,所以刚好有这个能力来实现这个方案。
方案简述:
一、经过研究发现获取数据所需要的COOKIE里有几个关键参数,wap_sid2、appmsg_token、pass_ticket,而经过抓包发现,这几个COOKIE参数是首次访问页面的时候被setCookie的。而这两个参数是X-WECHAT-UIN与X-WECHAT-KEY。
二、我只需要通过逆向手段分析到X-WECHAT-UIN与X-WECHAT-KEY的来源就可以获得COOKIE。
三、业务流程:向服务器提交一个需要抓取的页面(HTTP请求开始),服务器告诉手机微信需要抓取的网页(SOCKET),手机微信开始去访问这个页面,同时它会自动构造X-WECHAT-UIN与X-WECHAT-KEY,此时我们拦截这个请求并告诉服务器(SOCKET),服务器通过X-WECHAT-UIN与X-WECHAT-KEY去访问需要抓取的页面,抓取成功之后告诉请求者(HTTP请求结束),并且我们可以把这个COOKIE保存,用于访问同一个公众号下的其他页面。
由于采集数据量过大会导致微信封号,所以我们可以支持很多安装了微信的手机跟服务器建立长连接,每次请求由服务器分发请求。
最终实现:
硬件:一台或N台已越狱iPhone手机、一台服务器
向服务器上架设的接口POST一个历史页或文章页,服务器返回历史数据或阅读量,获取到KEY再获取COOKIE,耗时2s内,另外可以将这个COOKIE保存用于下一次访问同一个公众号的请求,可以节省不少时间。
所需技术:iOS正逆向、PHP、NodeJS
iOS篇
使用Theos创建tweak,HOOK微信的方法
微信启动时,我们实例化一个MMWebViewController,并且后面都用这一个,同时我们与服务器建立长连接,当我们需要访问某个页面,就调用这个对象的goToURL方法,同时拦截加载X-WECHAT-UIN与X-WECHAT-KEY的方法,向服务器回传这个UIN和KEY。
NodeJS篇
搭建Socket服务器,监听两个端口,一个端口给手机微信连接,另外一个给PHP传URL参数。这里我只做了只有一个手机在线的情况,如果获取多个手机微信的KEY,那只需把ServerSocket设置成一个数组,同时判断哪一个客户端正处于闲置状态即可。
var net= require('net');
var HOST = '192.168.10.209';
var WEBPORT= 9001;
var APPPORT= 9002;
var ServerSocket;
var clientSockets= new Array();
net.createServer(function(sock) {
console.log('CONNECTION: ' + sock.remoteAddress+ ':' + sock.remotePort);
clientSockets.push({data:sock});
// 为这个socket实例添加一个"data"事件处理函数
sock.on('data',function(data) {
//console.log('DATA' + sock.remoteAddress + ': ' + data);
if(ServerSocket==null){// APP服务器不在线
var jsonString = '{"meta":{"code":0,"message":"解码服务器不在线"},"data":""}';
var httpContent = 'HTTP\/1.1 200\nContent-Type:application\/json\n\n'+jsonString;
sock.write(Buffer.from(httpContent));
sock.end();
}else{
ServerSocket.write(sock.remoteAddress+'\n'+sock.remotePort+'\n'+data);
// 超时结束
setTimeout(function() {
// 如果还没有响应则超时处理
for (var i= 0; i< clientSockets.length; i++) {
var socket= clientSockets[i]['data'];
if(socket.remoteAddress==sock.remoteAddress&&socket.remotePort==sock.remotePort){
var jsonString = '{"meta":{"code":0,"message":"请求超时"},"data":""}';
var httpContent = 'HTTP\/1.1 200\nContent-Type:application\/json\n\n'+jsonString;
sock.write(Buffer.from(httpContent));
sock.end();
}
}
},15000);
}
});
// 为这个socket实例添加一个"close"事件处理函数
sock.on('close',function(data) {
removeSocket(sock);
console.log('CLOSED: ' +
sock.remoteAddress+ ' ' + sock.remotePort);
});
}).listen(WEBPORT,HOST);
// 与APP建立连接
net.createServer(function(sock) {
ServerSocket= sock;
console.log('iPhone is Online!');
sock.on('data',function(data) {// APP向服务器回传数据
var dataString= data+'';
var arr= dataString.split('\n');
var host= arr[0];
var port= arr[1];
var content= arr[2];
for (var i= 0; i< clientSockets.length; i++) {
var socket= clientSockets[i]['data'];
if(socket.remoteAddress==host&&socket.remotePort==port){
if(content=='close'){
var jsonString = '{"meta":{"code":0,"message":"非法请求"},"data":""}';
var httpContent = 'HTTP\/1.1 200\nContent-Type:application\/json\n\n'+jsonString;
socket.write(Buffer.from(httpContent));
socket.end();
break;
}
var jsonString = '{"meta":{"code":1,"message":"请求成功!"},"data":"'+content+'"}';
var httpContent = 'HTTP\/1.1 200\nContent-Type:application\/json\n\n'+jsonString;
socket.write(Buffer.from(httpContent));
socket.end();
break;
}
}
});
sock.on('close',function(data) {
console.log('iPhone is OffLine!');
ServerSocket= null;
});
}).listen(APPPORT,HOST);
// 从连接池删除指定Socket连接
function removeSocket(sock){
for (var i= 0; i< clientSockets.length; i++) {
var socket= clientSockets[i]['data'];
if(socket.remoteAddress==sock.remoteAddress&&socket.remotePort==sock.remotePort){
clientSockets.splice(i,1);
break;
}
}
}
console.log('Server is OPEN');
PHP篇
用户端POST一个URL到PHP,PHP向Node服务器以HTTP协议发送这个URL数据,Node服务器把这个数据转发给手机端口,这里我用的TP5,数据库设计如下。
CREATE TABLE `wc_log` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`original_url` varchar(512) DEFAULT NULL,
`conversion_url` varchar(512) DEFAULT NULL,
`wx_uin` varchar(512) DEFAULT NULL,
`wx_key` varchar(512) DEFAULT NULL,
`uid` varchar(128) DEFAULT NULL,
`biz` varchar(512) DEFAULT NULL,
`cookie` varchar(1024) DEFAULT NULL,
`pass_ticket` varchar(128) DEFAULT NULL,
`appmsg_token` varchar(256) DEFAULT NULL,
`create_time` int(11) DEFAULT NULL,
`update_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
实际上大可将手机直接做成Web服务器,但是有一个问题就是外网无法访问到手机,所以用服务器做中转,同时可以分发请求。