前言:爬虫作为新秀,入门门槛非常低,但是要想学好爬虫技术还是非常有难度,所以本文章主要讲解js逆向爬虫
本次爬取的网站是某个音乐网站;主要通过接口的方式去获取相关数据,然后得到我们想要的东西;本次爬取的方式步骤很简单,但是分析是最难的;下面就给大家一步一步进行详解。
1、一般我们爬取音乐相关的内容都是先搜索到自己想要的内容后在进行爬取,那么首先进入网址后找到搜索接口
找到search这个就是搜索接口,然后现在外面查看接口的详情
参数:
响应结果:
可以看到接口返回的歌曲相关数据在typeTrack这个字段里面,刚好20条数据,结合传的参数和响应结果证明该接口就是搜索接口那么现在外面在来看参数,有过前后端开发经验的朋友们都知道,接口当中有些参数很重要,缺少其中一个参数都会请求失败,那么接回刚刚说的参数问题;看上面的参数截图可以发现这个接口中总共6个参数,好像都是缺一不可那种,尤其是sign这个参数,有过前后端开发经验的都知道这是验签(签名验证),如果没有这个参数,那么请求必定会失败,因为这是验证这个请求是否合法的参数;
通过上面两次请求的搜索接口发现sign这个参数每次请求都在变,那么它是怎么变的呢?这就是本次给大家带来的知识 -JS逆向
首先我们得明白JS逆向是什么?我们都知道现在web页面的编程语言是HTML和JAVASCRIPT(js),所以前端页面的相关逻辑都是用js写的,但是js是不展示到页面的,而且js经过编译后已经变得跟写的时候完全不一样了;但是总归有方法的,那就是js逆向;
但是js逆向得先找到js才行,怎么找js呢?往下看;可以看到每个请求后面都有一个js,点击它
进入到的是一个资源页,那么这个下面就是目前该网站的所有静态js,所有的js都在这里面了;然后js找到了那怎么开始逆向呢?
首先回到刚刚参数哪里,我们js逆向的目的是为了找到sign是怎么生成的,所以现在就要去整个js里面去搜索关于sign这个参数;由于这个是一个耗费大量时间的过程,这里不过得介绍,最终我们在下面这个js里面找到了sign生成的方法
在上面可以看到sign生成的方法是createSign(e) 这个function方法;那么现在方法找到了,然后再去看怎么生成的?首先这个sign后面接的是md5(r += secret)这个东西,然后一看就知道md5(r += secret)这个也是一个方法,然后在上面找到了md5()这个方法,其实就是一个普通的md5加密方法,这里不过多简述,有兴趣的可以去看看MD5加密方式;但是md5这个方法中间还传了参数 r += secret(其实就等价与 r=r+secret);咱们一步一步分析,接着往下看
可以发现secret这个参数在上面已经定义了,所以先拿下它,把secret这个参数记下来;然后接着分析r,可以发现r是通过for循环生成的,而下面for循环是根据传入createSign这个方法的参数e进行循环;所以现在要找到e参数传的到底是什么?所以接下来就是最重要的一步;断点调试
在方法关键点上打上断点后进行搜索操作,然后运行断点;可以发现最终传入md5的就是一个字符串:appid=16073360×tamp=1703153606&type=&word=许嵩0b50b02fd0d73a9c4c8c3a781c30845f
仔细观察可以发现这个好像就是前面我们传参哪里的参数然后拼上的secret啊,原来这个方法其实就是处理存入参数然后进行拼接生成的一个字符串;由于这个字符串是固定格式的只是里面参数不同而已,所以直接把这个字符串写下来,然后修改里面的值就行了;上面还有一个参数忘记说了,那就是timestamp,这个就是时间戳,11位的时间戳;
到此我们sign和timestamp参数的生成方式都知道了后,那么可以开始伪造自己请求的代码了
#md5加密
def md5Encode(self,str):
md5 = hashlib.md5()
md5.update(str.encode('utf-8'))
return md5.hexdigest()
#获取时间戳
def getTimeStamp(self):
return int(time.time())
def searchMusic(self,word,pageNo,pageSize):
timeStamp = self.getTimeStamp() #获取时间戳
args = {
"sign": "",
"word": word,
"type": 1,
"pageNo": pageNo,
"pageSize": pageSize,
"appid": self.appid,
"timestamp": timeStamp
}
#这个是生成sign的拼接字符串;然后把里面的参数都替换了而已
signParam = "appid={0}&pageNo={1}&pageSize={2}×tamp={3}&type=1&word={4}{5}".format(self.appid,pageNo,pageSize,timeStamp,word,self.secret)
#生成sign的md5方法
args["sign"] = self.md5Encode(signParam)
res = requests.get(url=self.searchUrl,params=args)
return res.json()
到此为止搜索接口完成
二、歌曲详情文件地址接口
首先我们要知道,要播放一首歌,肯定要有源文件才行,要么是文件路径地址,要么是byte流;现在外面要找到这个接口,怎么找呢?肯定是在播放界面找啊,你要播放那就得要播放文件;所以现在进入播放页面然后挨个找接口
刚好该页面只有三个接口,找起来太容易了,最后在tracklink这个接口找到了我们要的东西 path;这个后面就是我们最终要的文件路径;这个是一个oss文件服务器的链接地址,这个可以复制出来在游览器打开后就能播放歌曲或者是下载,找到后同样需要对该接口进行分析
同样的这个接口还是有sign以及timestamp这两个传参,现在还是要对sign的生成方式进行js逆向分析;同样的操作
找到这个生成方式后跟上面搜索接口一样进行断点调试,查看md5()里面传入的参数;但是注意,因为这里有几个接口,所以可能打上断点后会运行几次createSign这个方法多次,所以有可能第一次找到的不一定是自己要的,所以还是要看参数是不是自己需要的参数
比如说下面这个参数然后跟我们接口的参数进行对比,很明显不像,因为上面接口传参中TSID只有一个参数,但是下面这个又三个参数,所以这里要注意一下,找到属于接口的那个参数
这个才是我们接口需要的参数
找到后同样跟搜索接口一样编写我们需要的代码块;注意这里有个参数就是TSID,这个东西其实就在我们搜索接口返回的数据里面,所以下面就是我们获取链接参数的方法
# 获取音乐详细信息
def getMusicInfo(self,TSID):
timeStamp = self.getTimeStamp()
args = {
"sign": "",
"appid": self.appid,
"TSID": TSID,
"timestamp": timeStamp
}
signParam = "TSID={0}&appid={1}×tamp={2}{3}".format(TSID,self.appid,timeStamp,self.secret)
args["sign"] = self.md5Encode(signParam)
res = requests.get(url=self.musicInfoUrl,params=args)
return res.json()
到此为止搜索接口、获取歌曲文件地址接口都编写完成,接下来就是优化了;然后下面是整个爬取的代码块
import requests,time,hashlib
class DownloadMusic:
#初始化配置
def __init__(self):
self.searchUrl = "https://music.91q.com/v1/search"
self.musicInfoUrl = "https://music.91q.com/v1/song/tracklink"
self.secret = "0b50b02fd0d73a9c4c8c3a781c30845f"
self.appid = 16073360
#md5加密
def md5Encode(self,str):
md5 = hashlib.md5()
md5.update(str.encode('utf-8'))
return md5.hexdigest()
#获取时间戳
def getTimeStamp(self):
return int(time.time())
#音乐搜索函数
def searchMusic(self,word,pageNo,pageSize):
timeStamp = self.getTimeStamp()
args = {
"sign": "",
"word": word,
"type": 1,
"pageNo": pageNo,
"pageSize": pageSize,
"appid": self.appid,
"timestamp": timeStamp
}
signParam = "appid={0}&pageNo={1}&pageSize={2}×tamp={3}&type=1&word={4}{5}".format(self.appid,pageNo,pageSize,timeStamp,word,self.secret)
args["sign"] = self.md5Encode(signParam)
res = requests.get(url=self.searchUrl,params=args)
return res.json()
# 获取音乐详细信息
def getMusicInfo(self,TSID):
timeStamp = self.getTimeStamp()
args = {
"sign": "",
"appid": self.appid,
"TSID": TSID,
"timestamp": timeStamp
}
signParam = "TSID={0}&appid={1}×tamp={2}{3}".format(TSID,self.appid,timeStamp,self.secret)
args["sign"] = self.md5Encode(signParam)
res = requests.get(url=self.musicInfoUrl,params=args)
return res.json()
# 处理音乐详细信息后获得音乐数据链接
def getMusicUrl(self,word,pageNo,pageSize):
global musicInfo
musicUrlList = []
searchMusicListInfo = self.searchMusic(word,pageNo,pageSize)["data"]["typeTrack"]
#想要获取歌曲的相关信息都在self.searchMusic(word,pageNo,pageSize)里面,想要什么东西自己去根据字段找就行
for item in searchMusicListInfo:
#捕获异常是因为在获取音乐详情数据地址时字段有些不一样,导致获取会报错,然后捕获异常后继续处理改变了字段的数据
try:
musicInfo = self.getMusicInfo(item["assetId"])
print(musicInfo["data"]["path"])
musicUrlList.append(musicInfo["data"]["path"])
except: #处理不同字段下的数据
print(musicInfo["data"]["trail_audio_info"]["path"])
musicUrlList.append(musicInfo["data"]["trail_audio_info"]["path"])
time.sleep(1) #进行爬取时最好加上休眠时间,有些请求做了反爬处理,如果同一ip在1s内处理达到阈值会封ip
return musicUrlList
#主运行方法
def main(self):
# 搜索内容
word = "许嵩"
#当前页数
pageNo = 1
# 每页的数量
pageSize = 20
#data就是我们爬取的所有歌曲链接地址,这个地址可以直接用方法下载
data = self.getMusicUrl(word,pageNo,pageSize)
print(data)
if __name__ == "__main__":
DownloadMusic().main()