- 一、爬虫是什么?
- 二、使用步骤
- 1.引入库
- 2.分析网站
- 3.扣js代码
- 3.访问链接,提取数据,下载文件
- 总结
一、爬虫是什么?
网络爬虫(又称为网页蜘蛛,网络机器人,在FOAF社区中间,更经常的称为网页追逐者),是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本。另外一些不常使用的名字还有蚂蚁、自动索引、模拟程序或者蠕虫。
二、使用步骤 1.引入库
import execjs import os import requests import json from prettytable import PrettyTable
execjs是python的一个可以执行js代码的类库
os库用来实现音乐文件的下载
requests和json库用来获得并解析请求的数据
prettytable 用来生成美观的表格
2.分析网站
打开开发者工具随便点开一首音乐播放
可以正常播放,接下来就是分析并构造这个得到的链接
https://xxxxxx.xxxxxx.com/71_53_T10038957307_128_4_1_0_sdk-cpm/cn/0311/M00/09/2A/ChAKDF1svm6AHYSzAD6g01xj8aU721.mp3?xcode=eee00a33db9116c68c5c93c960b8f1d427bd421
首先,这个链接特别复杂,还有路径也是,唯一一个突破口就是后面的参数xcode,所以考虑在开发者工具里搜索xcode字段
如图,得到六个结果,但是都不是我们要的xcode,有wxcodeVisible,wxcode等类型的含xcode字符串的变量,在前端文件里没有搜索到,我们可以考虑这个链接是否会包含在响应体里,然后我就在network里搜索xcode
果然被我找到了,在这个响应体里包含了一首歌全部的信息,并且这个音乐文件的链接也在其中
然后就该想想怎么构造这个链接了
https://XXXXX.XXXXX.com/v1/song/tracklink?sign=86c3590d5397c7fd770ef967fa2b4bc9&appid=16073360&TSID=T10038957304×tamp=1630377809
这就是刚刚那个得到xcode信息的请求头,我们可以看到这一共四个参数,sign,appid,TSID,timestamp,timestamp是时间戳,TSID是每首歌曲的ID,appid是不变的,应该是这网站的ID,只有sign最可疑,很明显,86c3590d5397c7fd770ef967fa2b4bc9这串32位的16进制数是通过md5加密方式得到的,所以重复刚刚的步骤,在前端文件里搜索sign。
但是仔细看,在六个匹配结果里面有2个sign: md5(r += secret),分别在37787行和40395行,经过我打断点调试发现,这两处加密的sign分别对应获取音乐文件详情(里面包含音乐文件链接)时的sign和用一个关键字搜索来得到后端返回的音乐列表的请求的参数sign,两个md5加密对应两个不同的sign创建方式
同学们可能看蒙了,现在的情况是这样的:有两个sign的加密方式
这是有xcode的链接
https://XXXXX.XXXXX.com/v1/song/tracklink?sign=86c3590d5397c7fd770ef967fa2b4bc9&appid=16073360&TSID=T10038957304×tamp=1630377809
这是通过搜索关键字得到音乐列表的一个链接,appid有了,时间戳在代码里创建,如果有TSID就能构建这样的链接
https://XXXXX.XXXXX.com/v1/search?sign=ebb28a01cc860659c1efb5da8332ba12&word=%E8%AE%B8%E5%B5%A9&type=1&pageSize=20&appid=16073360×tamp=1630380738
word就是要传的关键字,type等于1是固定的(意思是要后端给我有关的音乐),估计是一个请求类型吧,pageSize是我们想要的音乐数量,appid是固定的,时间戳也是代码里创建,所以我们先构建这个链接得到TSID,再用TSID就能构建第一个链接。
很明显,后端就是根据其他参数来验证sign,符合前后端统一的检验规则就能给你返回正确数据,否则就返回错误信息。
我现在想要这样一个过程,即先在我的python程序里传入一个关键字,和搜索范围(多少首音乐)先得到音乐列表,再,选择序号,下载对应的音乐,两者联系起来的关键就是TSID,而在第二个链接得到的json数据里,是有TSID的,如下图。
一个TSID对应一首歌,得到TSID就可以用它来构造第一个请求得到包含Xcode链接的url,也就是音乐文件,这是经过许多次开发者工具里调试js代码得到的结论。
那就来构建第二个链接吧,sign怎么得到呢,回到刚刚的问题,我已经得到md5的js代码了,前后端校验sign的规则是一样的,那我只有传参给js,得到sign的返回值就可以了,可是我太会前端js呀,而且我也根本不懂md5加密方式怎么办?没事,扣js代码
可以看到时间戳的创建方式:var t = Math.floor(Date.now() / 1e3);
传入怎样的参数:{TSID: ‘T10044518961’, appid: 16073360}
得到怎样的结果:return { sign: md5(r += secret), timestamp: t,md5: md5},这里有我们需要的时间戳和sign,取出来构造链接就可以了。
那我在python程序里怎么用js代码呀? 这时execjs库就起到作用了,众所周知,js能在node环境里运行,所以我们就可以用python创建一个node环境,加载这个js文件就可以了,python代码如下所示:
def getsonglist(name,number):
# url = "https://xxxxx.xxxxx.com/v1/search?sign={}&word={}&type=1&pageSize=20&appid=16073360×tamp={}".format()
ctx = execjs.compile(open('songlist.js', encoding='utf-8').read())
funcName1 = "createSign({word: " + name + ", type: 1, pageSize: "+number+", appid: 16073360}).sign"
funcName2 = "createSign({word: " + name + ", type: 1, pageSize: "+number+", appid: 16073360}).timestamp"
sign = ctx.eval(funcName1)
timestamp = ctx.eval(funcName2)
# print(sign)
# print(timestamp)
name = eval(name)
url = "https://xxxxx.xxxxx.com/v1/search?sign=" + sign + "&word=" + name + "&type=1&pageSize="+number+"&appid=16073360×tamp=" + str(
timestamp)
return url
另一个sign的md5加密也在这展示一下:
传入参数{word: ‘张雷’, type: 1, pageSize: 20, appid: 16073360}也能得到sign和时间戳。这样就能构建第二种链接了。其中pageSize是默认的20,我们可以自己定义。
还有,扣js代码要考虑到函数之间的依赖关系,下面就讲讲我是怎样扣js代码的:
其实我是不太懂js的,要我写,我肯定写不来这上千行js代码,折磨的我肯定不会全要,我只要能实现sign,和时间戳创建的代码就可以了,并且能返回到我的python程序中,整个过程,我不需要知道它是怎样实现的,就相当于,有一个函数,给他什么样的参数,它给我什么值,就是这样。
为此,我先准备一个参数:{word: ‘张雷’, type: 1, pageSize: 20, appid: 16073360}就用这个,
调用creatSign函数就可以了,以上是开发者工具控制台键入,我们先准备一个js代码调试工具,我用的是鬼鬼js调试工具,我先把creatSign函数放进去
如图,仅仅有creatSign函数是不够的,现在是我们的新环境里,不是在开发者工具里,所以还需要扣md5加密函数,md5加密函数又依赖createCommonjsModule函数,继续扣
secret没有定义,这边建议把全部js代码拷贝到自己的编辑器里,方便查找和查看,可以找到藏在js代码里的secret是写死的
secret = "0b50b02fd0d73a9c4c8c3a781c30845f"
成功得到sign,这边我把返回参数改了一下,只返回sign,返回对象的话,我这lj调试工具报错了,我也不知道怎么回事,但是放我pycharm里可以返回对象(包含sign和时间戳,另一个md5没有用),我这把js文件命名为songlist.js,扣完扣了200行,如下,使用方法,
def getsonglist(name,number):
# url = "https://xxxxxc.xxxxx.com/v1/search?sign={}&word={}&type=1&pageSize=20&appid=16073360×tamp={}".format()
ctx = execjs.compile(open('songlist.js', encoding='utf-8').read())
funcName1 = "createSign({word: " + name + ", type: 1, pageSize: "+number+", appid: 16073360}).sign"
funcName2 = "createSign({word: " + name + ", type: 1, pageSize: "+number+", appid: 16073360}).timestamp"
sign = ctx.eval(funcName1)
timestamp = ctx.eval(funcName2)
# print(sign)
# print(timestamp)
name = eval(name)
url = "https://xxxxx.xxxxx.com/v1/search?sign=" + sign + "&word=" + name + "&type=1&pageSize="+number+"&appid=16073360×tamp=" + str(
timestamp)
return url
sign = ctx.eval(funcName1)
timestamp = ctx.eval(funcName2)
字符串funcName1和funcName2就相当于在控制台输入的方法,sign和timestamp用来接收返回值
得到参数然后就能构造链接发送请求。
这只是第二个链接的构造,第一个链接的构造也是如此,我就不再啰嗦了。
3.访问链接,提取数据,下载文件
main函数
def main():
name = input("请输入歌手或歌名:")
name = "'" + name + "'"
# print(name)
number = input("请输入搜索范围:")
SongListJson = json.loads(getText(getsonglist(name,number)))
songs = handlejson(SongListJson)
print(songs)
number = input("*********************************选择你要听的歌的序号:*********************************n")
print("")
Id = SongListJson['data']['typeTrack'][int(number) - 1]["id"]
songName=SongListJson['data']['typeTrack'][int(number) - 1]["title"]
Id = "'" + Id + "'"
download(Id,songName)
得到链接响应内容:
def getText(url):
try:
header = {'User-Agent': 'Mozilla/5.0'}
r = requests.get(url, timeout=20, headers=header, stream=True)
r.raise_for_status()
return r.content
except:
print("获取数据失败")
处理音乐列表数据:
def handlejson(SongListJson):
table = PrettyTable(["序号", "歌曲", "歌手", "专辑", "要求会员?", "id"])
for i in range(SongListJson['data']['typeTrack'].__len__()):
table.add_row([str(i + 1), SongListJson['data']['typeTrack'][i]["title"],
SongListJson['data']['typeTrack'][i]["artist"][0]["name"],
SongListJson['data']['typeTrack'][i]['albumTitle'],
SongListJson['data']['typeTrack'][i]["isVip"], SongListJson['data']['typeTrack'][i]["id"]])
return table
构建得到音乐列表的链接:
def getsonglist(name,number):
# url = "https://XXXXX.XXXXX.com/v1/search?sign={}&word={}&type=1&pageSize=20&appid=16073360×tamp={}".format()
ctx = execjs.compile(open('songlist.js', encoding='utf-8').read())
funcName1 = "createSign({word: " + name + ", type: 1, pageSize: "+number+", appid: 16073360}).sign"
funcName2 = "createSign({word: " + name + ", type: 1, pageSize: "+number+", appid: 16073360}).timestamp"
sign = ctx.eval(funcName1)
timestamp = ctx.eval(funcName2)
# print(sign)
# print(timestamp)
name = eval(name)
url = "https://XXXXX.XXXXX.com/v1/search?sign=" + sign + "&word=" + name + "&type=1&pageSize="+number+"&appid=16073360×tamp=" + str(
timestamp)
return url
构建每个音乐文件详细信息并下载:
def download(Id,songName):
ctx = execjs.compile(open('abc.js', encoding='utf-8').read())
funcName1 = "createSign({TSID: " + Id + ", appid: 16073360}).sign"
funcName2 = "createSign({TSID: " + Id + ", appid: 16073360}).timestamp"
sign = ctx.eval(funcName1)
timestamp = ctx.eval(funcName2)
Id = eval(Id)
url = "https://XXXXX.XXXXX.com/v1/song/tracklink?sign={}&appid=16073360&TSID={}×tamp={}".format(sign, Id, timestamp)
# print(url)
ResponseJson=json.loads(getText(url))
musicUrl=ResponseJson["data"]["path"]
writeFile("下载音乐/"+songName+".mp3",getText(musicUrl))
总结
通过这个demo学到很多方面知识,尤其是我对浏览器的开发者工具的调试功能精进不少,也提高了我阅读分析Javascript代码的能力。还有,了解了一些网站对数据的保护措施以及反爬的手段。



