缘由

最近有点自闭。绩点虽然还可以,但是竞赛参加得少,奖学金加分少,奖学金也评不上一二等了。另外一个我们班很优秀的同学还跟我讲了一些干货,让我认清了自己有多菜的现实,就更自闭了。想着自己再这么菜下去就完了,于是又重新开始在百词斩背英语。为了激励自己,不要再三分钟热度,我就想把百词斩里背单词的“ 坚持天数 ” 挂在自己平时看的到的地方。

百词斩没有网页版,只有手机端。所以想要获得 “ 坚持天数 ” ,就只能去抓包找接口了。

抓包

对 iOS 抓包当然要用 Charles 了。我们在手机上把代理服务器设置好,然后点开百词斩里的打卡页面,就可以在 Charles 上捕获一个向

1
http://learn.baicizhan.com/rpc/user_study/user_daka_v2/1569296374384

发送的 POST。大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /rpc/user_study/user_daka_v2/1569296731407 HTTP/1.1
Host: learn.baicizhan.com
Accept: application/x-thrift
Content-Type: application/x-thrift
Cookie: device_name=iPhone10.3; device_version=13.1; device_id=; app_name=6030000; access_token=; serial=470EA08F-C24114531; os_version=13.1; app_version=6030000; channel=appstore; client_time=1569296731
User-Agent: Cocoa/THTTPClient bcz_app_iphone/6030000
Accept-Language: zh-cn
Accept-Encoding: gzip,deflate,sdch
Content-Length: 320
Connection: keep-alive

  <‚! user_daka_v2ˆÆ0€°ÇØ 
Asia/ShanghaiÆ0€°ÇØ üø¡
 € ¦r Þ¥ Øî Š  Ä  Ê  Ú  î  ¬  Þ  Ž  š  î  ’  š  Ì  ô  Š"  ®"  Æ.  Î/  †@  ”@  Ú@  ü@  ÀA  ÖA  úA    

主要关注点在这里

  • POSTURL 是做了一个拼接的,在基础地址的后面加上了一个毫秒时间戳,规则是 :
1
2
import time
url = "http://learn.baicizhan.com/rpc/user_study/user_daka_v2/{}".format(str(int(time.time() * 1000)))
  • POSTCookie 中有两个参数要注意:
    • client_time 参数是当前的秒级时间戳
    • serial 也是随着时间在改变,但目前没有找到它的计算公式。最后实际操作中发现服务器端貌似并没有对这个值作校验,所以这个值的计算问题我们现在暂时先跳过。
  • POST DATA,首先这个值是不会变化的,所以原则上我们只需要把抓到的包里面的值复制过去就可以了。但实际操作中遇到一个问题就是,如果直接复制到字符串里的话, python 会报错字符串初始化失败,原因是没有找到 EOF。所以我们这里要复制十六进制码,然后在 POST 的时候直接把这十六进制码转换成 char 后传输。
1
2
3
4
5
6
import binascii
import requests

json_data = "0000013C 8221000C 75736572 5F64616B 615F7632 1C150815 8803191C 15C61F15 12153016 80B0C7D8 0B00180D 41736961 2F536861 6E676861 691C15C6 1F151215 301680B0 C7D80B00 19FC1E15 F8A10115 04150A00 15800115 02150800 15A67215 02150800 15DEA501 15021508 0015D8EE 01150215 0800158A 03150015 040015C4 03150015 060015CA 03150015 060015DA 08150015 040015EE 08150015 040015AC 0F150015 060015DE 0F150015 0600158E 10150015 0600159A 11150015 060015EE 12150015 06001592 18150015 0600159A 1B150015 060015CC 1E150015 060015F4 1E150015 0600158A 22150015 060015AE 22150015 060015C6 2E150015 060015CE 2F150015 06001586 40150015 06001594 40150015 040015DA 40150015 060015FC 40150015 040015C0 41150015 040015D6 41150015 060015FA 41150015 06000000".replace(
    ' ', '')
r_data = requests.post(base_url, headers=headers, data=binascii.unhexlify(json_data))

这三个问题解决了就可以得到接口返回的数据了。

处理返回数据

访问这个接口的到的返回数据是这样的:

1
2
3
response.text="""
  0‚A user_daka_v2 Žä»Šå¤©æˆ‘在#百词斩#学了《托福词汇》196个单词,已坚持14天。你也来背单词吧 >>> 下载百词斩: http://t.cn/EPHIRea Dhttp://7n.bczcdn.com/daka_images/v201907tmp/daka_20190802_14_day.pngD我已经在百词斩上坚持了14天,今日过招196个单词。]http://learn.baicizhan.com/daka_page/weixin?sign=dG9kYXk9MTk2JmRheXM9MTQmdW5pZD0xMTcwMjUxNTc4X-/daka_images/800x1280/v1/c11cc937059ff17c.jpg-/daka_images/800x1280/v1/d677e9cc53323c6f.jpg-/daka_images/800x1280/v1/1389fab558257464.jpg-/daka_images/800x1280/v1/b98da0bbe035af90.jpg-/daka_images/800x1280/v1/a68661e80316ca47.jpg\http://learn.baicizhan.com/daka_page/qzone?sign=dG9kYXk9MTk2JmRheXM9MTQmdW5pZD0xMTcwMjUxNTc4lÆ&‚Ò’Ø Æ(‚˜Ø Æ*‚Þ§Ø Æ,‚¤²Ø Æ.‚ê¼Ø Æ0‚°ÇØ  
"""

我们把这个返回数据编码为 utf-8是这样的:

1
response.content.decode("utf-8") = ...user_daka_v2...今天我在#百词斩#学了《托福词汇》196个单词,已坚持14天。你也来背单词吧 >>> 下载百词斩: http://t.cn/EPHIRea Dhttp://7n.bczcdn.com/daka_images/v201907tmp/daka_20190802_14_day.pngD我已经在百词斩上坚持了14天,今日过招196个单词。]http://learn.baicizhan.com/daka_page/weixin?sign=dG9kYXk9MTk2JmRheXM9MTQmdW5pZD0xMTcwMjUxNTc4X-/daka_images/800x1280/v1/c11cc937059ff17c.jpg-/daka_images/800x1280/v1/d677e9cc53323c6f.jpg-/daka_images/800x1280/v1/1389fab558257464.jpg-/daka_images/800x1280/v1/b98da0bbe035af90.jpg-/daka_images/800x1280/v1/a68661e80316ca47.jpg\http://learn.baicizhan.com/daka_page/qzone?sign=dG9kYXk9MTk2JmRheXM9MTQmdW5pZD0xMTcwMjUxNTc4...

可以看到我们要找的数字 14 就已经出来了。我们现在要做的就是把 14 提取出来,提取方法有一万种,这里就说说我的方法。

我们查一下已坚持utf-8 编码是 \xe5\x9d\x9a\xe6\x8c\x81\xe4\xba\x86\xe5\xa4\xa9,我们就可以用正则 r'e59d9ae68c81e4ba86[\d]{1,}e5a4a9' 来匹配字节码。

Python 源代码

整个过程的源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import binascii
import time

import requests
import re

cur_time = time.time()
day = ""
base_url = "http://learn.baicizhan.com/rpc/user_study/user_daka_v2/{}".format(str(int(cur_time * 1000)))
base_cookie = "device_name=iPhone10.3; device_version=13.1; device_id=; app_name=6030000; access_token=; serial=470EA08F-C24112413; os_version=13.1; app_version=6030000; channel=appstore; client_time={}"
headers = {'Host': 'learn.baicizhan.com',
           'Accept': 'application/x-thrift',
           'Content-Type': 'application/x-thrift',
           'Cookie': base_cookie.format(str(int(cur_time))),
           'User-Agent': 'Cocoa/THTTPClient bcz_app_iphone/6030000',
           'Accept-Encoding': 'gzip,deflate,sdch',
           'Accept-Language': 'zh-cn'}

json_data = "0000013C 8221000C 75736572 5F64616B 615F7632 1C150815 8803191C 15C61F15 12153016 80B0C7D8 0B00180D 41736961 2F536861 6E676861 691C15C6 1F151215 301680B0 C7D80B00 19FC1E15 F8A10115 04150A00 15800115 02150800 15A67215 02150800 15DEA501 15021508 0015D8EE 01150215 0800158A 03150015 040015C4 03150015 060015CA 03150015 060015DA 08150015 040015EE 08150015 040015AC 0F150015 060015DE 0F150015 0600158E 10150015 0600159A 11150015 060015EE 12150015 06001592 18150015 0600159A 1B150015 060015CC 1E150015 060015F4 1E150015 0600158A 22150015 060015AE 22150015 060015C6 2E150015 060015CE 2F150015 06001586 40150015 06001594 40150015 040015DA 40150015 060015FC 40150015 040015C0 41150015 040015D6 41150015 060015FA 41150015 06000000".replace(
    ' ', '')
r_data = requests.post(base_url, headers=headers, data=binascii.unhexlify(json_data))
# print(r_data.content.hex())
searchObj = re.search(r'e59d9ae68c81e4ba86[\d]{1,}e5a4a9', r_data.content.hex())
if searchObj:
    data = searchObj.group(0)[18:len(searchObj.group(0)) - 6]
    data_extra = [data[i: i+2] for i in range(0, len(data), 2)]
    for d in data_extra:
        day = day + str(int(d) - 30)
    print(day)

部署

因为我的服务器主要还是用 php 来提供 API,所以为了部署代码到服务器上,我需要先把 python 代码转换成 php 代码·。转换完后是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php

function getCurrentMilis() {
    $mill_time = microtime();
    $timeInfo = explode(' ', $mill_time);
    $milis_time = sprintf('%d%03d',$timeInfo[1],$timeInfo[0] * 1000);
    return $milis_time;
}

$current_milis = getCurrentMilis();

$headers = array(
    "Host: learn.baicizhan.com",
    "Accept: application/x-thrift",
    "Content-Type: application/x-thrift",
    "Cookie: device_name=iPhone10.3; device_version=13.1; device_id=; app_name=6030000; access_token=; serial=470EA08F-C24112413; os_version=13.1; app_version=6030000; channel=appstore; client_time=".floor(getCurrentMilis() / 1000),
    "User-Agent: Cocoa/THTTPClient bcz_app_iphone/6030000",
    "Accept-Encoding: gzip,deflate,sdch",
    "Accept-Language: zh-cn"
);

function hextostr($hex){
    $string = "";
    for ($i = 0; $i < strlen($hex) - 1; $i += 2)
        $string .= chr(hexdec($hex[$i].$hex[$i+1]));
    return $string;
}

$hex_request_data = "0000013C8221000C757365725F64616B615F76321C1508158803191C15C61F151215301680B0C7D80B00180D417369612F5368616E676861691C15C61F151215301680B0C7D80B0019FC1E15F8A1011504150A00158001150215080015A672150215080015DEA501150215080015D8EE011502150800158A03150015040015C403150015060015CA03150015060015DA08150015040015EE08150015040015AC0F150015060015DE0F1500150600158E101500150600159A11150015060015EE1215001506001592181500150600159A1B150015060015CC1E150015060015F41E1500150600158A22150015060015AE22150015060015C62E150015060015CE2F15001506001586401500150600159440150015040015DA40150015060015FC40150015040015C041150015040015D641150015060015FA4115001506000000";

$curl = curl_init();
curl_setopt($curl,  CURLOPT_URL, "http://learn.baicizhan.com/rpc/user_study/user_daka_v2/".$current_milis);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, hextostr($hex_request_data));
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLINFO_HEADER_OUT, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl, CURLOPT_ENCODING, "");

// 这两行是为了设置代理服务器,方便抓包 debug
// curl_setopt($curl, CURLOPT_PROXY, "127.0.0.1");
// curl_setopt($curl, CURLOPT_PROXYPORT, "8888");

$data = curl_exec($curl);
curl_close($curl);

preg_match('~已坚持[\d]{1,}天~', $data, $matches);

if (count($matches) > 0) {
    echo substr($matches[0], 9, strlen($matches[0]) - 12);
} else {
    echo "-1";
}

完成

访问这个 API 地址,坚持天数就被成功返回回来了。接下来有了数据,做一个界面来展示也不是什么很难的事情了,这部分就留给读者自己发挥了。



发现存在错别字或者事实错误?请麻烦您点击 这里 汇报。谢谢您!