python爬虫实例:抓取音乐

第三方图床真折腾,先是新浪图床防盗链,再是sm图片无缘无故删图片,服了哦…

上次做完了一个抓取贴吧图片的爬虫,当时就感觉爬虫也就那么一回事…然而今天做这个实例的时候发现上次的爬虫实在太简陋了,抓取图片只要审查页面元素就好了,一下子就找到超链接了,实在是无趣了一些。相比于上一次的实例,这次学到的东西更多了…不过对于网络这块还是一窍不通,实在不想深入了解这方面。

做完这个爬虫有一天了,昨天花了很多时间去实现功能,今天花了很多时间去优化以及尝试其它平台。然而,现在各大音乐平台都很难去找到接口,更加不可能抓到付费或者无版权的歌曲…也不代表没有吧,或许有大神搞到接口…对于我这种小白,就只能想想了…

初步分析

这次的实例是抓取千千音乐的歌曲…(付费,免费,无版权,有版权都行!!!)

首先点开一首可以正常播放的歌曲,然后添加到播放列表,然后进入网站自带的播放器界面,也就是:此处

1


然后我们F12查看元素,发现在Elements里面是找不到歌曲链接的,因此用前面学到的知识没办法继续下去,不过没关系,chrome不只有这一个功能,我们点开 Network选项,此处是加载的资源概况。慢慢找,可以看到一个name.mp3的元素挺显眼的,歌曲也是mp3格式的,所以就是说这个资源就是服务器给我们浏览器的,而且我们打开这个URL发现正好就是演员这首歌曲,而且还可以直接下载,那不就好了,之后想下载什么歌曲就这样找什么歌曲呗。

2

链接分析

但是。。。咱们是为了学爬虫,手动下载一首歌有啥用?那我要把薛之谦的歌曲全部下载下来呢,难道要一个个手动去下载?那很累,不建议,而且这样获取不到那些收费和无版权歌曲。所以咱们分析一下那个URL吧。

1
http://zhangmenshiting.qianqian.com/data2/music/a70ef051941a4a620d15d9588845f46c/612410195/612410195.mp3?xcode=031ff7d93638dabe2e999d51bc758b81

http协议开头,然后是一个二级域名zhangmenshiting.qianqian.com再接了一个目录data2/music/。比较其他歌曲的URL发现只有前面的部分是一样的,就是说千千音乐服务器根据歌曲来给不同的URL,所以我们要做的就是找到这个链接的规律,然后爬虫就可以批量下载歌曲了。

每首歌的URL不一样,而且都带了一个不同xcode,暂且不看其他部分,xcode肯定是关键词。但是在这个界面我们是想不到xcode从哪里来的,我们换个思路,既然可以找到歌曲链接,所以服务器那边一定把链接在哪个地方了。所以我们直接在控制台搜索xcode值。

3

寻找有用的信息

好吧,我们索引之后发现只有三个文件带有那个xcode,可以发现前面两个文件就是第一次找的那个.mp3文件(为什么是两个…)。直接看第三个文件吧。点开songlink(直译不就是歌曲链接?),可以看到以下的信息。
4

可以看到头部有四个部分,分别是:General Response Headers Requesr Headers Form Data
这里就不多提了,关于网络的东西足够学好久了,只看请求链接和请求协议以及数据。
URL在这,一个POST请求,直接手动打开是没有什么东西的,因为POST请求需要带有data参数,所以这也就是为什么有一个From data,所以就是说我们让爬虫携带data然后对这个URL发起POST请求不就好了?当然,这是后面要做的,现在还是好好分析这个文件,头部看完了就直接看响应信息体response吧,其它的还是跳过为好。


由于response是没有格式化的,所以直接截图的Preview也就是经过浏览器处理后的信息。
5

这里信息可真是多啊,基本这首歌的信息全有了,可以看到xcode就在这里…而且之前那个链接也在这里(刷新界面链接会变化,所以和上面的链接不同是正常的)。其实我们可以想到,是先有这个文件再有之前那个.mp3文件的,就是从这个文件中拿到xcode或者说拿到歌曲URL然后再获取到歌曲,我们才听到歌曲了。到这里分析就完了,下面整理如下思路。

我们找的一个漏洞就是这个网页自带的播放器,先有播放器后有歌曲。意思呢就是打开播放器,如果播放器中有很多歌曲,服务器首先要返回这些歌曲的资源信息给我们,总不能说我们不听它就不返回数据吧,也就是说打开播放器服务器就已经把歌曲信息发给我们了,不信可以多添加几首歌曲进来,会发现songLink会有那些歌的所有信息。也就是说我们只要对http://play.taihe.com/data/music/songlink发起POST请求就可以得到那个Response,响应信息体里面也有我们想要歌曲的URL,有了歌曲的URL我们就可以直接保存信息了,所以这就是大致的思路。

爬虫开始第一步请求

第一步肯定是对这个链接发起请求,与GET请求不同的是POST需要携带参数才行,也就是说之前看到的那个From data我们需要加进去,但是我过滤了其他默认的数据,就是发起请求的时候只加songIds,其实也就是歌曲的ID值,这个值很好找,随便点开一首歌曲,它URL的结尾就是它的ID。这里还用演员的ID,代码如下:

1
2
3
4
5
6
import requests

Root_url = "http://play.taihe.com/data/music/songlink"
From_data = {'songIds': '242078437'}

response = requests.post(url = Root_url, data = From_data)

这里发起请求之后response可不是服务器看到的那些数据o,它是一个响应信息,就是发起了POST请求之后返回的状态码,打印出来就是<Response [200]>,百度可以知道200是正常响应值,也就是说咱们的请求成功了,这一刻,神州沸腾了…好吧,回到之前,既然我们用浏览器的控制台可以看到响应正常后的那些数据,那这里怎么看数据呢?

其实吧,这些数据很常见,前两天我改bilibili个人信息的时候也是改的这种数据,不卖关子了,也就是json数据,我们可以把浏览器的控制台里面的Response里面的信息复制出来,然后找一个JSON数据格式化的网站,格式化之后就可以看得很清楚了,那这里我们也就调用response.json()就好了,数据如下(刚才好像千千音乐把我的IP屏蔽了…爬虫不犯法吧,学习…我只是为了学习…)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
'errorCode': 22000,
'data': {
'time': 3600,
'xcode': '7cf461c96271034822dcc64431de272b',
'songList': [{
'queryId': '242078437',
'status': 0,
'songId': 242078437,
'songName': '演员',
'artistId': '88',
'artistName': '薛之谦',
'albumId': 241838068,
'albumName': '初学者',
'lrcLink': 'http://qukufile2.qianqian.com/data2/lrc/d2dd6042c4b233b2e902206a2f2447bf/612410245/612410245.lrc',
'time': 261,
'linkCode': 22000,
'songLink': 'http://zhangmenshiting.qianqian.com/data2/music/15414b755fbf0ab7caaac12957051f3a/612410192/612410192.mp3?xcode=471de392a76515a808a65907a49a17b0',
'showLink': 'http://zhangmenshiting.qianqian.com/data2/music/15414b755fbf0ab7caaac12957051f3a/612410192/612410192.mp3?xcode=471de392a76515a808a65907a49a17b0',
'format': 'mp3',
'rate': 128,
'size': 4182351,
'linkinfo': None,
'version': '',
'copyType': 0,
'enhancement': '3.019997'
}]
}
}

数据处理

这个数据里面就有我们想要的数据,怎么提取出来呢?这外边是一个字典,取出data,发现data还是一个字典,再取出songList,发现songList是只有一个元素且这个元素是字典的列表,好吧,那就取出songList[0],也就是用一个字典保存,然后保存我们想要的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests, pprint

Root_url = "http://play.taihe.com/data/music/songlink"
From_data = {'songIds': '242078437'}

response = requests.post(url = Root_url, data = From_data)
music_info = response.json()['data']['songList'][0]

albumName = music_info['albumName']
artistName = music_info['artistName']
lrcLink = music_info['lrcLink']
showLink = music_info['showLink']
songName = music_info['songName']

保存了专辑名,作者名,歌词链接,歌曲链接,歌曲名字,数据如下:

1
2
3
4
5
演员
初学者
薛之谦
http://qukufile2.qianqian.com/data2/lrc/d2dd6042c4b233b2e902206a2f2447bf/612410245/612410245.lrc
http://zhangmenshiting.qianqian.com/data2/music/15414b755fbf0ab7caaac12957051f3a/612410192/612410192.mp3?xcode=90cad862d2e3eb0b3d7cab37c0a62272

好吧,到这里就完事了,我们有了歌曲名,有了各种信息,而且有了歌曲链接还不是我们手动去找的,何乐而不为呢?下面就保存吧。完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import requests, os

Root_url = "http://play.taihe.com/data/music/songlink"
From_data = {'songIds': '242078437'}

response = requests.post(url = Root_url, data = From_data)
music_info = response.json()['data']['songList'][0]

albumName = music_info['albumName']
artistName = music_info['artistName']
lrcLink = music_info['lrcLink']
songLink = music_info['songLink']
songName = music_info['songName']

# path = 'D:\\temp\\'
# os.chdir(path)
# os.mkdir(artistName)
# os.chdir(path + artistName)
# os.mkdir(albumName)
# os.chdir(path + artistName + "\\" + albumName)

print("正在保存歌曲:" + songName)
response = requests.get(songLink)
with open(songName + '.mp3', 'wb') as f:
f.write(response.content)

#print("正在保存歌词...")
response = requests.get(lrcLink)
with open(songName + '.lrc', 'wb') as f:
f.write(response.content)
print("执行完毕...")

6

如何批量获取?

通过上面的代码只要更换ID值就可以抓到任意歌曲…(如果千千音乐服务器有那首歌),不过这就完了吗?就这样的话我就不好说学到了很多东西了…继续往下走。咱们一次只抓一首歌还是太慢了,虽然比人手动快,但是ID值竟然还要我们自己去找,简直….不可理喻啊。我最主要的目的是为了什么?学习,附带把周杰伦的歌下载下来,嘿嘿。现在千千音乐是没有周杰伦的版权的,那么用上面那段代码更换ID能不能行呢?可以试一试哦~

我们需要一个歌曲列表,就好比下面的:
7

全是灰色的呀,想起以前,听歌怎么会收费,怎么会有版权一说?世代在变化,现在歌曲慢慢注重收费版权等等,而抄袭借鉴也被越来越重视…就比如莫裁缝,某花…还是不说这些了,我还是更注重歌好听与否。这里我们可以看到一个列表,像第一个爬虫一样,找id是一件很简单事情,开始吧。

8.png

我们就是要找到这些a标签href里面的那串数字,那就是我们需要的歌曲ID,和上一个不同的是这次使用的正则表达式获取,相对而言更专业一些?好吧,其实前几天改bilibili数据的时候也接触到了正则表达式,发现也不是那么的难,具体还是看代码吧,哦,忘记了,需要一个模块re。找到了所有的ID值再用一个循环请求呗,没毛病。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import requests, re
import os

song_list_url = "http://music.taihe.com/artist/7994"
response = requests.get(url = song_list_url)
id_list = re.findall('href="/song/(\\d+)"', response.text)

for song_id in id_list:
Root_url = "http://play.taihe.com/data/music/songlink"
From_data = {'songIds': song_id}

response = requests.post(url = Root_url, data = From_data)
music_info = response.json()['data']['songList'][0]

albumName = music_info['albumName']
artistName = music_info['artistName']
lrcLink = music_info['lrcLink']
songLink = music_info['songLink']
songName = music_info['songName']

path = 'D:\\temp\\'
os.chdir(path)
# os.mkdir(artistName)
# os.chdir(path + artistName)
# os.mkdir(albumName)
# os.chdir(path + artistName + "\\" + albumName)

print("正在保存歌曲:" + songName)
response = requests.get(songLink)
with open(songName + '.mp3', 'wb') as f:
f.write(response.content)

#print("正在保存歌词...")
response = requests.get(lrcLink)
with open(songName + '.lrc', 'wb') as f:
f.write(response.content)
print("执行完毕...")

9.png

为什么只有一页?

还没完,可以看到上面的输出,其实抓完一页爬虫就停了,也很正常,我们没有翻页,当然只能获取到第一页歌曲的ID,接下来就翻页吧。

10.png

好了,咱们手动翻页完了,很简单对不对,只要点一下就行了,但是爬虫做不到。不难发现,我们点击第二页之后浏览器上面那个URL并没有变化,而且第二页那个按钮或者是那个下一页都不是特定的超链接,经过百度“翻页URL不变”发现这种界面是动态刷新的,就是点下一页的时候之刷新局部内容而不是整个页面,这个技术叫做ajax,好巧好巧,我们两个星期帮同学做一个.NET小项目“实时聊天室”的时候就接触到了这个东西,它就是局部刷新聊天界面的,有信息就刷新,而我们这个页面点下一页或者第几页就刷新对应的内容。

我们这个爬虫做不到,但是有一个叫做webdriver的东西能做到,这东西需要百度使用,类似于让爬虫模拟人的行为去浏览器获取信息,就是自动化的打开浏览器然后执行操作,具体事先如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import requests, re, os, time
from selenium import webdriver
from bs4 import BeautifulSoup

song_list_url = "http://music.taihe.com/artist/7994"

driver = webdriver.Chrome()
driver.get(song_list_url)

i = 1
while True:
response = driver.page_source
id_list = re.findall('href="/song/(\\d+)"', response)
isEnd = BeautifulSoup(response, 'html.parser').find('span', class_='page-navigator-next')

for song_id in id_list:
Root_url = "http://play.taihe.com/data/music/songlink"
From_data = {'songIds': song_id}

response = requests.post(url = Root_url, data = From_data)
music_info = response.json()['data']['songList'][0]

albumName = music_info['albumName']
artistName = music_info['artistName']
lrcLink = music_info['lrcLink']
songLink = music_info['songLink']
songName = music_info['songName']

path = 'D:\\temp\\'
os.chdir(path)
# os.mkdir(artistName)
# os.chdir(path + artistName)
# os.mkdir(albumName)
# os.chdir(path + artistName + "\\" + albumName)

i += 1
print(str(i) + ".正在保存歌曲:" + songName)
response = requests.get(songLink)
with open(songName + '.mp3', 'wb') as f:
f.write(response.content)

#print("正在保存歌词...")
response = requests.get(lrcLink)
with open(songName + '.lrc', 'wb') as f:
f.write(response.content)

if not isEnd:
print("点击下一页...")
driver.find_element_by_link_text('>').click()
time.sleep(1)
else:
print("所有页面已遍历完成")
break

11.png

到这里其实实现差不多了,webdriver是一个很好用的工具,这里就不多提了,百度有很具体的用法。我判断最后一页用的一个isEnd,它的原理在于最后一个>变成了一个span标签,如果到最后一页这个外面的循环就直接跳出。但是有一点是如果歌手下面的评论有多页,也就是不只有一个>,爬虫就会绕很大的弯,还有就是如果歌曲链接是空或者歌词链接为空程序会报错停止,也就是说在保存的时候我们需要判断一下,具体还有很多细节处理,这里就不一一写出来了。

但是,这样就完了吗?

不,还没有完,最后还得提一点,我们一直没有关注的Cookies,这个东西是什么呢?有什么作用呢?怎么用呢?

12.png

这么一长串的东西,可以百度一下Cookies的作用…稍微介绍一下,服务器会对Cookies进行分析来判断我们是以什么身份发起请求的,简单的说就是你登录了和没有登录是不一样的!!!记住,这点很重要,如果你有一个会员账号,那么你就可以请求到高音质的歌曲,所以这个Cookies就可以用来…很巧,我们爬虫完全可以携带这个Cookies来发起请求,也就是说我们携带一个Cookies发起请求可以得到高音质的音乐链接,然后保存…嘿嘿嘿。当然,前提是你有会员账号的Cookies而且这个Cookies有效期不是很短很短的那种(有点网址刷新Cookies很快)。其实还有一种办法,就是让我们的爬虫先登录再请求,就是登录之后的Cookies来请求的了,哈哈,那怎么模拟登录呢?又是一个坑点,这里千千音乐的登录界面是类似弹窗的,需要复杂一点的实现,我弄了一下发现搞不上去就用的自己手动登录的Cookies,到这里实例就差不多了。

之后呢?

其实吧,今天我看了很多网站,发现都没有这么容易抓取,就是说像QQ音乐网易云音乐这种都是没办法直接抓付费或者无版权音乐的,它们的请求URL不像千千音乐这个是不变的。

就比如QQ音乐,它连歌曲列表都给你藏起来了,叫你下载客户端才能显示完全,你说这不…其实这种情况也不是没办法,我们可以用各种抓包软件抓到歌曲的ID,我用手机的Thor也抓到了,但是又有什么办法呢?付费歌曲只给你一分钟,明面上咱们都找不到完整歌曲的链接,而且没有像千千音乐这种先播放器后音乐的逻辑。

说实话千千音乐真的不是很完善,也许它以后也会改版吧。所以趁现在还可以爬取一些音乐就爬一下吧,学习…忘记说了,QQ音乐的请求URL带了一堆参数,是一堆,你QQ号也带进去了,还有一个随机的vkey….反正我是找不到它的接口…继续学习吧,这次还是学到了许多新知识了,慢慢进步了。