更多详见博客:https://www.oysterqaq.com/archives/850
仅仅只是忠实记录开发过程,最终教程另见
1)模拟登陆
在准备阶段收集了一些情报(个人习惯)得知Pixiv下载大图必须账户登录(实际上并不需要),按着网上python爬虫教程的分析,也试着对pixiv登陆过程进行了抓包分析(为了查看登录页面post的参数,务必勾选上preserve log)
登录界面url:https://accounts.pixiv.net/login?lang=zh&source=pc&view_type=page&ref=wwwtop_accounts_index
输入账户名和密码后点击登录后,查看对登录接口所post的数据
参数中唯一有疑问的就是post_key了,既然是post的请求参数,大概率应该在表单中。新建一个无痕窗口,进入登录页面F12查看网页源码,找到表单部分,会发现几个隐藏域
换浏览器多次抓包后基本实锤了,post_key是每次在第一次进入登录界面时服务器随机生成的一个校验码与sessionid相对应,添加到隐藏域中在post请求中一并提交到后台验证。
那么情况就比较明朗了,在get登录页面的响应中将post_key取出,加入post参数中,之后持有返回的cookie维持在登录状态则web端模拟登陆完毕。
2)抓取日排行
日排行url:https://www.pixiv.net/ranking.php?mode=daily
分析了下日排行页面的html,发现基本信息都包含在每个section内
页面往下拉,抓取ajax请求链接
ajax机制为每50个名次进行一次ajax请求,既然有了ajax请求链接,那么首页信息猜测也可以通过ajax请求获得(不需要分析html直接获取json数据),具体过程不说了,直接贴ajax请求的链接,不管是首页还是下一页的json都可以从这里获取(也就是说不用解析排行榜首页html也不用模拟登陆)
ajax请求url:https://www.pixiv.net/ranking.php?mode=daily&p=1&format=json
可以看出json中已经包含原图的链接,但是测试发现请求原图有请求来源限制(防盗链)
Referrer:https://www.pixiv.net/member_illust.php?mode=medium&illust_id=69526398
Json中得到可以url:https://i.pximg.net/c/240x480/img-master/img/2018/07/04/00/03/26/69526398_p0_master1200.jpg和图片id,根据这两者进行拼接,可以直接get得到原图(但是后缀到底是png还是jpg并不明确,错误会返回403,默认使用jpg,预计在开发中若返回错误,换参递归调用自身)
1)实现非会员热门度搜索
众所周知,在pixiv是否是高级会员影响最大的部分是热门度搜索,普通会员搜索时默认按照日期排序,由于pixiv投稿并不筛选,导致各类良莠不齐的作品出现在搜索结果中
普通会员:
高级会员:
目标是实现非会员类热门度搜索,基本想法是提取筛选排序搜索结果的json数据
搜索页面url:https://www.pixiv.net/search.php?word=fate&order=date_d&p=2
使用postman模拟发送get请求后发现搜索结果是由js动态生成页面
由于是第一次写爬虫,第一时间有点傻眼,查看html发现搜索结果的json数据是包含在一个tag的字段中
估计是页面加载完成后通过js动态添加到html中展示
推荐关键词位置:
到这里就差不多了,大概过程就是获取总搜索结果数算出总页数,遍历页面的html,使用正则清洗筛选json数据字符串(只需要画作id,订阅个数,画作缩略图url),排序搜索结果(可以使用js排序json数据)
实际上以上工作单线程版写完后发现,效率还是很低,关键就是web端搜索结果是分页展示而不是ajax,这就导致得一次一次获取html而不能直接获取json
思维发散一下,注意到pixiv的app端,搜索结果是类似ajax加载的展示效果,猜测应该有直接返回搜索结果json的api,于是开始对app进行抓包,使用fiddler对手机抓包,需要注意的是在安装证书时候碰到了无法访问电脑端的问题,关闭windows防火墙就行(只是开启单个端口并没有作用)
不出所料,app端确实是有直接返回json数据的接口
查看请求头
多次请求后发现X-Client-Hash\X-Client-Time\Authorization用于校验:
Authorization是类似用户cookie,服务端不出意外一般可以一直使用,时间格式也好办,但是问题出在X-Client-Hash的生成方式,无法确定是如何对时间信息进行加密
由于没法确定请求头的生成方式,考虑反编译apk,寻找生成X-Client-Hash的方法
着手反编译(反编译结果其实并不好用),查找
r8 = this;
r7 = " ~@~@~@~@~@~@~@~@~@~@~ Smob - Mod protection tool v2.5 by Kirlif' ~@~@~@~@~@~@~@~@~@~@~ ";
r0 = new java.text.SimpleDateFormat;
r1 = "yyyy-MM-dd'T'HH:mm:ssZZZZZ";
r7 = 3;
r2 = java.util.Locale.US;
r0.(r1, r2);
r1 = new java.util.Date;
r7 = 4;
r1.();
r0 = r0.format(r1);
r7 = 0;
r1 = new java.lang.StringBuilder;
r1.();
r7 = 0;
r1.append(r0);
r2 = "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
r1.append(r2);
r7 = 6;
r1 = r1.toString();
r7 = 6;
r1 = jp.pxv.android.q.bb.b(r1);
r2 = r9.request();
r7 = 0;
r2 = r2.newBuilder();
r7 = 6;
r3 = "User-Agent";
r4 = jp.pxv.android.client.h.a;
r2 = r2.addHeader(r3, r4);
r7 = 5;
r3 = "Content-Type";
r7 = 4;
r4 = "application/x-www-form-urlencoded;charset=UTF-8";
r7 = 4;
r2 = r2.addHeader(r3, r4);
r7 = 6;
r3 = "Accept-Language";
r7 = 2;
r4 = java.util.Locale.getDefault();
r7 = 0;
r4 = r4.toString();
r2 = r2.addHeader(r3, r4);
r3 = "App-OS";
r7 = 5;
r4 = "android";
r2 = r2.addHeader(r3, r4);
r7 = 3;
r3 = "App-OS-Version";
r7 = 6;
r4 = android.os.Build.VERSION.RELEASE;
r7 = 5;
r2 = r2.addHeader(r3, r4);
r7 = 4;
r3 = "App-Version";
r7 = 3;
r4 = "5.0.104";
r7 = 2;
r2 = r2.addHeader(r3, r4);
r7 = 3;
r3 = "X-Client-Time";
r0 = r2.addHeader(r3, r0);
r7 = 4;
r2 = "X-Client-Hash";
r7 = 4;
r0 = r0.addHeader(r2, r1);
r7 = 6;
r0 = r0.build();
r7 = 0;
jp.pxv.android.q.as.a(r8);
r0 = "MD5"; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 3; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r0 = java.security.MessageDigest.getInstance(r0); Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 3; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r8 = r8.getBytes(); Catch:{ NoSuchAlgorithmException -> 0x0047 }
r8 = r0.digest(r8); Catch:{ NoSuchAlgorithmException -> 0x0047 }
r0 = new java.lang.StringBuilder; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r0.(); Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 4; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r1 = r8.length; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 5; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r2 = 0; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r3 = 0; Catch:{ NoSuchAlgorithmException -> 0x0047 }
L_0x001d:
if (r3 >= r1) goto L_0x003f; Catch:{ NoSuchAlgorithmException -> 0x0047 }
L_0x001f:
r7 = 1; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r4 = r8[r3]; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 1; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r5 = "%02x"; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r6 = 1; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r6 = 1; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 7; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r6 = new java.lang.Object[r6]; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 1; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r4 = java.lang.Byte.valueOf(r4); Catch:{ NoSuchAlgorithmException -> 0x0047 }
r6[r2] = r4; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 2; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r4 = java.lang.String.format(r5, r6); Catch:{ NoSuchAlgorithmException -> 0x0047 }
r0.append(r4); Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 0; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r3 = r3 + 1; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 3; Catch:{ NoSuchAlgorithmException -> 0x0047 }
goto L_0x001d; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r1 = 3; Catch:{ NoSuchAlgorithmException -> 0x0047 }
L_0x003f:
r7 = 4; Catch:{ NoSuchAlgorithmException -> 0x0047 }
r8 = r0.toString(); Catch:{ NoSuchAlgorithmException -> 0x0047 }
r7 = 4;
return r8;
r2 = 0;
L_0x0047:
r8 = move-exception;
r7 = 4;
r0 = "StringUtils";
r1 = "NoSuchAlgorithmException";
jp.pxv.android.q.ae.c(r0, r1, r8);
r8 = "";
r7 = 2;
return r8;
r5 = 4;
刚看着结果emmm了一会,还是忍住恶心看下去,由于多次测试知道hash是根据时间变化的,半猜着的得出了结果
public static String[] gethash() throws NoSuchAlgorithmException {
SimpleDateFormat simpleDateFormat;
String fortmat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ";
simpleDateFormat = new SimpleDateFormat(fortmat, Locale.US);
Date date = new Date();
String time = simpleDateFormat.format(date);
MessageDigest md5 = MessageDigest.getInstance("MD5");
String seed = time + "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
byte[] digest = md5.digest(seed.getBytes());
StringBuilder hash = new StringBuilder();
for (int r3 = 0; r3 < digest.length; r3++) {
hash.append(String.format("%02x", Byte.valueOf(digest[r3])));
}
return new String[]{time, hash.toString()};
}
到这里就先告一段落,由于自己挖坑,可能之前web端所做的分析都白费了(app端的请求操作相对web端方便了很多),之后将从app端的模拟登陆开始分析起,以上仅仅只是一次记录。