Python爬虫实战2:渤大教务课表邮件及模拟登陆

本人大学教务系统必须连接校园网或者使用校方给的VPN进行操作。

此次目标网址:http://jw.bhu.edu.cn/ 新网址:http://210.47.188.39/jsxsd/


2021-03-28更新:由于学校更换了教务系统 新版内容如下

抓包分析

登录

我们打开官网后,输入账号密码点击登录按钮,发现第一个包是POST /Logon.do?method=logon&flag=sess返回了一串字符,到这里不清楚这是干啥的;然后第二个包是POST /Logon.do?method=logon,传入了userAccount(学号)encoded(密文)返回了一个重定向,第三个包是访问了刚刚的重定向网址结果又重定向到了/jsxsd/framework/xsMain.jsp主页。

分析到这里,我们发现密码是被加密了再传输的。我第一个想到的是右键查看源代码看看密文是怎么来的吧,登录按钮直接绑定了js函数login(),跳转到这里发现如下代码:

function login() {
	if($("#userAccount").val() == "") {
		$("#showMsg").text("请输入账号");
		$("#userAccount").focus();
		return false;
	}
	if($("#userPassword").val() == "") {
		$("#showMsg").text("请输入密码");
		$("#userPassword").focus();
		return false;
	}
	
		
		var strUrl = "/Logon.do?method=logon&flag=sess";
	

	$.ajax( {
		url:strUrl,
  		type:"post",
  		cache:false,
		dataType:"text",
		success:function(dataStr) {
			if(dataStr=="no"){
				return false;
	 		}else{
	 			var scode=dataStr.split("#")[0];
		     	var sxh=dataStr.split("#")[1];
		     	var code=document.getElementById("userAccount").value+"%%%"+document.getElementById("userPassword").value;
			 	var encoded="";
				for(var i=0;i<code.length;i++){
					if(i<20){
						encoded=encoded+code.substring(i,i+1)+scode.substring(0,parseInt(sxh.substring(i,i+1)));
					    scode = scode.substring(parseInt(sxh.substring(i,i+1)),scode.length);
					}else{
					    encoded=encoded+code.substring(i,code.length);
					    i=code.length;
					}
				}
				document.getElementById("encoded").value=encoded;
				if("logon"!="logonLdap"){
					document.getElementById("userPassword").value="";
				}

				document.getElementById("loginForm").submit();
	 		}
		},
		error:function() {
	 		alert("计算异常!");
		}
  	});
}

这里访问了第一个包得到的字符串是一串可以说是密钥的东西;通过一系列算法把密码加密成密文,我把这段翻译成了Python代码:

def encrypt_password(rawuser, rawpsw):
	# rawuser='学号'
	# rawpsw='密码'
	user = base64.b64encode(rawuser.encode('utf-8'))
	pwd = base64.b64encode(rawpsw.encode('utf-8'))
	enText = user.decode("utf-8") + '%%%' + pwd.decode("utf-8")
	return enText

得到加密的密码之后,就可以正常登录了。

def login(user,psd):
	r = session.get(url="http://210.47.188.39/jsxsd/")
	r = session.post(url="http://210.47.188.39/jsxsd/xk/LoginToXk",data={"userAccount": user, "encoded": encrypt_password(user, psd)})
	r_week= session.get('http://210.47.188.39/jsxsd/framework/xsMain_new.jsp')
	StudengtName=re.findall(r'学生姓名:</div><div class="middletopdwxxcont">(.*?)</div></div>',r_week.text,re.S)[0]
	StudengtId=re.findall(r'学生编号:</div><div class="middletopdwxxcont">(.*?)</div></div>',r_week.text,re.S)[0]
	StudengtPart=re.findall(r'所属院系:</div><div class="middletopdwxxcont">(.*?)</div></div>',r_week.text,re.S)[0]
	Now_Week=re.findall(r'<span class="main_text main_color">(.*?)</span>',r_week.text,re.S)[0]
	today = time.strftime("%Y-%m-%d")
	return StudengtName,StudengtId,StudengtPart,Now_Week,today

[可选]获得姓名

登录后重定向两次到了/jsxsd/framework/xsMain.jsp,里面含有名字可以取出,查看源码容易知道用正则

<span class="glyphicon-class">(.*?)</span>可以返回的第一个数据就是姓名。

[可选]获得当前周

我们发现主页是多个iframe框架拼凑的,第几周的信息在/jsxsd/framework/xsMain_new.jsp路径下,查看源码容易知道用正则>(.*?)</span>/(.*?)$可以返回第几周和总周数。

获得课表

分析包容易发现/jsxsd/framework/main_index_loadkb.jsp就是课表的网页,传入的rq参数是当天的日期,传入日期不同课表显示不同。但是课表名字过长的话会显示..导致显示不全,刚开始我还以为是前端CSS造成的,结果是后端就处理好的!汗!幸好我们将鼠标滑动的时候能显示出详细的信息,所以通过查看源码可以得到课程的完整名称和地点。课程的详细信息在td标签里的p标签title属性里。我们把所有的数据打包成一个二维数组,供生成图片使用。

代码如下:

	r = session.post(url="http://210.47.188.39/jsxsd/framework/main_index_loadkb.jsp",data={"rq": time.strftime("%Y-%m-%d"),'sjmsValue':'D372870E55974EE983039E1D2EA358B7'})
	table_data = []  # 全部数据数组
	table_header = re.findall(r'<th[^>]*">(.*?)</th>', r.text)  # 表头
	table_data.append(table_header)
	header_length = len(table_header)  # 取得几列 也就是表头个数
	all_td_content = re.findall(r'<td[^>]*>(.*?)</td>',r.text.replace("\t", "").replace("\n", "").replace("\r",""))  # 取得所有td内容 删除换行符号和制表符
	print(all_td_content)
	temp = []  # 临时存储单行的数据
	for i, v in enumerate(all_td_content):
		if i % header_length == 0:  # 说明这是这一行的第一个数据 也就是表格中的第几节
			dijijie = v.split("<br/>")
			temp.append("\n%s\n%s\n%s" % (dijijie[0], dijijie[1], dijijie[2]))
		else:  # 否则就是具体课程啦
			name = findMiddle(v, "课程名称:", "<br/>")
			room = findMiddle(v, "上课地点:", "'")
			temp.append("\n" + str(name) + "\n\n" + str(room) + "\n")
		if (i + 1) % header_length == 0:  # 检测目前td内容是不是一行的最后一个数据 如果是就把这一整行给加入总的二维表
			table_data.append(temp)
			temp = []
	print(table_data)

然后参考https://www.cnblogs.com/xiamibk/p/10299609.html的文章,执行

create_table_img(table_data, 
                user + '(' + today + ')课表.png', 
                font="font.ttf",
                describe=['以上数据仅供参考,作者:N(G)博客地址:https://fm90.cn.cn/'],
                table_title=user + '(' + today + ')[%s]的课程表' % Now_Week)

将二维数组表转换成图片就是这样:

自动发送邮件

首先我们在163邮箱里创建一个授权码当做第三方客户端的密码,然后就可以将当天的课表和本周的课表图发出去了。

获得当天课表

t = table_data.copy()  # 复制一份刚刚生成的二维表
del (t[0])  # 删除第一行 也就是星期几的那一行
table_data_90 = [[row[i] for row in t] for i in range(len(t[0]))]  # 矩阵转置
today = "你的今日课表如下:<br>"
for i, v in enumerate(table_data_90[0]):
    detail_kecheng = table_data_90[datetime.datetime.now().weekday() + 1][i].split()
    if len(detail_kecheng) != 0:
        today = today + "%s:%s(%s)<br>" % (v.split()[2], detail_kecheng[0], detail_kecheng[1])

PNG图片转Base64

def img_to_base64_img(fileName):
    img_file = open(fileName, 'rb')
    base64_data = base64.b64encode(img_file.read())
    img_file.close()
    return '<img src="data:image/png;base64,%s">' % str(base64_data, "utf8")

发送邮件

def sent_email(sender, pwd, receiver, text, img_path):
    host = 'smtp.163.com'
    port = 465
    body = img_to_base64_img(img_path)
    msg = MIMEText(text + "<br>" + body, 'html')
    msg['subject'] = '%s的课表' % rq
    msg['from'] = sender
    msg['to'] = receiver
    try:
        s = smtplib.SMTP_SSL(host, port)
        s.login(sender, pwd)
        s.sendmail(sender, receiver, msg.as_string())
        s.close()
        print('Done.sent email success')
    except smtplib.SMTPException:
        print('Error.sent email fail')

目标:利用python实现URP教务系统的模拟登录

重点说一下验证码部分 :

F12定位到验证码位置,可见验证码的url为http://jw.bhu.edu.cn/validateCodeAction.do?random=0.3781515662057473

验证码图片的连接指向了validateCodeAction.do这个组建,并且还有一个请求字段random= 0.3781515662057473 ,刷新网页后,会发现这个random值是变化的。直接通过浏览器访问一下这个validateCodeAction.do,并带上相同的random,会发现验证码变了。

经过一番研究,发现这个验证码是有漏洞的。对于同一个cookie,同样的random字段,每次访问都会返回不同的验证码图片,但是实际上任何一个验证码图片都是被后台认可的。也就是说,当我们打开教务网首页的时候,出现了验证码a,我们带着相同的random值去另外访问验证码页面,得到了验证码b,我们将验证码b输入到主页,也是可以成功登录的。

下面附上可执行的python代码:

# -*- coding: utf-8 -*-
#   @Time    : 2019/12/5 17:54
#   @Author  : 南国旧梦i
#   @FileName: newjw.py
#   @Software: PyCharm
#初始登陆网页界面
import requests
import re
url = 'http://jw.bhu.edu.cn/'
headers = {
    'Connection': 'keep-alive',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8application/signed-exchange;v=b3',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 SE 2.X MetaSr 1.0',
    'Accept-Encoding': 'gzip, deflate',
    'Upgrade-Insecure-Requests':'1',
    'Referer': 'http://jw.bhu.edu.cn/',
    'Host': 'jw.bhu.edu.cn',
}
session = requests.session()
#登陆

#验证码获取部分
imgurl = 'http://jw.bhu.edu.cn/validateCodeAction.do?random=0.3781515662057473'
req_image = session.get(imgurl)
with open('code.png', 'wb') as f:
    f.write(req_image.content)
#登陆部分
url_login = 'http://jw.bhu.edu.cn/loginAction.do'
code = input("code:")
form = {
        'zjh1': '',
        'tips': '',
        'lx': '',
        'evalue': '',
        'eflag': '',
        'fs': '',
        'dzslh': '',
        'zjh': '学号',
        'mm': '学号',
        'v_yzm': code
    }
req_login = session.post(url_login,data=form,headers=headers)
res = req_login.status_code
print(res)
#获取登陆后的界面
re1 = session.get('http://jw.bhu.edu.cn/xjInfoAction.do?oper=xjxx')
page = re1.content.decode('GBK')
# html =re.findall(r'<td class="fieldName" width="180">姓名:&nbsp;</td>(.*?)<td class="fieldName" width="150"></td>',page,re.S)[0]
print(page)

说明:

  • 如需批量爬虫可以使用验证码自动识别
  • 模拟登陆后可以进行相关页面的数据爬虫整理
南国旧梦i

南国旧梦i

出生于苏北小镇 习惯了一个人坐在电脑前 喜欢一个人听着音乐 梦想着一天有一趟说走就走的旅行·······目前过着大学生活,开始了程序人生。

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>