书蜗抢座爬虫

懒癌福音, 书蜗爬虫[懒癌你还去个毛图书馆啊摔]

前言

谢大佬在上学期期末迷上了去 B208 自习, 蓝鹅却时常抢不到座位, 于是用请客收买了我让我写了一个爬虫. 爬虫不难写, 就是书蜗是手机 App, 抓包有点麻烦而已. 刚开始想在 ios 端抓, 穷逼没有 mac 也买不起 ios 端的抓包软件(几百块一个), 于是想在电脑开热点, 结果校园网开热点失败, 原因未知. 手边还有一台红米 1 好久没开机了, 估计也不咋好使. 想来想去还是安卓模拟器好. 安卓端有个抓包软件, 叫 Packet Capture, 就长这样
app
还不错, 缺点是不能批量导出, 不能分类查看(图片, js, css, html), 以及有时候会出错. 不知道是不是模拟器的问题, 还不支持复制...手打链接美滋滋
抓完包看了一下, 书蜗与服务器之前的通信还是很简洁的, 所以没费多少时间(不到 3h)就写完了, 所以建议自己动动手, 可以作为爬虫入门.

你需要

  1. 安卓模拟器(我用的是逍遥安卓模拟器)
  2. Packet Capture
  3. Python2.7
  4. requests (pip install requests)
  5. yagmail (pip install yagmail)

分析过程

懒得每个包都截图了, 重点的通信过程如下:

  1. 输入用户名, 密码
    登录
    请求学生信息(返回 json)
    请求图书馆信息(返回 json)
    检查学生是否通过入学考试(返回 json)

  2. 点击"空间"->"预约座位"
    获取分馆(只有南馆)(返回 json)

  3. 点击"自助选座"
    查询分馆里所有预约教室所在楼层(只有 F2)(返回 json)

  4. 点击"自助选座"
    查询选定楼层里所有预约教室(只有 B208)(返回 json)

  5. 点击"自助选座"
    获取座位分布(返回 html)

  6. 点击某个座位, 确认选座
    提交座位数据, 成功预约(返回 json)

包长这样:
示例包

很好理解, 其中'name'就是参数名

返回的是大家喜闻乐见的 json
返回的 json

模拟每个包发送的过程无非就是
通信链接
post/get/others
参数

而接收的时候, 提取出所需要的信息就行了(如下一次通信所需的参数)

大同小异, 这么多包就不一一说明了.
抓一下包你就会一目了然
而有一点偷懒了, 由于南校区的图书馆就一间自习室, 所以 roomid, floorid 都是固定的, 我就没有每次运行的时候去获取, 而是直接赋值, 如果他们改变会导致代码失效, 就要重新分析第 2-4 步,
还有一些不必要的地方也省略了.
代码通信过程如下:

  1. 输入用户名, 密码
    登录

  2. 点击"自助选座"
    获取座位分布(返回 html)

  3. 点击某个座位, 确认选座
    提交座位数据, 成功预约(返回 json)

代码

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# -*- coding:gbk -*- 
import requests
import pprint,re
import time,os
import yagmail

class Sw():
def __init__(self, username, password, dayTime, seatid):
self.username = username
self.password = password
self.dayTime = dayTime
self.seatid = seatid
self.host = 'http://t1.beijingzhangtu.com'
self.appointmentStartTime = '08:00'
self.appointmentEndTime = '22:00'

def Login(self):
loginPayload = {
'phone': self.username,
'pass': self.password
}

login_json = requests.post(self.host+'/api/user/loginByPhone.html', data=loginPayload).json() #登录

assert int(login_json['code']), login_json['msg']
print login_json['msg']

self.userId = login_json['data']['id']
#pprint.pprint(login_json)
self.libraryId = login_json['data']['libraries'][0]['id']
self.token = login_json['data']['token']

def getStuInfo(self):
page = '1'
ListPayload = {
'page': page,
'userId': self.userId,
'libraryId': self.libraryId,
'token': self.token
}
List_json = requests.post(self.host+'/api/userLibrary/getList.html', data=ListPayload).json() #学生信息
studentName = List_json['data'][0]['cardusername']
print studentName, u'你好'

def RoomInfo(self):
SeatsPayload = {
'roomid': '36', #南校区只有 B208
'appointmentDay': self.dayTime,
'libraryid': self.libraryId,
}

Seats_html = requests.get(self.host+'/library/seatview/seatList.html', params=SeatsPayload).text #座位列表
SelectableList = re.findall(r"'([0-9][0-9][0-9][0-9])'", Seats_html)
print u'可选座位有', len(SelectableList), u'个'

return self.seatid in SelectableList


def getAppointmentInfo(self):
AppointmentPayload = {
'libraryid': self.libraryId,
'userid': self.userId,
'token': self.token
}
Appointment_json = requests.post(self.host+'/api/YySeatAppointment/getUserAppointmentInfoes.html', data=AppointmentPayload).json() #预约信息

if Appointment_json['msg']:
print Appointment_json['msg']
return 0
else:
self.seatAppointmentId = Appointment_json['data'][0]['keyid']
return 1


def AppointSeat(self, seatid):
self.seatid = str(int(seatid) + 3935)
#if self.RoomInfo():
AppointSeatPayload = {
'appointmentStartTime': self.appointmentStartTime,
'appointmentEndTime': self.appointmentEndTime,
'appointmentDay': self.dayTime,
'seatid': self.seatid,
'libraryid': self.libraryId,
'userid': self.userId,
'token': self.token
}
AppointSeat_json = requests.post(self.host+'/api/YySeatAppointment/addes.html', data=AppointSeatPayload).json() #抢座
print AppointSeat_json['msg']
if int(AppointSeat_json['code']):
print u'预约的座位号为', AppointSeat_json['data']['num'], u', 别忘了 30min 过去签到'
return [1, u'预约的座位号为' + AppointSeat_json['data']['num'] + u', 别忘了 30min 过去签到']
else:
return [0]

#else:
#print '你要的座位有人预约了'
#return [0]


def RandomAppointment(self):
RandomPayload = {
'floorid': '9',
'roomid': '36',
'appointmentStartTime': self.appointmentStartTime,
'appointmentEndTime': self.appointmentEndTime,
'buildingid': '8',
'appointmentDay': self.dayTime,
'libraryid': self.libraryId,
'userid': self.userId,
'token': self.token
}

print RandomPayload
Random_json = requests.post(self.host+'/api/YySeatAppointment/autoAppointmentes.html', data=RandomPayload).json() #随机预约
print Random_json['msg']
if int(Random_json['code']):
print u'随机预约的座位号为', Random_json['data']['num'], u', 别忘了 30min 过去签到'
return [1, u'随机预约的座位号为' + Random_json['data']['num'] + u', 别忘了 30min 过去签到']
else:
return [0]


def Cancle(self):
if self.getAppointmentInfo():
cancelPayload = {
'seatAppointmentId': self.seatAppointmentId,
'libraryId': self.libraryId,
'userId': self.userId,
'token': self.token
}

cancel_json = requests.post(self.host+'/api/YySeatAppointment/cancelAppointmentes.html', data=cancelPayload).json() #取消预约
print cancel_json['msg']


def SpyOneSeat(self, seatid): #持续监视指定座位
self.Login()
while 1:
try:
Info = self.AppointSeat(seatid)
except:
self.Login() #token 失效
Info = self.AppointSeat(seatid)

if Info[0]:
print '-'*10, u'成功抢到座位', '-'*10
content = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + u' 成功抢到座位! ' + Info[1]
self.Email(content.encode('utf8'))
return 1

time.sleep(2)
os.system('clear')



def SpyAllSeat(self): #持续监视所有座位
self.Login()

while 1:
try:
Info = self.RandomAppointment()
except:
self.Login()
Info = self.RandomAppointment()

if Info[0]:
print '-'*10, u'成功抢到座位', '-'*10
content = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + u' 成功抢到座位! ' + Info[1]
self.Email(content.encode('utf8'))
return 1

time.sleep(60)
os.system('clear')


def affirmSeat(self, seatid):
affirmPayload = {
'libraryid': self.libraryId,
'seatid': str(int(seatid) + 3935),
'token': self.token,
'userid': self.userId,
}
affirm_json = requests.post(self.host+'/api/YySeatAppointment/affirmSeat.html',data = affirmPayload).json()
print affirm_json['msg']
if u'异常' in affirm_json['msg']:
return 0
else:
return 1


def Release(self, seatid):
sw.getAppointmentInfo()
keyid = self.seatAppointmentId

ReleasePayload = {
'libraryid':self.libraryId,
'seatAppointmentId': keyid,
'token': self.token,
'userid': self.userId,
}
release_json = requests.post(self.host+'/api/YySeatAppointment/releaseBySelfes.html', data = ReleasePayload).json()
print release_json['msg']
if u'异常' in release_json['msg']:
return 0
else:
return 1


def SpyTime(self, Time, seatid): #指定时间抢座, 如果使用 SpyOneSea(), 要添加参数 seatid
print u'等待时间到', Time
while 1:
if time.strftime("%H:%M:%S", time.localtime()) == Time:
print '-'*10, u'时间到!', '-'*10
#self.SpyAllSeat()
#self.SpyOneSeat(seatid)
break

def Email(self, content):
yag = yagmail.SMTP(user = '发送方邮箱', password = '发送方邮箱密码', host = '邮箱 host', port = '25') #如 139 邮箱 host 是 smtp.139.com
yag.send(to = "接收方邮箱", subject = "Troy's Sw_Spider", contents = content)
print 'Email successfully!'


username = '***********' #账号
password = '*********' #密码
dayTime = '2017-08-09' #预约日期(格式要对)
seatid = '160' #预约座位号
Time = '10:12:05' #预约时间(格式要对)

sw = Sw(username, password, dayTime, seatid)
sw.Login() #登录
#sw.RandomAppointment() #随机选择空座位
sw.getStuInfo() #获取学生信息
sw.getAppointmentInfo() #获取预约信息
sw.RoomInfo() #获取自习室信息
#sw.AppointSeat(seatid) #预约指定座位
#sw.affirmSeat(seatid) #确认入座
#sw.Release(seatid) #释放座位
#sw.SpyOneSeat(seatid) #监视指定座位
#sw.SpyAllSeat() #监视所有座位
#sw.SpyTime(Time, seatid) #指定时间抢座
#sw.Cancle() #取消预约的座位

使用说明

  1. 首先创建一个对象
    sw = Sw(username, password, dayTime)
  2. seatid 为座位号(南馆, F2, B208, 座位号为 1-160)
  3. 时间, 日期格式要正确

已有功能

已有功能封装好了, 可以直接调用

监视指定座位

预约指定的座位, 如果被占了将持续监测(间隔 60s), 直到占到这个座位
调用 SpyOneSeat(seatid)即可
最好双击运行

监视所有座位

一旦有空位就选择预约, 座位是随机选择的. 如果整个教室都没有空位,则持续监测(间隔 60s), 直到有位置
调用 SpyAllSeat()即可
最好双击运行

指定时间抢座

当时间到达某一时刻(精确到秒), 开始抢座. 注意, SpyTime()中, 要指定一下利用 SpyAllSeat()抢座还是利用 SpyOneSeat(seatid)抢座
调用 SpyTime(Time, seatid)即可
最好双击运行

确认入座

确认入座本来是在自习室打开蓝牙才能完成的,但是由于书蜗的逻辑简单,只是发包给服务器进行确认,所以我们在任何地方都可以确认入座

调用 affirmSeat 确认入座

调用 Release 释放座位

书蜗的座位在被预约后会自动倒计时 30min,一旦 30min 后还没被确认入座,那么系统会自动释放这个座位。我们把 affirmSeat 加入到抢座的函数其中即可实现抢座后自动确认入座。这样那个座位就一直是你的了(毒瘤啊

零件

如果你想自己组装一个功能, 那么可以创建对象后进行调用
比如想在抢到座位时发送邮件到指定邮箱, 那么调用 Email(content)即可

登录

调用 Login()即可

获取学生信息

  1. 先登录, sw.Login()
  2. 调用 getStuInfo()

获取已预约信息

  1. 先登录, sw.Login()
  2. 调用 getAppointmentInfo()

获取自习室信息

  1. 先登录, sw.Login()
  2. 调用 RoomInfo()

预约指定座位

直接预约指定的座位

  1. 先登录, sw.Login()
  2. 调用 SpyOneSeat(seatid)

随机选择空座位

随机选择空座位

  1. 先登录, sw.Login()
  2. 调用 RandomAppointment()

取消预约的座位

  1. 先登录, sw.Login()
  2. 调用 Cancle()

确认入座

  1. 先登录, sw.Login()
  2. 调用 affirmSeat

释放座位

  1. 先登录, sw.Login()
  2. 调用 Release

发送邮件

Email(content) #content 为邮件正文

还不够?

对于每次返回的 json, 我提取了我认为重要的信息. 如果你还要更多 json 里的信息, 可以使用 pprint.pprint(返回的 json) 来查看具体的 json.

测试一下

结果
邮件

注意

  1. token 是最重要的参数, 如果你没有重新登录, token 不会改变, 可以重复使用, 所以由于你运行了代码, 更新了 token, 手机端的 app 会出现提示:
    错误
    我猜 app 可能在手机本地保存了 token(设置一个有效期), 这样就不需要每次登录, 带着 token 去访问就好了(听起来像 cookie).
    懒得再抓包验证, 思路大概是 打开书蜗, 退出登录, 抓登录包, 记下 token, 退出书蜗, 再次打开书蜗, 抓书蜗打开时的包, 记下 token, 对比即可
    同样的道理, 脚本在持续运行的时候使用手机 app 登录的话, 会返回 '当前 token 失效' 的信息.

  2. 座位分布
    座位分布
    3936-4095 对应的是 1-160(post 的 seatid 是 3936-4095), 减去 3935 即可

  3. 反爬检测
    貌似没有 所以时间可以设置为 60s, 甚至更短

  4. 辣鸡邮件
    有时候你的接收邮箱会把这个代码发出去的邮件视为辣鸡邮件, 你就需要把发送方的邮件添加到接收方的联系人里

  5. 代码失效
    由于我偷懒, 没写那么多的故障检测, 也许书蜗后台大更新一下就不能用了, 到时候看看再更新吧, 反正我也用不了几年了, 挖个坑留给学弟学妹吧~~

  6. 上文里的学生信息应该是借书证, 而一个账号可以添加多个借书证, 代码默认使用第一个借书证, 正好就是南馆的借书证. 如有其它需要自己分析一下 json 即可.

后语

Emmmmmm...等着谢大佬请客咯...


来呀快活呀


书蜗抢座爬虫
https://www.tr0y.wang/2017/08/09/SwSpider/
作者
Tr0y
发布于
2017年8月9日
更新于
2024年4月19日
许可协议