Pixiv爬虫分析记录

发布于 2018-07-22  563 次阅读


仅仅只是忠实记录开发过程,最终教程另见

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查看网页源码,找到表单部分,会发现几个隐藏域

<form action="/login" method="POST"><input type="hidden" name="post_key" value="77bfb3fb0ba5b46bea1d1c1e2134135b">
    <input
            type="hidden" name="return_to" value="https://www.pixiv.net/"><input type="hidden" name="lang" value="zh">
    <input
            type="hidden" name="source" value="pc">
    <div class="input-field-group">
        <div class="input-field"><input type="text" name="pixiv_id" placeholder="邮箱地址/pixiv ID" autocapitalize="off">
        </div>
        <div class="input-field"><input type="password" name="password" placeholder="密码" autocapitalize="off"></div>
    </div>
    <ul class="error-msg-list"></ul>
    <button type="submit" class="signup-form__submit">登录</button>
    <div class="signup-form-nav">
        <div class="left"></div>
        <div class="right"><a href="https://www.pixiv.net/reminder.php" target="_blank">忘记了</a></div>
    </div>
</form>

换浏览器多次抓包后基本实锤了,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,预计在开发中若返回错误,换参递归调用自身)

3)实现非会员热门度搜索

众所周知,在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用于校验:

  • X-Client-Hash为基于时间的加密字符串
  • X-Client-Time为pixiv规定格式的时间信息
  • Authorization猜测为类似sessionid

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.<init>(r1, r2);
            r1 = new java.util.Date;
            r7 = 4;
            r1.<init>();
            r0 = r0.format(r1);
            r7 = 0;
            r1 = new java.lang.StringBuilder;
            r1.<init>();
            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.<init>();	 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端的模拟登陆开始分析起,以上仅仅只是一次记录。

 

 

 

 

 

 


面向ACG编程