起源
应该是受疫情影响 ,这学期的期末考试成绩一直到年后才公布出来,以往年前就可以查了。这个成绩查询网站用了很多年了,因为学院内部用的而且整体做得也比较简单,还是用的年代很久远的ASP开发的。查完成绩之后,就想着爬一爬这个网站抓取一些学生成绩来瞄一瞄O(∩_∩)O哈哈~,因为我学的.NET开发,我最开始想到的就是利用.NET的HttpWebRequest发送HTTP请求试一试,结果弄了一下午没搞明白,加上还有别的事情要做,就放下了。昨天想着还是用爬虫老大哥Python试试吧,虽然不太熟悉Python,但是大二的时候也在Python课程中学习了基本语法、OOP、I/O等基础知识(过了一年多了,基本都还给我亲爱的老师啦-_-||),下面是实现想法的详细过程。
成绩查询网站:
吐槽:(整个网站主要DOM元素只有三个input,一个img,两个button[注册button还是失效滴] 其余的就是一张背景图\(^o^)/~)
思路描述
思路应该是很清楚的: 1.模拟填写用户名、密码=>2.识别验证码图片=>3.填写验证码=>4.提交表单向服务器发送请求=>5.抓取数据
理清思路后,看起来很简单,但是对于我这个爬虫门外汉来说,一步步实现起来并不是想象的那么轻松
前期准备
做这个以前,要对Web请求方式(get,post),Request报文,Response报文,Cookie,Session,浏览器调试工具或者Fiddler(抓包工具)做一定的了解和学习,因为一直在做ASP.NET Web开发,对这些东西没有太大的问题。
这里对get,post请求,Cookie,Session做一个简单的说明,以免对这块不太熟悉的同学后面读起来比较生涩。
常用的请求方式(get post):
get请求:参数(Querystring)key,value的形式跟在url后面,只用进行url编码,数据暴露在url中不安全(这是相对的!),get请求一次产生一个TCP数据包。一般向服务器请求数据时用get请求。
post请求:参数放在Request body中,可以有多种编码方式(Base64等等),相对来说较为安全,post请求一次产生两个TCP数据包。一般向服务器提交数据时用post请求。
Cookie和Session:
Cookie:保存在客户端(浏览器),存储量少,不安全,保存非敏感信息。
Session:保存在服务器端,存储量大,安全,但是随着访问增多会对服务器的性能产生影响。
客户端第一次访问服务器会在服务器端创建一个session,最后会在客户端以cookie的形式保存一个sessionid,每个用户的sessionid是唯一的,因此每次请求时服务器会根据带过来的sessionid来区别是哪个用户的会话。(后面会着重用到这个)
一、分析Http请求,找到表单提交时请求的地址和FormData参数
1.获取登录时的请求地址
输入学号,密码,验证码点击登录,Chrome浏览器F12后Network进行观察后发现,表单提交后,向服务器发送了一个post请求,并且获取到请求地址:
2.查看post请求中的FormData
可以看出表单提交时一共向服务器发送10个参数: r1:学号 r2:密码 r3:验证码 r4-r10:空值
密码这里设置的是姓名中的一个汉字,但是提交的时候显示一个字符,应该对中文做了一定编码,通过查看源代码发现编码格式为gb2312,所以后面模拟提交的时候也要对汉字进行gb23121编码,否则会登录失败 !
二、分析验证码
点击验证码右键 检查发现这个验证码是后台生成的,没有固定url,每次点击会附带时间戳get一次,从服务器重新发送给客户端一个新的图片。我前面博客中我讲过这种验证码的生成办法。
之后我们要做的就是识别验证码,我这里是把验证码图片存到本地再用第三方库去识别验证码,我选择的库是百度文字识别API。
因为以前用百度AI接口做过下面这种小demo,所以第一个想到的就是百度AI。
深入分析
经过以上的准备我们的思路越来越清晰,我们可以同时模拟向服务器请求验证码,解码之后与用户名,密码一起放入formData中一起post提交给服务器即可!
但是问题来了,怎么才让服务器知道我们首先请求到的验证码就是这次我表单一起提交需要验证的验证码呢?
最初就是没有考虑到这个问题,卡了半天没有开窍,后来一想就明白了。
换而言之就是这样,有两个同学小明和小王在两台不同的设备同时获取到验证码,填完表单提交到服务器,服务器如何才能区别这两个会话呢?
实际我前面在简单提到session的时候,答案就说出来了O(∩_∩)O哈哈~
下面,我用两个不同的浏览器分别模拟小明和小王同学登录一次一探究竟:
浏览器1(小明):
请求验证码图片时的请求报文如下:
表单提交时的请求报文如下:
我们可以很明显的看到,两次请求时cookie携带的sessionid完全一样
浏览器2(小王):
请求验证码图片时的请求报文如下:
表单提交时的请求报文如下:
两次请求时cookie携带的sessionid完全一样
所以,我们只需保证两次(获取验证码图片,表单提交)请求时携带的sessionid一致,服务器就可以知道这是一个用户的一套行为,当然通过比对sessionid不同,服务器就区分了两次不同会话。
代码实现
当一切都理顺之后,思路清晰,代码实现就很容易。我一直都认为写代码,想清楚逻辑,思路理顺再写,比反复试来试去,error成堆推翻重构要好得多。
主要使用的库:
- requests库(第三方Python库 模拟http请求,处理URL资源,简单且功能强大)
- 百度文字识别库(识别验证码图片) 也可以用第三方tesseract库 但是安装麻烦 识别率低
- PIL库(处理图片有关问题)
说明:
下载完验证码图片到本地后,调用OCR接口试了一下,发现图片尺寸太小,导致无法识别,故用PIL库处理下图片大小
方法一 导入必要的第三方库:
import requests
from PIL import Image
from aip import AipOcr
方法二 加载百度文字识别client
这三个参数需要去官网创建一个应用,之后会给你这三个参数,私密参数,故不再展示啦
###加载client
def Load_Client():
APP_ID = 'xxxxxxxx'
API_KEY = 'xxxxxxxx'
SECRET_KEY = 'xxxxxxxxxxxxxxxx'
return AipOcr(APP_ID,API_KEY,SECRET_KEY) #创建客户端
方法三 读取本地图片
###读取本地图片到I/O中
def get_file_content(filepath):
with open(filepath,'rb') as fp:
return fp.read()
方法四 修改图片尺寸
###修改图片尺寸
def set_imgsize(path):
pic=Image.open(path)
pic = pic.resize((120, 50))#这个尺寸即可识别出来
pic.save(path)
方法五 验证码图片转文字
###验证码图片转文字 返回验证码字符串
def get_v_code(path):
img_byte=get_file_content(path)
result_dic=client.basicGeneral(img_byte) #文字识别
return result_dic["words_result"][0]["words"]
主要方法 模拟登录
为了解决“深入分析”中的遇到的问题,requests提供了很好的解决方案 requests.session()
代码分析:
源码如下:
def simulation_login(url,imgurl,path,stu_number,stu_name):
print("模拟登陆!")
session=requests.session()# 保持两次请求 sessionid 相同
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36'}
img_response=session.get(imgurl,headers=headers) #请求验证码图片
code=0
if img_response.status_code==200:
print("验证码获取成功!")
##保存验证码图片到本地
open(path,"wb").write(img_response.content)
set_imgsize(path)
code=get_v_code(path)
print("验证码识别成功:"+code)
formData = {'r1': stu_number,'r2': stu_name.encode("gb2312"),'r3': code} #,'r4': '','r5': '','r6': '','r7': '','r8': '','r9': '','r10': ''
response=session.post(url,data=formData,headers=headers)
if response.status_code==200:
print("表单提交成功!")
response.encoding=response.apparent_encoding
if "重新登录" in response.text:
global error_count;
error_count+=1
print("登录失败[验证码或密码错误]!")
else:
print(stu_number+" 登录成功!")
global success_count;
success_count+=1
test_info=response.text.replace("<script>parent.location.reload();alert('","").replace("\\n"," ").replace("');</script>","").replace("2019-2020学年第一学期成绩如下:","")
test_info=test_info[0:test_info.rindex("学号")]#去重
open("学生成绩.txt","a",encoding='utf-8').write(test_info+"\n")
print(test_info)
运行效果:
批量爬取一个班实际效果:
结
用周末整整的一个下午,对这一整个过程进行了细致的描述,从需求到思考到分析到最终的代码实现,从截图到录视频到gif,为了就是展现的很清楚,因为我看过一些帖子一大段的代码往上一放就完事了,跑通跑不通都另说,更别说去理解代码片的意思了。当时用.net没有做出来的时候,就放下不想搞了,但是最终还是通过Python一步步学习和思考实现了,虽然技术手段很简单,爬取的网站也很简单,但是正是因为继续探索了,才看到其他语言在一些方面的优势,并且将.net的一些东西用到这个小案例中,同时也学习到了很多新东西,对以后的开发有很多帮助O(∩_∩)O哈哈~
希望有跟我一样在这方面还是小白的朋友,看了之后有新的认识。