利用 Python 实现自动抢报告
上了研究生才知道北化的研究生每年需要听 15 个报告,而且最重要的是这些报告都是算分 数的,而且更重要的是你的分数是和最后的奖学金评定挂钩的。这也就决定了大家爲了那麽 点报告分数挤教务网都挤破了头(北化的服务器大家都懂),虽然场面不如开学抢课那麽火 爆,却需要长时间挂着教务网疯狂刷新,爲了交换个讲座也需要大半夜起来,生怕被别人截 胡。爲了解放生产力,同时熟悉爬虫技术、神经网络技术、装饰器等进阶内容,写了这个爬 虫来练练手。
0.1 声明
该爬虫是本人 (Cycoe) 练习 python 编程技巧和神经网络所编写,使用造成的任何责任与 本人无关
0.2 问题分解
想要顺利的拿到抢讲座的 session,需要如下的步骤:
- 处理登陆问题,包括处理验证码、页面表单和 cookies
- 获取报告列表
- 获取抢报告的地址
- 提交表单数据
0.3 框架设计
好的框架应具有良好的可维护性和扩展性,目前正朝着这个方向努力
- 採用交互式的命令行设计,分离 login, robSpeech 等方法
- 将 login, robSpeech, robClass 等方法封装成 Robber 对象
- 将底层的 requests 封装成 Spider 对象
0.4 逐步解决
0.4.1 构造 Headers
通过 firefox 或 chrome 的 debug 模式可以查看在访问网页时的 request 和 response。仿照浏览器在访问教务网时提交的 headers 构造如下字典
class Spider(object):
@staticmethod
def formatHeaders(referer=None, contentLength=None, originHost=None):
"""
封装请求的 headers
:param referer: 跳转标记,告诉 web 服务器自己是从哪个页面跳转过来的
:param contentLength: 作用未知
:param originHost: 原始主机地址
:returns: headers 字典
"""
headers = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7',
'Cache-Control': 'max-age=0',
'Connection': 'keep-alive',
'Content-Type': 'application/x-www-form-urlencoded',
'DNT': '1',
'Host': 'graduate.buct.edu.cn:8080',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (X11;Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.79 Safari/537.36',
'Referer': referer,
'Content-Length': contentLength,
'Origin': originHost,
}
0.4.2 构造请求对象
封装 request 请求需要的 prepareBody 对象, response = session.send(prepareBody)
,response 就是我们拿到的服务器的响应对象,可通过 response.text
得到网页内容, response.status_code
得到状态码, response.url
得到响应的地址。
def prepare(self, referer=None, originHost=None, method='GET',
url=None, data=None, params=None):
""" 生成用于请求的 prepare
:param referer: 跳转标记,告诉 web 服务器自己是从哪个页面跳转过来的
:param originHost: 原始主机地址
:param method: 请求方法 in ['GET', 'POST']
:param url: 请求的 url 地址
:param data: 封装的 post 数据
:param params: post 参数
:return: prepare 对象
"""
headers = self.formatHeaders(referer=referer, originHost=originHost)
req = Request(method, url, headers=headers, data=data, params=params)
return self.session.prepare_request(req)
prepareBody = prepare(
referer=None, originHost=None, method='GET',
url=UrlBean.jwglLoginUrl, data=None, params=None
)
response = session.send(prepareBody)
0.4.3 请求登录
请求登录网址,提交表单数据。根据 response 返回的响应体内容判断是否登陆成功。
def getVIEWSTATE(self):
""" 正则获取页面的 __VIEWSTATE
:returns: 页面的 __VIEWSTATE
"""
VIEWSTATE = re.findall('<.*name="__VIEWSTATE".*value="(.*)?".*/>', self.response.text)
if len(VIEWSTATE) > 0:
return VIEWSTATE
else:
return None
def getEVENTVALIDATION(self):
""" 正则获取页面的 __EVENTVALIDATION
:returns: 页面的 __EVENTVALIDATION
"""
EVENTVALIDATION = re.findall('<.*name="__EVENTVALIDATION".*value="(.*)?".*/>', self.response.text)
if len(EVENTVALIDATION) > 0:
return EVENTVALIDATION
else:
return None
def login(self):
""" 登录教务网 """
# 在登录前请求一次登录页面,获取网页的隐藏表单数据
prepareBody = self.prepare(referer=None,
originHost=None,
method='GET',
url=UrlBean.jwglLoginUrl,
data=None,
params=None)
# 登陆主循环
while True:
self.response = self.session.send(prepareBody)
self.VIEWSTATE = self.getVIEWSTATE()
self.EVENTVALIDATION = self.getEVENTVALIDATION()
if self.VIEWSTATE is not None and self.EVENTVALIDATION is not None:
break
Logger.log("Retrying fetching login page viewState...", level=Logger.warning)
reInput = True # 是否需要重新输入用户名和密码
while True:
# 输入用户名和密码
if reInput:
if Config.checkUserFile():
Config.readUserInfo()
else:
Config.userName = input("> UserName: ")
Config.password = input("> Password: ")
reInput = False
prepareBody = self.prepare(referer=UrlBean.jwglLoginUrl,
originHost=None,
method='GET',
url=UrlBean.verifyCodeUrl,
data=None,
params=None)
while True:
codeImg = self.session.send(prepareBody) # 获取验证码图片
if codeImg.status_code == 200:
break
else:
Logger.log("retrying fetching vertify code...", level=Logger.warning)
with open('check.gif', 'wb') as fr: # 保存验证码图片
for chunk in codeImg:
fr.write(chunk)
print_vertify_code()
verCode = input("input verify code:")
# verCode = self.classifier.recognizer("check.gif") # 识别验证码
# 构造登陆表单
postData = {
'__VIEWSTATE': self.VIEWSTATE,
'__EVENTVALIDATION': self.EVENTVALIDATION,
'_ctl0:txtusername': Config.userName,
'_ctl0:txtpassword': Config.password,
'_ctl0:txtyzm': verCode,
'_ctl0:ImageButton1.x': '43',
'_ctl0:ImageButton1.y': '21',
}
prepareBody = self.prepare(referer=UrlBean.jwglLoginUrl,
originHost=UrlBean.jwglOriginUrl,
method='POST',
url=UrlBean.jwglLoginUrl,
data=postData,
params=None)
# 获取登陆 response
while True:
self.response = self.session.send(prepareBody)
if self.response.status_code == 200:
break
# 根据返回的 html 判断是否登录成功
if re.search('用户名不存在', self.response.text):
Logger.log('No such a user!', ['Cleaning password file'], level=Logger.error)
print(OutputFormater.table([['No such a user!'], ['Cleaning password file']], padding=2))
Config.cleanUserInfo()
reInput = True
elif re.search('密码错误', self.response.text):
Logger.log('Wrong password!', ['Cleaning password file'], level=Logger.error)
print(OutputFormater.table([['Wrong password!'], ['Cleaning password file']], padding=2))
Config.cleanUserInfo()
reInput = True
elif re.search('请输入验证码', self.response.text):
Logger.log('Please input vertify code!', ['Retrying...'], level=Logger.error)
print(OutputFormater.table([['Please input vertify code!'], ['Retrying...']], padding=2))
elif re.search('验证码错误', self.response.text):
Logger.log('Wrong vertify code!', ['Retrying...'], level=Logger.error)
print(OutputFormater.table([['Wrong vertify code!'], ['Retrying...']], padding=2))
else:
Logger.log('Login successfully!', ['UserName: ' + Config.userName, 'Password: ' + Config.password], level=Logger.warning)
print(OutputFormater.table([['Login successfully!']], padding=2))
Config.dumpUserInfo()
break
0.4.4 拿到 session
拿到已登陆的 session 后,抢课和抢报告都是非常方便的,只要按照浏览器提交的数据构造 headers 和表单数据后就可以获得正常的 response
0.5 暗坑总结
- 刚开始抓到的网页内容中文都是乱码,后来 google 解决,发现是 python 的编码和 asp 框架的编码问题造成的,python 中的编码问题真的是让人头大
- 由于网站的防爬虫设计,会在 html 源码中插入很多隐藏的表单数据,如此处的
__VIEWSTATE
和__EVENTVALIDATION
,这两个是非常重要的参数。否则无法成功登陆 - 两次访问之间要有一定的时间间隔,如此处用了一个随机函数的闭包来获得随机时间的间隔
- 使用装饰器解决了在访问抢课网页前判断登录的问题
- 接下来将循环封装成函数,加入最大循环次数和超时
- 完善边界检查和异常处理
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 一种通过最小二乘法求转变点的方法
下一篇: 如何同步 GitHub 上游更新
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论