注意!!!本文仅供学习参考!!!严禁用于商业用途!!!未经许可严禁转载!!!
本案例中涉及的逆向技术:JSL多层响应、js补环境。
在正式开始分析之前提供一段通用请求模板尝尝鲜(再也不用每次都去重新写请求了)
import requests
import time
import random
class SendRequest:
"""基本请求模板,待完善"""
def __init__(self):
self.url = ''
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
}
self.cookies = {} # cookie设置
self.data = {} # 表单数据
self.page = 1 # 翻页控制参数
self.session = requests.session()
@property
def UGetRequest(self):
response = self.session.get(url=self.url, headers=self.headers, cookies=self.cookies)
time.sleep(random.randint(1, 3))
return response
@UGetRequest.setter
def UGetRequest(self, kwargs: dict):
if kwargs.get('url'):
self.url = kwargs.get('url')
if kwargs.get('referer'):
self.headers['referer'] = kwargs.get('referer')
@property
def UPostRequest(self):
response = self.session.post(url=self.url, headers=self.headers, cookies=self.cookies, data=self.data)
return response
@UPostRequest.setter
def UPostRequest(self, kwargs: dict):
if kwargs.get('url'):
self.url = kwargs.get('url')
if kwargs.get('referer'):
self.headers['referer'] = kwargs.get('referer')
好了,进入主题!!!!
一、目标网站结构分析
注意!所有的URL皆经过脱敏处理,大家测试的时候记得先转换回明文!
主页地址:aHR0cHM6Ly93d3cubWFmZW5nd28uY24v
1.1 主页分析
根据需求需要采集热门游记文章,那么从主页来看的话是存在着热门游记板块的,但是并不是说我们去访问主页的这个url就能够获取到这些游记数据。这里为了方便测试,大家可在自己的电脑中安装一个接口测试工具,例如apipost
或者postman
等。实在不想安装的话就麻烦一点通过代码进行测试。这里我们就选择使用postman来进行接口测试了。
首先,做爬虫分析网页的第一步,先打开开发者工具,毕竟网页写的再好看对于我们做爬虫来说没有任何作用,我只关注你的数据在哪里。
箭头所指即为访问主页的这个url时服务器返回给我们的数据包,直接点击查看,然后在响应内容中查看是否存在我们需要的目标数据
很明显可以看到搜索结果为零,而这个搜索内容正是我们在页面上看到的第一篇游记中的标题部分内容。
所以到这一步我们就心知肚明了——**目标数据不在主页接口。**也就是说目标数据是ajax异步加载而来,那么我们可以先定位到XHR之中。然后老方法进行搜索。
搜索了,可是数据呢?难道他不是异步加载的不成?肯定不是,这里同样是异步加载,只不过这些数据并没有一个是请求直接返回的,而是将这些数据放在了一个JS包中再由JS来渲染在页面上,所以我们在XHR中就找不到目标数据所在的数据包了。也因此我们再来到JS文件中去进行查找。
这里大家就不用去搜索内容(人杰地灵)了,搜不到的,不信邪的话可以测试一样,当然如果多天后网站更新来能搜到了的话咱又另说…既然搜标题搜不到,那我们换一个思路,不搜标题了,我们来搜地址。
随便点一篇文章进入到详情页,例如此处第一篇文章。
很明显可以看到他是一个.html
的扩展,那如果把他类比到我们的电脑中的文件存储方式的话这是不是就是一个文件呢?其实就是这样,那也就是意味着这一串数字肯定就是唯一的,因为在同一路径下不可能存在文件名相同的两个不同文件。所以,我们就回到主页开发者工具抓到的包中搜索一下这串数字。
到位!这样我们就找到了一个包,然后近一步分析这个包。直接预览先看一下内容是些什么。
直接就是好家伙,原来他是一个html页面的代码,那么至于这里的html代码是不是我们想要的游记文章呢?搞一下不就知道了嘛…上代码,请求的代码在文章头部,这里就不再重复书写了。
class DataDeal(SendRequest):
def getIndexData(self):
"""主页处理"""
self.UGetRequest = {
'url': 'aHR0cHM6Ly9wYWdlbGV0Lm1hZmVuZ3dvLmNuL25vdGUvcGFnZWxldC9yZWNvbW1lbmROb3RlQXBpP2NhbGxiYWNrPWpRdWVyeTE4MTA5MjA3MzE5ODUxMTk4ODY1XzE2ODM3MTM1MDE4MjImcGFyYW1zPSU3QiUyMnR5cGUlMjIlM0ElMjIwJTIyJTdEJl89MTY4MzcxMzUwMTg2NA==',
'referer': 'aHR0cHM6Ly93d3cubWFmZW5nd28uY24v' # 防盗链
}
response = self.UGetRequest
print(response.content.decode())
执行结果:
来了!和浏览器中看到的一模一样,但是问题来了,要把html节点的内容提取出来的话好像不是那么方便,因为符号实在太多太杂,正则不太好写,不过钥匙能把花括号外面一层去掉的话直接就能当作json来处理了。这里要去掉他的方法很简单,就不再过多描述,例如直接根据它的规律用正则将开头的JQuery…这么一长串字符给删掉,然后再删掉头尾的括号再去进行json转换就好了…但好像还是麻烦了一点,所以这里我们做最简单的方法,直接从请求的URL中去删掉回调文件的callback参数。
删除之后再去进行访问就能够拿到一个非常规范便于转换的json串了。代码如下:
def getIndexData(self):
"""主页处理"""
self.UGetRequest = {
'url': 'aHR0cHM6Ly9wYWdlbGV0Lm1hZmVuZ3dvLmNuL25vdGUvcGFnZWxldC9yZWNvbW1lbmROb3RlQXBpP3BhcmFtcz0lN0IlMjJ0eXBlJTIyJTNBJTIyMCUyMiU3RCZfPTE2ODM3MTM1MDE4NjQ=',
'referer': 'aHR0cHM6Ly93d3cubWFmZW5nd28uY24v'
} # 防盗链
response = self.UGetRequest
text_html = response.json().get('data').get('html')
with open('mfw.html', 'w', encoding='utf-8')as f:
f.write(text_html)
将获取到的html节点下的内容写到本地html文件中打开查看。
虽然出现了中文乱码的情况,但是不影响我们的判断,在其中找到了文章的详情页URL,并且发表的用户是和浏览器正常访问的时候是一模一样的,也就是说在这里面就是我们需要的游记入口了,那么接下来我们要做的就是将这些游记的详情页提取出来进行访问再获取到详情页的数据即可。那么接下来我们进入到详情页再来进行分析。
1.2 详情页分析
来到详情页之后一样的直接就开始抓包就行,然后就会发现惊喜所在。
讲个严肃的事情,打了马赛克的美女是我的…(要保护别人隐私!这是每一个爬虫人要有的常识!)
好了,惊喜呢?看开发者工具中,Doc里面那两个大大的出现了异常的红色的数据包。那么这两个出现异常的包是什么情况呢?
preview和response中也看不到任何内容,那咋办?再往下看成功的那个包又是能够看到响应内容的。
那既然如此,不要犹豫了,冲一发就知道了,打开我们的接口测试工具,或者直接上代码访问然后打印响应来看也是一样的!
postman
测试访问结果:
看到这里,直接一个好家伙,不熟悉的人直接就是一个好家伙,熟悉的人看到也直摇头,但是问题不大,看到那么明显的一个script
标签没,也就是说,这不就是一段JavaScript代码嘛,也就是咱说的JS。那既然是代码的话要知道你是个啥,跑一下不就知道了嘛是吧。将标签中的js代码赋值下来然后粘贴到浏览器开发者工具的console中执行,如果本地配置了js执行环境的话在本地执行也可以,但是可能需要修改一些环境,这个是之后的内容,现在先在浏览器中去执行看看。
当然复制的时候只需要复制cookie那部分的js代码,至于后面的location.href部分就不要去复制了,因为那部分就是从当前的环境中去读取路径然后刷新当前页面以添加前方代码生成的cookie而已。
执行出来后就发现这不就是生成了__jsl_clearance_s
这个参数嘛,所以实际是不是这样呢,来将生成的这个值带进去瞅瞅。
好家伙,带进去一看,这比刚刚还得劲了。此时相信大家的内心已经开始方了,这是常人能看懂的吗?那肯定不是,正常人谁看这个,但是搞爬虫有几个正常人,正常人谁会喜欢被各种反爬虐的体无完肤还要继续的对吧。
先不管他返回的是啥吧,总之现在思路是有了,那么根据这地方的部分代码来看的话(甚至看都不用看)这第二次返回的代码肯定又是要设置某一个参数,这里我们就先不去分析怎么判断了,后面会有详细的对本处的代码分析如何找到目标点的过程,此处暂时先直接告诉大家,这里就是对__jsl_clearance_s
这个参数的二次生成。
而这个猜想也是很容易就能验证的,来到浏览器中,将访问成功的那一个数据包中的该参数添加到cookie中再来进行访问(要注意此处还有一个__jsluid_s
参数,该参数是第一次访问时服务器设置的,可以直接从响应中获取)。
所以,在浏览器中我们拿访问成功时的这两个参数来测试一下看看是否能够获取到目标页面内容,能的话那么意味着这两个参数的生成就是我们案例中的关键点所在了。
Postman
设置两个Cookie
实际发送时的Cookie与浏览器中截图里面有所不同,但是来源都是一次成功的访问,与下方代码是一样的原理。
import requests
url = 'aHR0cHM6Ly93d3cubWFmZW5nd28uY24vaS8yNDIyNDA2OC5odG1s'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',
'Cookie': 'X19qc2x1aWRfcz1iMGVjYjI4Zjg4NTM5OWQ3OGQ1ZjU2MmI2ZDgzMTc3ZTsgX19qc2xfY2xlYXJhbmNlX3M9MTY4MzcyMTMyOS4xNDJ8MHwlMkJxU2xvc1Eyb0klMkZXNjF2ZjFNSUpDdUt6ZGp3JTNE'
}
res = requests.get(url, headers=headers)
print(res.content.decode())
然后直接访问拿到响应如下
postman中
代码执行结果
访问的时候大家一定要注意设置上referer
和UA
,否则的话就会回到最初的起点。
好了,分析到这一步,就已经很明确了,要拿到详情页,我们就需要拿到准确的__jsluid_s
和__jsl_clearance_s
这两个参数。
第一个参数是由服务器生成,这里在第一次访问查看响应头的时候就可以看到。第二个参数则是经历了两次访问生成而来,且使用的都是上一次生成的cookie。
总结下来编码的时候需要注意的点有:
1.请求的会话状态需要保持
2.共计需要对同一个详情页的地址请求三次,第一次获取一个cookie值用于请求第二次,第二次生成一个cookie值用于请求第三次
提示:此时应是消化时间,先不要直接看第二节,消化一下上方内容再往下看。
二、编码思路
对于网站的分析结束,现在就来开始编码,根据刚才分析的情况来看,我们一共是需要构造至少三次请求,第一次获取__jsluid_s
以及根据返回的响应内容来生成第一次的__jsl_clearance_s
参数,第二次则是解读第二次响应的js代码来生成第二次的__jsl_clearance_s
参数,第三次则能够通过两次的cookie来获取准确响应内容。
2.1 获取详情页URL列表
对于存放了详情页的这个URL是没有反爬的,所以我们只需要正常访问即可,考虑到翻页及其他情况(毕竟网页是随时变化的),先来对访问这个接口时参数的变化进行分析,现在我们看到的URL如下:
其中共3个参数,callback参数被我们删除不作分析,看params和下划线_这个参数,其中以下划线这个参数最为明显,其就是一个时间戳,所以在代码中可以通过time模块来获取到这个参数的值。
那么就还剩下params这个参数,这里呢是需要我们去稍微转换一下的,毕竟浏览器的编码和我们代码中所认可的不同,不转换的话我们也不知道这个参数要如何去构造,转换的方法直接使用urllib模块中提供的unquote方法即可
可以看到输出结果是一个json串,那么有没有可能在这个json串中其实还有别的参数来控制翻页或者其他状况呢?不多说,直接测试一下!
点击第二页然后看抓包情况。
果然,第二页的URL中这个params参数值变多了,同样的方式解码来看一下
多了几个键值对,再测试几页来看会发现这几个参数只有page在变化,第三页时page为3,第四页page为4以此类推,那么这个page就是控制了要返回第几页的数据,所以我们可以在创建的类中添加声明一个page属性来控制我们访问的页数。page的初始值为1(已经定义在了第一个类中)。那么到这里后对于详情页URL列表的获取就可以实现了,代码如下:
def getIndexData(self):
"""主页处理"""
daytime = str(int(time.time()*1000))
self.UGetRequest = {'url': 'https://pagelet.mafengwo.cn/note/pagelet/recommendNoteApi?params={"type":0,"objid":0,"page":%s,"ajax":1,"retina":1}&_=%s' % (self.page, daytime),
'referer': 'https://www.mafengwo.cn/'} # 防盗链
response = self.UGetRequest
text_html = response.json().get('data').get('html')
# with open('mfw.html', 'w', encoding='utf-8')as f:
# f.write(text_html)
tree = etree.HTML(text_html)
url_list = tree.xpath('//*[@id="_j_tn_content"]/div[1]/div/div[1]/a/@href')
url_list = ['https://www.mafengwo.cn' + url for url in url_list] # 拼接出完整的游记详情页URL地址
return url_list
2.2 详情页访问
详情页列表有了,接下来就是要对每一个详情页进行请求,而访问详情页需要经过两次跳转用以生成Cookie值,现在我们来看第一次Cookie的获取。
首先,将getIndexData
方法返回的URL列表进行遍历,实现逐个访问,同时考虑到后面还会有第二次访问,并且两次cookie的生成逻辑是不同的,因此我们需要将这两次获取分开,定义一个getFirstCookie
方法用以获取第一次访问时的Cookie。但是在此之前我们可以先定义一个详情页处理的方法先大致定义一个处理结构出来,如果说先去实现这个cookie的获取方法的话首先调试的时候在提取详情页URL的处会存在不便,其次实现之后也是需要进一步嵌套在详情页处理之中的,所以这里我们就可以先给出一个详情页处理的大致架构,再去其中嵌套Cookie的获取方法。
2.2.1 详情页处理方法基本结构设计
定义详情页处理方法detailDeal
def detailDeal(self):
url_list = self.getIndexData() # 获取到一页的游记
for url in url_list: # 遍历游记列表
self.UGetRequest = {'url': url, 'rederer': ''}
response = self.UGetRequest
如上方,一个基本的遍历访问结构就出来了。再考虑到翻页,前面我们已经考虑到了翻页的情况,所以提前就定义好了page属性,因此就可以通过这个page属性的自增来控制翻页了,而控制的方式则在主页处理方法中通过将page属性格式化到我们构造的URL中,如下方所示。
self.UGetRequest = {'url': 'https://pagelet.mafengwo.cn/note/pagelet/recommendNoteApi?params={"type":0,"objid":0,"page":%s,"ajax":1,"retina":1}&_=%s' % (self.page, daytime)
所以我们只需要在detailDeal
方法中实现此页数据采集结束后的page属性自增即可,然后别忘了我们的两次Cookie 的获取。代码如下:
def detailDeal(self):
url_list = self.getIndexData() # 获取到一页的游记
for url in url_list: # 遍历游记列表
self.UGetRequest = {'url': url, 'rederer': ''}
self.getFirstCookie() # 第一次Cookie设置
self.getSecCookie() # 第二次Cookie设置
response = self.UGetRequest
data = response.content.decode()
print(data)
# 上方循环结束则证明一页结束,接下来就是要翻页
self.page += 1
if self.page <= 3: # 采集3页的数据
return self.detailDeal()
2.2.2 获取第一次Cookie
详情页访问的基本结构是有了,那接下来就是对Cookie进行处理了。定义一个getFirstCookie
方法用于获取第一次的Cookie。基本结构如下:
def getFirstCookie(self):
"""获取第一次返回的Cookie"""
response = self.UGetRequest
res_data = response.content.decode()
print(res_data)
执行后输出结果与我们在postman
中测试的时候情况是一样的。**注意:代码处于开发阶段,所以循环是没有打开的,养成习惯不要在开发过程中就直接打开全部循环最终导致一些不必要的麻烦。**输出结果如下:
那么接下来就是要将响应内容中document.cookie
后面的js代码执行生成我们的第一次__jsl_clearance_s
参数。那么在执行这JS代码之前就需要我们先将代码提取出来。通过正则进行提取,此处的正则我们采用了最朴素的方式(直接复制完全体然后将要提取的目标值换成(.*?)),如下:
import re
...
res_jscode = re.findall('<script>document\.cookie=(.*?)location\.href=location\.pathname\+location\.search</script>',res_data)[0][:-1] # 提取出第一次返回的js代码
...
提取出来之后就是要去执行这串代码了,Python解释器肯定是不能直接执行JS代码的,所以借助第三方模块pyexecjs
中提供的eval
方法来执行(关于该模块如果有不了解的靓女靓仔可以前往公众号或者本人的CSDN或者知乎看《Python爬虫从0到1》专栏第十三天的内容)。代码如下所示:
import execjs
...
res_jscode = execjs.eval(res_jscode) # 执行JS代码并返回运行结果
...
那么到这里第一次的__jsl_clearance_s
参数就搞到了,接下来就是将其键值从这整个字符串中分割出来再添加到我们定义好的cookie字典(在开头提供的万能请求模板)中即可。根据此输出结果来看的话,我们可以先通过";“进行分割,再通过”="进行分割就可以拿到__jsl_clearance_s
键值对了。
...
__jsl_clearance_s = res_jscode.split(';')[0]
k, v = __jsl_clearance_s.split('=')
...
拿到键值对之后将其添加到cookie中
self.cookies[k] = v
接下来就是__jsluid_s
参数了,在前面分析的时候知道了这个参数是由服务器为我们设置的所以直接从响应中去获取就好了,可能有人会有疑惑我都没有去验证过就直接凭一句话就说它是服务器来的…如果有这样的想法的话,建议再去看看专栏文章关于Cookie的定义,回顾一下就是Cookie是由服务器设置,用户保存。那么看看浏览器中第一次访问的时候请求头中有__jsl_clearance_s
这个参数吗?明显没有的,找到海枯石烂都找不到,因为这个参数是第一次访问后服务器才给生成的。但是却是有__jsluid_s
参数的,那证明什么?证明__jsluid_s
参数在我们访问详情页之前就已经是存在了。那既然已经存在了直接从响应中去拿不就好了。
代码如下:
...
__jsluid_s = response.cookies.get('__jsluid_s') # 获取Cookie中__jsluid_s参数
self.cookies['__jsluid_s'] = __jsluid_s
...
那么到这里,第一次cookie的设置就完成了,整合后代码如下:
def getFirstCookie(self):
"""获取第一次返回的Cookie"""
response = self.UGetRequest
res_data = response.content.decode()
res_jscode = re.findall('<script>document\.cookie=(.*?)location\.href=location\.pathname\+location\.search</script>',res_data)[0][:-1] # 提取出第一次返回的js代码
res_jscode = execjs.eval(res_jscode) # 执行JS代码并返回运行结果
__jsl_clearance_s = res_jscode.split(';')[0]
k, v = __jsl_clearance_s.split('=')
__jsluid_s = response.cookies.get('__jsluid_s') # 获取Cookie中__jsluid_s参数
self.cookies[k] = v
self.cookies['__jsluid_s'] = __jsluid_s
调用后输出如下:
成功,那么接下来就要根据这一次的访问来开始第二次的Cookie设置了,前方高能,准备好纸巾,擦眼泪。
2.2.3 获取第二次Cookie
然后就是重头戏了,这么长的代码,还是混淆的,要想说提取出来通过execjs执行,那几乎是不可能的,必然会缺斤少两,不信咱就直接将这代码复制到一个js文件中运行一看大家就知道了。
格式化了一下,一共524行,勉勉强强,也不算多,但是,要梳理出来的话,难啊!这就算让js创始人来让他把这524行代码梳理清楚他也得挠头,耶稣来了也没用!那咋办?硬刚就完了!当然有取巧方式还是要用一下,这里测试了好几个现成解混淆的工具都没用,解了后代码没有毛线变化,也懒得自己去AST了,代码不多,咱直接就硬刚完事吧。
别管那么多了,先把这代码执行一遍看看啥情况。
意料之中,缺少浏览器环境,那就补吧,window没有定义,那就补一个。
注意!补的这些参数如果不想去梳理调用逻辑的话最简单粗暴的就是直接生命为全局变量在代码头部
var window = global;
补完window再执行,结果如下:
又来了,这次是少了UA,UA是navigator中的,那就来吧,接着补
navigator = {
'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'
};
再执行如下:
缺document,再补再来!
document = {};
还来…location,没办法,老实继续。
location = {};
终于。。。补完了。可是结果呢?
没输出对吧?那我们手动构造一个看看?先输出一下看看go()
方法的返回结果呢?
恩。没任何意义,那接下来咋整?不慌,问题不大!
既然调用了go
方法,那我们就先找到这个方法定义的位置先。
这,有点着急啊这代码长得,从389行开始到516行结束为go方法的逻辑,看起来有那么一丢丢复杂,那现在我来给大家简单优化一下下。
怎么样,是不是看起来稍微舒服了那么一点点其实这是比较常见的js代码防护手段之一(OB混淆),就是防止我们直接看出它的代码逻辑,可是啊,道高一尺魔高一丈不是,大家都看到了,我也就只是简单的删掉了部分没用的花指令,甚至平坦化流程我都没管它就成这样了,我可没有动手…
甚至都不用去进一步AST还原了!
看这里,又是document又是location的,关键还有个啥?"ie"啊,那么大的两个字母在那里别告诉我没看见,综合这几点一看,都熟悉了吧,好像刚刚才见过啊!
如果忘了的话我再来提醒一下各位。
是不是实锤了?那既然这样,咱们也就别不好意思了,咱都还没有还原呢,删掉你几个花指令就这样了,那大家就动手完事吧。回到完整代码中(由于没有AST还原,所以要执行的话还是需要靠这些花指令的,不是删了就完事了)在此处打上断点开始调试之路。
一看控制台…简直不要太明显对吧,那就往上跟一下看看,从调试情况来看,明显的_0x3640f1[0x0]
值就是我们需要的__jsl_clearance_s
参数值了,所以往上去跟一下_0x3640f1
参数,直接一步到位ctrl+f搜索其定义位置完事。
找到后一样的打上断点瞅瞅,但别忘了先给他换个行。
打上之后重新debug一下,代码就断在这此处定义了。
那接下来就来看一下他是个什么玩意儿咯。点击执行到下一步之后在终端输出一下看看。
这不明显了嘛,这家伙是一个数组!第一个元素就是我们要的目标,那既然这样,关键就是这应该怎么返回出去到python中才能应用呢?
难道要去梳理这代码逻辑找到点位后去输出吗?如果头铁的话大家可以去试试,咱们在这里选择直接取巧!反正我们已经调试知道了这个参数就是最终的与我们目标参数有关,那么你又是在一个方法里面,同时又是一个方法中的一个局部变量,那既然这样的话,总有你这个方法执行结束的时候吧?所以直接在方法执行末尾加上return _0x3640f1
完事,看结果。
拿到了,所以接下来就是从python中去拿这个结果就行了,只是Python中调用的时候也不要忘了传参就是了。而参数就是第二次访问时的js代码中调用go方法时的传参。
同样的还是一步一步实现一下,首先是携带第一次获取的Cookie进行请求并提取出go方法调用时传输的参数
def getSecCookie(self):
"""获取第二次返回的cookie"""
response = self.UGetRequest
sec_jscode = response.content.decode()
go_data = re.findall('}};go\((.*?)\)</script>', sec_jscode)[0] # 提取出js代码中需要传递的参数
dic_data = json.loads(go_data) # 转换成字典
然后将这个提取出来的参数转换成字典之后传入到本地的js代码中调用这个go方法
location_jscode = open('mfwjs.js', encoding='utf-8').read() # 此处打开的代码为第二次请求返回的JS代码,该JS代码就是用于生成第二次cookie的
js_res = execjs.compile(location_jscode)
__jsl_clearance_s = js_res.call('go', dic_data)[0]
self.cookies['__jsl_clearance_s'] = __jsl_clearance_s
这样我们的第二次cookie就设置好了。
2.3 详情页采集
那么将第二次的cookie带进去访问看看情况呢。
出现异常,但是这是必然情况,因为真实浏览器环境和我们本地环境肯定不一样,特别我们还是用了第三方模块,所以在于V8引擎(我配置过的默认是V8,大家没有配置的话默认应该是JSC)通信的时候存在一定的时差是必然的!所以如果出现这样的必然的误差,那么只需要去无限循环直到吻合的这一次就好。代码如下:
def detailDeal(self):
url_list = self.getIndexData() # 获取到一页的游记
for url in url_list: # 遍历游记列表
self.UGetRequest = {'url': url, 'rederer': ''}
self.getFirstCookie() # 第一次Cookie
while 1:
try:
self.getSecCookie() # 第二次Cookie
response = self.UGetRequest
text_html = response.content.decode()
print(text_html)
break
except Exception as e:
print(e)
continue
break
成功!那么我们将循环打开看一下(提取就正常的xpath就好,由于篇幅问题就不再分析,相对而言解析就是随便拿捏了的)
一开循环就暴露了吧,就只拿了一篇游记就直接报错,通过错误信息直接定位到问题是发生在第一次cookie提取中,所以是我们的逻辑出问题了吗?当然不是,只是我们目前来说考虑到的情况还不完全。试想一下,有没有一种可能在第一篇获取后我们就不用去重新获取新的cookie了呢?
为了验证我们的猜想,可以通过响应状态码来进行判断,因为如果有二次跳转的话那么第一次和第二次在获取设置cookie的时候其响应状态码为521,而如果成功的话状态码则是200。根据这样的思路,我们将getFirstCookie
方法进行修改如下:
def getFirstCookie(self):
"""获取第一次返回的Cookie"""
response = self.UGetRequest
status_code = response.status_code # 返回状态码
if status_code == 200: # 判断状态码如果为200则证明没有二次跳转,这种情况发生在第一篇游记请求成功之后
return response
res_data = response.content.decode()
res_jscode = re.findall('<script>document\.cookie=(.*?)location\.href=location\.pathname\+location\.search</script>',res_data)[0][:-1] # 提取出第一次返回的js代码
res_jscode = execjs.eval(res_jscode) # 执行JS代码并返回运行结果
__jsl_clearance_s = res_jscode.split(';')[0]
k, v = __jsl_clearance_s.split('=')
__jsluid_s = response.cookies.get('__jsluid_s') # 获取Cookie中__jsluid_s参数
self.cookies[k] = v
self.cookies['__jsluid_s'] = __jsluid_s
此处可以看到我们仅仅添加了一个状态码的判断,然后来到detailDeal
方法进行应用。
def detailDeal(self):
url_list = self.getIndexData() # 获取到一页的游记
num = 1
for url in url_list: # 遍历游记列表
print(f'------开始采集第{num}篇游记')
self.UGetRequest = {'url': url, 'rederer': ''}
FckResponse = self.getFirstCookie() # 第一次cookie
if not FckResponse: # 判断获取第一次cookie时是发生跳转还是响应成功,响应成功则证明是获取到了游记内容,此情况发生于第一次访问成功之后
# 如果没有响应成功,则证明需要我们进一步获取第二次的cookie再次请求
while 1:
try:
self.getSecCookie() # 第二次Cookie
response = self.UGetRequest
text_html = response.content.decode()
print(text_html)
break
except Exception as e:
print(e)
continue
else: # 如果成功,则直接通过返回的响应对象获取文本
text_html = FckResponse.content.decode()
print(text_html)
num += 1
print(f'-----第{self.page}页采集结束------')
# 上方循环结束则证明一页结束,接下来就是要翻页
# self.page += 1
# if self.page <= 3: # 采集3页的数据
# return self.detailDeal()
为了方便观察,此处添加了一点print作为提示。
可以看到执行结果,此时的第十篇游记刚好就是图中所示
三、小结
作为比较经典的JSL类型的反爬,难度只能算是一般般,所以大家在学习参考过程中尽可能的根据文章的给出的思路先行分析再去考虑其他因素,因为这个案例有更简单的方法进行优化,此处我们只是给出了对于JSL类型的反爬在遇到的时候应该有的解决思路,毕竟并不是只有MFW一个网站用到JSL,其他网站也有在使用的,并且还是魔改之后的,可能就不会说想这里一样的容易了。
爬虫路漫漫,愿有你相伴!
新开星球限时MF加入,欢迎进群!