一、背景
之前有记录过普通的scrapy模拟登录,这种方法可以满足了日常爬虫的登录需求。
但是技术一直在进步,近几年前后端分离的趋势越来越好,很多web都采用前后端分离的技术。那么登录后的用户权限验证就会出现jwt的形式。(主要是token方式的验证,在模拟登录中要解决的问题)
这里记录一下。
二、登录操作
前后端分离的项目,一般都是react、vue等js语言编写的(没有这方面经验的同志,可以不用往下看了)
有些会采用成型的前端框架,如AntDesign,ElementUI等,它们写出来的web页面,如果用css定位或者xpath定位,是很不准确的。所以最好的办法就是观察数据流,找到api和发送的参数进行构造。
以这里的登录为例,通过css定位其实也可以,但是有不稳定的风险。所以还是看api和参数比较稳妥,毕竟css怎么变,api都不会随意改变。
选中post那条数据流,看到右侧的请求地址、请求头和参数
[图片上传失败...(image-9401fa-1531469273677)]
这样就可以根据请求地址、请求头和参数来构造登录用的代码:
def start_requests(self):
""" 重载start_requests方法 通过is_login方法判断是否成功登录 """
login_url = "http://xxx.yyy.ccc.aa/api/v1/oauth/login"
login_data = {
"username": "abcd@easub.com",
"password": "faabbccddeeffggd5",
"type": "email"
}
return [scrapy.FormRequest(url=login_url, formdata=login_data, callback=self.is_login)]
def is_login(self, response):
"""
根据返回值中的message值来判断是否登录成功
如果登录成功则对数据传输页发起请求,并将结果回传给parse方法
如果登录失败则提示
由于后面的用户权限验证需要用到token信息,所以这里取到登录后返回的token并传递给下一个方法
"""
results = json.loads(response.text)
if results['message'] == "succeed":
urls = 'http://xxx.yyy.ccc.aa'
access_token = results['data']['access_token']
print("登录成功,开始调用方法")
yield Request(url=urls, callback=self.parse, meta={"access_token": access_token})
else:
print("登录失败,请重新检查")
如果返回信息的json里面message值为succeed及认为登录成功,并调用parse方法。
三、用户权限验证
登录完毕后,我想执行其他的操作,比如上传(post)数据,跟刚才一样,需要观察api的地址和所需参数请求头信息等。
同样是根据返回的参数和请求头,来构造代码
然而这次却不行,返回的状态码是401,由于scrapy默认只管200和300的状态码,4开头和5开头的都不处理。但是又需要观察401状态返回的东西,可以在settings.py中空白处新增代码:
""" 状态码处理 """
HTTPERROR_ALLOWED_CODES = [400, 401]
然后在下一个方法中观察response回来的数据。
======================================
后来又查询了401的意思,就是未获得授权,也就是用户权限验证不通过,经过多方资料查找,发现请求头中有这么一条:
[图片上传失败...(image-1322be-1531469273677)]
它就是用于用户权限验证的,authorization的值分为两部分<type>和<credentials>,前者是验证采用的类型,后者是具体的参数值。这里的类型可以看到用的是Bearer类型,(传说值默认是用户名+密码的base64字符串,但这个这么长,显然不是64)。
我又去观察登录时候的返回值,发现登录成功后的返回值除了succeed之外,还有其他的一些返回值,里面包括了一个叫access_token的字段,它是用于JWT登录方式用来鉴权的token信息,而且authorization用的也正好就是这个token作为值。
那么代码就应该在第一次登录时候,取出access_token的值,并传递下去,用于后面请求的鉴权:
def is_login(self, response):
"""
根据返回值中的message值来判断是否登录成功
如果登录成功则对数据传输页发起请求,并将结果回传给parse方法
如果登录失败则提示
由于后面的用户权限验证需要用到token信息,所以这里取到登录后返回的token并传递给下一个方法
"""
results = json.loads(response.text)
if results['message'] == "succeed":
urls = 'http://xxx.yyy.ccc.aa'
access_token = results['data']['access_token']
print("登录成功,开始调用方法")
yield Request(url=urls, callback=self.parse, meta={"access_token": access_token})
else:
print("登录失败,请重新检查")
下面的pase方法中,将authorization设定到header中以对数据进行请求:
header = {
"authorization": "Bearer " + access_token
}
这样就解决了用户权限的问题,不再出现401
四、postman发送请求特殊格式数据(json)
在parse方法中,根据浏览器观察到的参数,进行构造:
datas = {
"url": "https://www.youtube.com/watch?v=eWeACm7v01Y",
"title": "看上去可爱其实很笨的狗#动物萌宠#",
"share_text": "看上去可爱其实很笨的狗#动物萌宠#[doge]",
"categories": {'0': '00e2e120-37fd-47a8-a96b-c6fec7eb563d'}
}
由于categories里面是个数组,所以在构造的时候也可以直接写数据,然后用scrapy.Formdata来进行post。发现返回的状态是这次是400,并且提示:categories必须是数组
再次观察请求头信息,发现请求头信息中还有:
[图片上传失败...(image-e2574e-1531469273677)]
叫做content-type的参数,我将它加入到header中:
header = {
"authorization": "Bearer " + access_token,
"content-type": "application/json",
}
这样关于categories的提示就没有了。但是返回的状态码依然是400,而且提示变成了url不能为空,这到底又是怎么一回事?
多方探查都没有结果。
真是伤心
后来我又想起了,既然这里的文本类型是application/json,那么提交出去的文本应该是json类型数据,而不是python的dict字典类型数据。
于是打开json在线解析,对传递的参数进行观察,发现这样的数据并不满足json格式:
后来尝试对它进行更改:
在外层增加了一对{},然后又将categories的值加上了双引号,才是正确的json格式。
但是如果这样,拿到postman中进行测试,是不行的,后来经过反复测试,最终确定了postman的请求格式为:
对Auth、Headers和Raw进行设置,才终于成功发送post,返回正确的信息!!!
五、scrapy发送Json格式数据
在postman测试通过后,说明这样的做法是可行的,但是代码上怎么编写呢?
用之前的scrapy.Formdata是不行的,它的formdat=默认使用dict格式,如果强行转成json格式也是会报错的。经过群里咨询和搜索,发现要用scrapy.http的Requst方法(平时常用的这个):
access_token = response.meta['access_token']
urls = "http://aaa.bbb.xxx.yy/api/v1/material/extract"
datas = {
"url": "https://www.youtube.com/watch?v=eWeACm7v01Y",
"title": "看上去可爱其实很笨的狗#动物萌宠#",
"share_text": "看上去可爱其实很笨的狗#动物萌宠#[doge]",
"categories": {'0': '00e2e120-37fd-47a8-a96b-c6fec7eb563d'}
}
header = {
"authorization": "Bearer " + access_token,
"content-type": "application/json",
}
yield Request(url=urls, method='POST', body=json.dumps(datas), headers=header, callback=self.parse_details)
这样才发送请求,终于成功了!!!