前言

QNDXX 一周一次,每周一凌晨一点后刷新,周日下午六点截止。但是每到周一就开始催观看以及发截图,一直催到最后一天,十分让人反感。开始自动签到,拒绝焦虑,拒绝被@,每周一自动打卡,不用再为打卡而烦恼。


抓包与分析

微信通过 青年之声 公众号进入到团员主页,点击 开始学习

青年之声页面


将链接发送到电脑端发现没有提示未登录,可以正常点击按钮以及观看视频

观察链接地址,发现只传入了一个 sign 参数,将此参数删除提示未登录(无痕浏览)

初步判断页面根据 sign 值判断账号

1
https://youthstudy.12355.net/h5/#/?sign=xxx%xxxx%xxx

未登录提示


抓包工具使用的是 Fiddler ,此页面中找到一个有效的数据包,先记录保存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST https://youthstudy.12355.net/apih5/api/user/get HTTP/1.1
Host: youthstudy.12355.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
X-Litemall-Token:
Content-Type: application/x-www-form-urlencoded
X-Litemall-IdentiFication: young
Content-Length: 122
Origin: https://youthstudy.12355.net
DNT: 1
Connection: keep-alive
Referer: https://youthstudy.12355.net/h5/

sign=xxxxx

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
{
"errmsg": "成功",
"errno": 0,
"data": {
"total": 0,
"pageNo": 1,
"pageSize": 20,
"entity": {
"pageNo": 1,
"pageSize": 20,
"sortBy": "desc",
"orderBy": "update_date",
"beginDate": null,
"endDate": null,
"id": "xxx",
"createDate": "2021-03-15 11:26:46",
"updateDate": "2021-07-11 11:12:44",
"status": 0,
"platform": "young",
"expand": "{}",
"mobile": null,
"phone": "0",
"photo": null,
"nickName": "",
"remarks": null,
"birthday": null,
"sex": 3,
"userType": 0,
"userName": "xxx",
"type": null,
"name": null,
"accountStatus": 0,
"avatarUrl": "",
"trueName": null,
"email": null,
"idCard": null,
"area": null,
"personalLabel": null,
"paymentTerm": null,
"cashAccount": null,
"memberLevel": 1,
"level": null,
"levelNumber": null,
"numberOfShop": 0,
"money": 0.00,
"shopId": null,
"jobNumber": null,
"organizeId": null,
"building": null,
"room": null,
"payment": 0.00,
"commission": 0.00,
"unionId": "",
"age": null,
"qrcode": null,
"lastLoginDevice": "0",
"beginLoginDate": null,
"endLoginDate": "2021-07-11 11:12:44",
"faceId": "xxx",
"operationId": null,
"openId": null,
"appOpenId": "",
"pubOpenId": "",
"isBindEnterprisEmail": 0,
"gender": null,
"openMemberNumber": 0,
"sort": null,
"weChat": "",
"qq": "",
"userIdentity": 0,
"token": "xxxxx",
"integral": 0,
"zanNum": 0,
"postNum": 0,
"organizeIds": null,
"commentNum": 0,
"focusNum": 0,
"fansNum": 0,
"forwardNum": 0,
"roles": null,
"permissions": null,
"rolesName": null,
"canFocus": true,
"isOtherFocus": false,
"isPayPassword": null,
"isOpenCredit": null,
"credit": null,
"yunxinToken": null,
"weight": null,
"height": null,
"industry": null,
"range": null,
"auth": null,
"annexId": null,
"isNew": false,
"froutTime": null,
"isFouce": false,
"referrInfo": null,
"opeAwardMoney": 0,
"typeText": "未知",
"anchorStatus": null,
"anchorAddress": null,
"anchorVideo": null,
"anchorTime": null,
"anchorPrice": null,
"videoAddress": null,
"focusStatus": null,
"goodList": null,
"izyFirstOrgId": null,
"izySecondOrgId": null,
"izyOrgType": null,
"izyFirstOrgName": null,
"izySecondOrgName": null,
"tuanjianUserInfo": null,
"memberBeginDate": null,
"memberEndDate": null,
"between": false
},
"list": []
},
"detailMessage": null,
"debugMessage": null,
"msg": "成功"
}

抓包_主页


继续下一步,点击 参与当期学习 后,信息就很多了

其中包括整个视频,整个音频,以及答题的素材(题目和选项都是图片形式)

在靠前的数据包中看到这一条使用了 token 参数,看链接名称应该用于获取最新的文章

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
{
"errmsg": "成功",
"errno": 0,
"data": {
"total": 0,
"pageNo": 1,
"pageSize": 20,
"entity": {
"pageNo": 1,
"pageSize": 20,
"sortBy": "desc",
"orderBy": "update_date",
"beginDate": null,
"endDate": null,
"id": "xxx",
"createDate": "2021-07-05 11:08:19",
"updateDate": "2021-07-05 11:08:53",
"status": 0,
"platform": "young",
"name": "“青年大学习”第十一季第十六期",
"code": 16,
"sort": 16,
"url": "https://h5.cyol.com/special/daxuexi/ac7fx0es1z/index.html",
"pid": "xxx",
"isEnable": 1,
"between": false
},
"list": []
},
"detailMessage": null,
"debugMessage": null,
"msg": "成功"
}

抓包_新章节


另一条 saveHistory 并不明确意思,同样也用到了 token 。其中 chapterId 在上一个包中有返回。

抓包_记录


视频观看完毕后没有新的数据了,猜测 saveHistory ,就是签到数据。


另外找到一个图片地址:https://h5.cyol.com/special/daxuexi/ac7fx0es1z/images/04.jpg

不及格


推断其他等级为不同的编号,测试 01.jpg,得到:https://h5.cyol.com/special/daxuexi/ac7fx0es1z/images/01.jpg

优秀


后来发现仅本期为此地址,一般的地址应该为 /images/end.jpg


程序编写

数据搜集得差不多了,现在开始编写程序。使用 Python 语言快速实现所需功能,用到的库有 faker json 以及 requests 。首先获取最新章节的 id,得到后通过已知的 sign 获得 token,最后将章节 id 以及 token 数据提交到签到接口。


获取最新章节的 ID

1
2
3
url = "https://youthstudy.12355.net/apih5/api/user/get"
response = r.post(url, headers=headers, data=sign)
ret = json.loads(response.text)["data"]["entity"]["token"]

获取 token

1
2
3
4
url = "https://youthstudy.12355.net/apih5/api/young/chapter/new"
headers = {'X-Litemall-IdentiFication': 'young'}
response = r.get(url, headers=headers)
ret = json.loads(response.text)["data"]["entity"]["id"]

签到

1
2
3
4
headers["X-Litemall-Token"] = token
url = "https://youthstudy.12355.net/apih5/api/young/course/chapter/saveHistory"
data = 'chapterId=' + str(chapter_id)
response = r.post(url, headers=headers, data=data)

完整程序

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
57
58
59
60
61
import json
from urllib import parse
import requests as r
from faker import Factory


def api_request(method, url, headers, data=''):
response = r.request(method, url, headers=headers, data=data)
res = json.loads(response.text)
return res

def sign_in(sign):
mainHeaders = {
'Host': 'youthstudy.12355.net',
'User-Agent': '',
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'X-Litemall-Token': '',
'Content-Type': 'application/x-www-form-urlencoded',
'X-Litemall-IdentiFication': 'young',
'Origin': 'https://youthstudy.12355.net',
'DNT': '1',
'Connection': 'keep-alive',
'Referer': 'https://youthstudy.12355.net/h5/'
}

url_token = "https://youthstudy.12355.net/apih5/api/user/get"
usrl_sign = "https://youthstudy.12355.net/apih5/api/young/course/chapter/saveHistory"

ret = api_request('GET', url_token, mainHeaders, sign)
if int(ret["errno"]) == 0:
token = ret["data"]["entity"]["token"]
else:
print("获取token失败")
print(ret["errmsg"])
exit(1)

fk = Factory.create()
ua = fk.user_agent()
mainHeaders["User-Agent"] = ua
mainHeaders["X-Litemall-Token"] = token
ret = api_request('POST', usrl_sign, mainHeaders, 'chapterId=' + str(chapterId))
if int(ret["errno"]) == 0:
print("签到成功!")
else:
print("签到失败")
print(ret["errmsg"])
mainHeaders["User-Agent"] = ''
mainHeaders["X-Litemall-Token"] = ''


if __name__ == "__main__":
url_chapter = "https://youthstudy.12355.net/apih5/api/young/chapter/new"
chapter_headers = {'X-Litemall-IdentiFication': 'young'}
ret = api_request('GET', url_chapter, chapter_headers)
chapterId = ret["data"]["entity"]["id"]

mysign = "sign=" + parse.quote("xxx")
sign_in(mysign)


自动签到

利用 github action 服务运行 python 程序,并设置运行时间,实现每周自动签到。

点击 Actions 创建一个新的 workflows

action


.github/workflows/dxx.yml

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
# 注意yml文件格式,冒号后面有一个空格

# 名称随意
name: qndxx

# 触发条件
on:
# 在 Action 下会有一个按钮提供手动点击运行
workflow_dispatch:

# 定时任务,依次是‘分,时,日,月,星期’
# 时间是UTC,设置时注意与北京时间的差
schedule:
- cron: '0 0 * * 1'

# jobs层,固定层次格式
jobs:

# 名称随意
MyWork:

# 运行环境设置
runs-on: ubuntu-latest

# steps层,固定层次格式
# 每一步必须有name/uses,name名字随意
steps:
- uses: actions/checkout@v2

- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9

- name: Install dependencies
run: python -m pip install --upgrade pip requests faker

- name: Run QNDXX
run: python qndxx.py

获取完成图

图片地址 = 最新章节地址 + /images/end.jpg,一般情况下是这样的,像我这一期刚好就特殊了。在获取最新章节 ID 的返回数据中,其中一项为 “name”,对应的是网页标题。通过这些信息生成 HTML,图片设置不平铺并拉伸填充整个屏幕。利用 Github Pages 免费静态网页服务提供浏览支持,Github Action 中 Push 上最新生成的 HTML。我的 地址

qndxx.py

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
def html_make():
html = '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" ' \
'content="IE=edge"><meta name="viewport" content="width=device-width, ' \
'initial-scale=1.0"><title>'+chapterTitle+'</title></head><style>body {margin: 0;padding: ' \
'0;width:100%;height:100%;background: url("qndxx.jpg") no-repeat;background-size: 100vw ' \
'100vh;}</style><body></body></html> '
try:
with open('qndxx.html', 'w') as f:
f.write(html) # 写入文件
print("网页生成成功")
except Exception as e:
print("网页生成失败")
print(e)


if __name__ == "__main__":
url_chapter = "https://youthstudy.12355.net/apih5/api/young/chapter/new"
chapter_headers = {'X-Litemall-IdentiFication': 'young'}
ret = api_request('GET', url_chapter, chapter_headers)

chapterId = ret["data"]["entity"]["id"]
chapterURL = ret["data"]["entity"]["url"].replace('https', 'http').replace('index.html', r'images/end.jpg')
chapterTitle = ret["data"]["entity"]["name"]
resp = r.get(url=chapterURL)
try:
with open('qndxx.jpg', 'wb') as f:
f.write(resp.content) # 写入文件
print("图片获取成功")
html_make()
except Exception as e:
print("图片获取失败")
print(e)

mysign = "sign=" + parse.quote("xxx")
sign_in(mysign)

.github/workflows/dxx.yml

1
2
3
4
5
6
7
8
9
10
11
- name: Push ready
run: |
git config --global user.email xxx
git config --global user.name xxx
git add .
git commit -m "Timed task"

- name: Push
uses: ad-m/github-push-action@v0.6.0
with:
github_token: ${{secrets.TOKEN}}