在分布式爬虫实战中,我们介绍了使用Requests爬取QQ音乐,虽然使用了多进程和多线程,在测试的时候效率仍然比较低,这篇文章将使用Scrapy爬取QQ音乐,实现相同的功能,代码大体上都是一样的,所以这里不再对QQ音乐网站进行分析,只介绍有关Scrapy的使用。
项目分析
在歌手列表(https://y.qq.com/portal/singer_list.html)中按照字母类别对歌手分类,遍历每个分类下的每位歌手页面,然后获取每位歌手页面的全部歌曲信息。分析遍历次数:
(1)遍历每位歌手的歌曲页数。
(2)遍历每个字母分类的每页歌手信息。
(3)遍历每个字母分类的歌手总页数。
(4)遍历26个字母和特殊符号分类的歌手列表。
统计遍历次数,主要是能让我们对项目开发有一个整体设计逻辑。使用模块化设计思想,整个项目模块的划分如下:
(1)歌曲下载。
(2)歌手信息和歌曲信息。
(3)字母分类下的歌手列表。
(4)全站歌手列表。
按照上述方案,Scrapy爬取QQ音乐的开发顺序如下:
(1)setting.py:配置爬虫信息,如请求头、数据库信息、文件保存路径。
(2)items.py:定义存储数据对象,主要存储歌曲相关信息。
(3)pipelines.py:数据存储,实现歌曲信息入库和歌曲下载。
(4)spiders文件夹(musicSpider.py):编写爬虫规则。
项目创建
首先创建Scrapy爬虫项目,命名为music,并在项目中的spiders文件夹里创建创建musicSpider.py文件:
scrapy startproject music
cd music
scrapy genspider musicSpider xxx.com
项目配置
完成项目创建后,接下来就进入项目开发。按照制定的开发顺序,首先完成项目配置的开发,打开项目的配置文件setting.py,由于文件在创建的时候已有较多的注释代码,因此此处只列出项目所需的代码内容。其代码如下:
# -*- coding: utf-8 -*-
BOT_NAME = 'music'
SPIDER_MODULES = ['music.spiders']
NEWSPIDER_MODULE = 'music.spiders'
# 定义请求头
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
"User-Agent":"Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)"
}
# 设置为False
ROBOTSTXT_OBEY = False
LOG_LEVEL='ERROR'
# 设置管道功能
ITEM_PIPELINES = {
'music.pipelines.MusicPipeline': 300,
'music.pipelines.DownMusicPipeline': 301,
}
项目分别对Item Pipelines、数据库信息、请求头和文件保存路径进行配置,各个配置说明:
● Item Pipelines:创建项目时,默认配置了类MusicPipeline。在此项目中,需要添加一个下载类DownloadMusicPipeline,该类继承自父类FilesPipeline,主要实现歌曲下载功能。
● 请求头:配置默认的请求头内容,如果项目中发送HTTP请求并没有指定请求头,就默认使用该配置作为请求头。
除此之外,还可以配置并发数和下载延时等相关信息,使用默认配置即可
定义存储字段
items.py主要用于定义歌曲信息的存储对象,衔接爬虫规则Spider和管道Item Pipelines,使两者之间的数据交互传递。歌曲下载链接song_url,除了写入MySQL数据库之外,还用于管道类DownloadMusicPipeline实现歌曲下载。综合分析,项目文件items.py的代码如下:
# -*- coding: utf-8 -*-
import scrapy
class MusicItem(scrapy.Item):
song_name = scrapy.Field() #歌名
song_album = scrapy.Field() #所属专辑
song_interval = scrapy.Field() #歌曲大小
song_mid = scrapy.Field() #歌曲mid
song_singer = scrapy.Field() #歌手
song_url = scrapy.Field() #歌曲播放链接
定义管道类
根据items.py的存储字段来实现数据存储功能,数据存储功能是在项目文件pipelines.py里实现,该文件主要实现两个功能:歌曲信息入库和歌曲下载。
1.歌曲信息入库歌曲信息入库主要由MusicPipeline实现,该类在创建项目时已自动生成,但具体的存储过程还需要自行编写相关代码,数据存储的代码如下:
# -*- coding: utf-8 -*-
import requests,os
from pymongo import MongoClient
class MusicPipeline(object):
def open_spider(self,spider):
self.client=MongoClient(host='127.0.0.1',port=27017)
self.database=self.client.Music
self.collection=self.database.music
def process_item(self, item, spider):
song_url=item['song_url']
song_name=item['song_name']
song_album=item['song_album']
song_singer=item['song_singer']
self.collection.insert_one({'song_name':song_name,'song_album':song_album,'song_singer':song_singer,'song_url':song_url})
return item
def close_spider(self,spider):
print('存储到mondodb成功!')
1.歌曲下载歌曲下载主要由DownMusicPipeline实现,代码如下:
class DownMusicPipeline(object):
def process_item(self,item,spider):
if item['song_url'] and 'vkey' in item['song_url'] :
url = 'http://isure.stream.qqmusic.qq.com/%s' % item['song_url']
res = requests.get(url,timeout=30)
print('正在下载歌曲:%s-%s' % (item['song_singer'], item['song_name']))
file_path = '{0}/{1}/{2}'.format('G:', 'a_music', item['song_singer'])
path = '{0}.m4a'.format(item['song_name'])
if not os.path.exists(file_path):
os.makedirs(file_path)
with open(os.path.join(file_path, path), 'wb') as f:
f.write(res.content)
return True
else:
with open(os.path.join(file_path, path), 'wb') as f:
f.write(res.content)
return True
return item
编写爬虫规则
爬虫规则是整个项目中的难点,同时也是代码量最多的一个功能。整个程序共发送了6个不同的请求。对于Scrapy的Spider来说,一个HTTP请求是以一个类方法表示,因此本项目的Spider共有6个类方法,相应的功能有:
(1)歌手字母分类A~Z和特殊符号#。
(2)获取每个字母分类下的每页歌手。
(3)获取每一个歌手信息。
(4)获取歌手的每一页歌曲。
(5)获取每一页的每一首歌曲信息。
(6)每一首歌曲信息。
综合上述分析,项目文件musicSpider.py的爬虫代码如下所示:
# -*- coding: utf-8 -*-
import scrapy,re,os
import json,math,requests
from urllib.parse import quote
from music.items import MusicItem
class MusicspiderSpider(scrapy.Spider):
name = 'musicSpider'
allowed_domains = ['qq.com']
#start_urls是歌手列表
start_urls=[
'https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=jsonp&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0&data=%7B%22comm%22%3A%7B%22ct%22%3A24%2C%22cv%22%3A0%7D%2C%22singerList%22%3A%7B%22module%22%3A%22Music.SingerListServer%22%2C%22method%22%3A%22get_singer_list%22%2C%22param%22%3A%7B%22area%22%3A-100%2C%22sex%22%3A-100%2C%22genre%22%3A-100%2C%22index%22%3A',
'%2C%22sin%22%3A0%2C%22cur_page%22%3A1%7D%7D%7D'
]
# 遍历歌手字母分类A-Z和#
def start_requests(self):
#lua="""function main(splash)
#splash:go("https://y.qq.com")
#splash:wait(3)
#splash:go("https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg?loginUin=0&hostUin=0&singermid=001fNHEf1SFEFN&order=listen&begin=0&num=30&songstatus=1")
#splash:wait(3)
#return{splash:get_cookies()}
#end"""
url='http://192.168.99.100:8050/execute?lua_source='+quote(lua)
response = requests.get(url)
cookie_dict={}
for i in response.json()['1']:
cookie_dict[i['name']] = i['value']
self.guid=cookie_dict['pgv_pvid'] # 获取cookie中的pgv_pvid
self.cookies=cookie_dict
for index in range(1,28):
url=self.start_urls[0]+str(index)+self.start_urls[1]
yield scrapy.Request(url,dont_filter=True,callback=self.get_singer,meta={'index':index})
# 获取每个字母分类下的每页歌手
def get_singer(self,response):
index=response.meta['index']
# 从函数start_requests得出响应内容,获取总页数
pagenum=json.loads(response.body.decode('utf-8'))['singerList']['data']['total']
# 生成列表
page_list=[ i for i in range(1,pagenum+1)]
for page in page_list:
url='https://u.y.qq.com/cgi-bin/musicu.fcg?loginUin=0&hostUin=0&format=jsonp&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0&data=%7B%22comm%22%3A%7B%22ct%22%3A24%2C%22cv%22%3A0%7D%2C%22singerList%22%3A%7B%22module%22%3A%22Music.SingerListServer%22%2C%22method%22%3A%22get_singer_list%22%2C%22param%22%3A%7B%22area%22%3A-100%2C%22sex%22%3A-100%2C%22genre%22%3A-100%2C%22index%22%3A'+str(index)+'%2C%22sin%22%3A'+str((page-1)*80)+'%2C%22cur_page%22%3A'+str(page)+'%7D%7D%7D'
# dont_filter取消重复请求
yield scrapy.Request(url,dont_filter=True,callback=self.get_singer_songs)
# 获取每一个歌手信息
def get_singer_songs(self,response):
# 获取每个字母分类下的每页歌手的全部信息
singer_mid_list=json.loads(response.body.decode('utf-8'))['singerList']['data']['singerlist']
for k in singer_mid_list:
singer_mid=k['singer_mid']
url='https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg?loginUin=0&hostUin=0&singermid='+singer_mid+'&order=listen&begin=0&num=30&songstatus=1'
yield scrapy.Request(url,dont_filter=True,callback=self.get_singer_info,meta={'singer_mid':singer_mid})
# 获取歌手的每一页信息
def get_singer_info(self,response):
# 参数传递获取singermid
singer_mid=response.meta['singer_mid']
# 获取歌手的名字,总页数
song_info=json.loads(response.body.decode('utf-8'))
song_singer =song_info['data']['singer_name']
# 获取歌曲总页数
songcount = song_info['data']['total']
# 根据歌曲总数计算总页数
pagecount = math.ceil(int(songcount / 10))
for i in range(pagecount):
url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg?loginUin=0&hostUin=0&singermid=' + singer_mid + '&order=listen&begin=%s&num=30&songstatus=1' % (
i * 30)
yield scrapy.Request(url,dont_filter=True,callback=self.get_song_info,meta={'song_singer':song_singer})
# 获取每一页的每一首歌曲信息
def get_song_info(self, response):
# 参数传递获取歌手名字
song_singer=response.meta['song_singer']
music_data=json.loads(response.body.decode('utf-8'))['data']['list']
for i in music_data:
songmid=i['musicData']['songmid']
url = 'https://u.y.qq.com/cgi-bin/musicu.fcg?callback=getplaysongvkey7398934035082677&g_tk=1550558782&jsonpCallback=getplaysongvkey7398934035082677&loginUin=0&hostUin=0&format=jsonp&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0&data=%7B%22req%22%3A%7B%22module%22%3A%22CDN.SrfCdnDispatchServer%22%2C%22method%22%3A%22GetCdnDispatch%22%2C%22param%22%3A%7B%22guid%22%3A%222891348329%22%2C%22calltype%22%3A0%2C%22userip%22%3A%22%22%7D%7D%2C%22req_0%22%3A%7B%22module%22%3A%22vkey.GetVkeyServer%22%2C%22method%22%3A%22CgiGetVkey%22%2C%22param%22%3A%7B%22guid%22%3A%22' + self.guid + '%22%2C%22songmid%22%3A%5B%22' + songmid + '%22%5D%2C%22songtype%22%3A%5B0%5D%2C%22uin%22%3A%220%22%2C%22loginflag%22%3A1%2C%22platform%22%3A%2220%22%7D%7D%2C%22comm%22%3A%7B%22uin%22%3A0%2C%22format%22%3A%22json%22%2C%22ct%22%3A24%2C%22cv%22%3A0%7D%7D'
yield scrapy.Request(url,dont_filter=True,callback=self.get_data,meta={'i':i,'song_singer':song_singer},cookies=self.cookies)
# 每一首歌曲信息
def get_data(self,response):
# 参数传递
# song_singer歌手名,i歌曲信息
song_singer=response.meta['song_singer']
i=response.meta['i']
html=re.findall('getplaysongvkey.*?\((.*?)\)',response.body.decode('utf-8'),re.S)[0]
#获取下载歌曲的purl
purl=eval(html)['req_0']['data']['midurlinfo'][0]['purl']
items = MusicItem()
items['song_url']='http://isure.stream.qqmusic.qq.com/'+ purl
items['song_name'] = i['musicData']['songname']
items['song_album'] = i['musicData']['albumname']
items['song_interval'] = i['musicData']['interval']
items['song_mid'] = i['musicData']['songmid']
items['song_singer'] = song_singer
yield items
代码一共定义了6个类方法,每个方法所实现的功能如下:
● start_requests()首先获得start_urls里面的URL信息,然后循环27次,分别得到字母A~Z特殊符号#传入URL,生成不同字母分类的URL地址。最后对27个URL发送GET请求,由于项目需要对同一个URL发送多次请求获取不同的数据,因此dont_filter=True是关闭重复访问URL的设置;参数meta是将start_requests()的数据传递到回调方法get_genre_singer()。此外还使用了Splash实现网站的Cookies获取,由于歌曲下载需要使用Cookies里的信息,因此通过Splash来获取Cookies内容,从而得到歌曲下载的请求参数。
● get_genre_singer()是处理start_requests()对27个URL发送GET请求的响应内容。首先分别获取start_requests()传递的meta参数和当前分类的总页数(来自于start_requests()发送GET请求的响应内容),使用得到的数据构建新的URL,其URL代表当前分类的每一页的歌手信息,最后对新构建的URL发送GET请求,回调方法为get_singer_songs()。
● get_singer_songs()是处理get_genre_singer()发送GET请求的响应内容。其响应内容是获取当前分类的每一页的歌手信息,得到的歌手信息以列表形式表示,通过遍历该列表分别得到每位歌手的singermid,然后构建新的URL,其URL代表当前歌手的主页面。最后对新的URL发送GET请求,获取当前歌手的全部信息,回调方法是get_singer_info(),并传递参数meta,代表当前歌手的singermid。
● get_singer_info()是处理get_singer_songs()发送GET请求的响应内容,从响应内容中获取歌手的信息并计算歌曲的总页数,使用得到的信息构建新的URL,代表当前歌手的每一页歌曲,最后对新构建的URL发送GET请求,回调方法是get_song_info(),参数meta为歌手姓名。
● get_song_info()是从上一请求的响应内容中获取歌曲信息,歌曲信息以列表形式表示,通过遍历该列表分别获取每一首歌的信息,并使用得到的歌曲信息构建新的URL,其URL用于获取下载歌曲的vkey等下载信息,最后对该URL发送GET请求,回调方法为get_data(),参数meta是歌手和歌曲信息。
● get_data()是最后一个回调方法,主要用于获取歌曲的下载链接和歌曲信息。通过处理上一请求的响应内容得到歌曲下载的信息并构建歌曲下载链接,同时将得到的歌曲信息和新构建的下载链接写入Items对象并返回给Item Pipelines。
从整个Spider分析,实现步骤是:全部字母分类歌手列表→当前字母分类歌手列表→当前分类每页的歌手列表→当前分类当前页数的每位歌手→当前歌手的每页歌曲列表→当前歌手的当前歌曲页数的每一首歌曲→获取歌曲信息并下载歌曲。
运行项目之前,还需要开启Splash服务器,在电脑上找到Docker Quickstart Terminal图标并双击运行。成功启动Docker之后,输入Splash启动指令docker run-p 8050:8050 scrapinghub/splash并按回车键即可,如果不会,可参考:Splash简介及安装方法。
2. 本站不保证所提供下载的资源的准确性、安全性和完整性,资源仅供下载学习之用!如有链接无法下载、失效或广告,请联系客服处理!
3. 您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容资源!如用于商业或者非法用途,与本站无关,一切后果请用户自负!