NO.35——qq音乐全站分布式爬虫(一)

一、目的

       qq音乐提供免费在线试听,但是下载需要付费,通过开发爬虫,绕过付费环节,直接下载我们需要的歌曲。

二、方法

       爬取对象是web端qq音乐,爬取范围是全站的歌曲信息,爬取方式是在歌手列表下获取每一位歌手的全部歌曲。

由于爬取量过大,采用异步编程的方式实现分布式爬虫开发,提高爬虫效率。

      整个爬虫项目按功能分为爬虫规则和数据入库。

      爬虫规则:

       在歌手列表https://y.qq.com/portal/singer_list.html按姓氏字母类别对歌手进行分类,遍历每个分类下的每个歌手页面,然后获取每个歌手页面下的全部歌曲信息。其整体思路:

首先打开包含26个字母的歌手页面,查找请求信息

    复制请求URL打开

      里边包含一个重要参数,singer_mid。然后任意打开某个歌手的页面

          复制请求URL打开

       里边包含一个重要参数songmid 。然后播放某个歌曲,找到它的播放接口,

            复制某个参数,ctrl+f查找,找到歌曲搜索接口

          复制URL打开

        发现sip+purl就是音乐播放接口。而音乐播放接口包含三个主要参数:  C400+songmid   、guid vkey。分三步走:通过歌手列表页面找到singer_mid ,通过单个歌手的详情页找到songmid,通过歌曲搜索接口找到vekyguid来自cookies 

设计遍历方案(由内循环到外循环):

      5、遍历每个歌手的每个页面的歌曲信息。

      4、遍历每个歌手的歌曲页数;

      3、遍历每个字母分类下的每个页面的歌手信息;

      2、遍历每个字母分类下的歌手总页数;   

      1、遍历26个字母分类的歌手列表;

      理论设计上至少需要五次遍历,实际开发中遍历次数要多得多。整个开发过程采用模块化设计思想,划分模块如下:

  1.       歌曲下载
  2.       歌手信息和歌曲信息
  3.       字母分类下的歌手列表
  4.       全部歌手列表

(一)歌曲下载

            下载歌曲前,首先要找到某个歌手某个歌曲的下载链接。

            在网页中,点击播放某歌曲,打开谷歌的开发者模式,在Media选项卡可以找到该歌曲的播放文件,复制该URL在浏览器打开,发现歌曲可以播放:

           分析这个歌曲信息的URL,这是一个GET请求,并附带各种请求参数,如下:

http://dl.stream.qqmusic.qq.com/C400002stZ4548h0kT.m4a?guid=9613835105&vkey=FFE06ED227150F12AC92890FF951088A7B37F68950DD08F8994A633D908621681BC3F74798A3F4F7E30E3B057ECF62EC1AF4A00DAE934E0D&uin=0&fromtag=66

          那么,要实现歌曲的下载,首先要找到歌曲文件的URL请求参数。以vkey为例,复制这个请求参数到其他请求信息的Preview响应内容里查找,结果在JS选项卡下找到该请求参数:

          从上图分析,purl的值是歌曲URL信息的组成部分,再前边只需加上完整的域名就可以得到完整的歌曲文件URL。对于域名的选择,qq音乐提供了五个域名,每个域名都可以获取文件,这是一种集群的管理方式。在req的sip下可以找到具体的五个域名:

       我们继续这个URL请求的地址,这个URL地址很长,并且有复杂的请求参数,请求参数分为三大类型:

  1. 整个参数可以直接去掉;
  2. 参数值固定不变;
  3. 参数值从其他请求信息获取。

      复制整个URL到地址栏进行访问,逐一实验把各参数去掉,观察响应内容是否发生变化 ,    

           

         对于尚不明确的参数guid和songmid,songmid从命名角度看,是歌曲的唯一标识符,每首歌曲的songmid是固定且唯一的。参数guid则来自cookies,这是一种常见的反爬虫机制。我们将歌曲下载定义为函数download,并设置参数guid、songmid和cookie_dict,分别代表请求参数guid、songmid和用户的cookie信息,具体代码如下:

          

import requests, time
import math
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from music_db import *
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# 创建请求头和会话
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
           }
session = requests.session()

# 下载歌曲
def download(guid, songmid, song_name,cookie_dict):
    # 参数guid来自cookies的pgv_pvid
    url = 'https://u.y.qq.com/cgi-bin/musicu.fcg?-=getplaysongvkey11136773093082608&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={"req":{"module":"CDN.SrfCdnDispatchServer","method":"GetCdnDispatch","param":{"guid":"'+guid+'","calltype":0,"userip":""}},"req_0":{"module":"vkey.GetVkeyServer","method":"CgiGetVkey","param":{"guid":"'+guid+'","songmid":["'+songmid+'"],"songtype":[0],"uin":"0","loginflag":1,"platform":"20"}},"comm":{"uin":0,"format":"json","ct":24,"cv":0}}'
    r = session.get(url, headers=headers,cookies=cookie_dict)
    purl = r.json()['req_0']['data']['midurlinfo'][0]['purl']
    # 下载歌曲
    if purl:
        url = 'http://isure.stream.qqmusic.qq.com/%s' %(purl)
        r = requests.get(url, headers=headers)
        f = open('song/' + song_name + '.m4a', 'wb')
        f.write(r.content)
        f.close()
        return True
    else:
        return False

        对于cookies信息的获取,需要使用selenium实现,并且进行两次操作,才能获得cookies信息:第一次先访问qq音乐首页,第二次访问歌手页面,在JS选项卡下的请求中能找到cookie信息:

       生成cookies信息还需要将其转化成字典格式,因此cookies的获取过程如下:

         

# 使用Selenium获取Cookies
# 因为歌曲下载的请求参数guid是来自Cookies,因此要使用Selenium获取Cookies,这是常见的反爬虫措施之一
def getCookies():
    # 某个歌手的歌曲信息,用于获取Cookies,因为不是全部请求地址都有Cookies
    url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg?g_tk=5381&jsonpCallback=MusicJsonCallbacksinger_track&loginUin=0&hostUin=0&format=jsonp&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0&singermid=001fNHEf1SFEFN&order=listen&begin=0&num=30&songstatus=1'
    chrome_options = Options()
    # 设置浏览器参数
    # --headless是不显示浏览器启动以及执行过程
    chrome_options.add_argument('--headless')
    driver = webdriver.Chrome(chrome_options=chrome_options)
    # 访问两个URL,QQ网站才能生成Cookies
    driver.get('https://y.qq.com/')
    time.sleep(1)
    driver.get(url)
    time.sleep(1)
    one_cookie = driver.get_cookies()
    driver.quit()
    # Cookies格式化
    cookie_dict = {}
    for i in one_cookie:
        cookie_dict[i['name']] = i['value']
    return cookie_dict

           现在将download()和getCookie()方法结合使用,就能实现单首歌曲的下载。

if __name__ == '__main__':
    cookie_dict=getCookies()
    download(cookie_dict['pgv_pvid'],'001X0PDf0W4lBq',cookie_dict)

songmid值的获取从下一小节回答。

(二)歌手信息和歌曲信息 

            在上一节中,实现了单首歌曲的下载,调用download()函数,传入不同的参数songmid即可实现下载不同的歌曲。在本节,通过歌手页面获取不同歌曲的songmid值。以邓紫棋为例,打开歌手页面,并在开发者工具下查找歌曲信息,最后在JS选项卡下找到歌曲信息,如下:

     分析图上请求的url,某些参数存在固定规律,比如singermid是每位歌手的唯一标识符;begin是页数,每一页有30个歌曲,第一页为0,第二页为30...其余参数固定不变。

        本小节实现的代码主要针对图上的请求URL进行。首先获取歌手的总歌曲数量,然后根据总歌曲数量来计算页数,最后遍历每一页来获取每首歌曲的信息以及歌曲的songmid进行歌曲下载,代码如下:

# 获取歌手的全部歌曲
def get_singer_songs(singermid, cookie_dict):
    # 获取歌手姓名和歌曲总数
    url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg?loginUin=0&hostUin=0&singermid=%s' \
          '&order=listen&begin=0&num=30&songstatus=1' % (singermid)
    r = session.get(url)
    # 获取歌手姓名
    song_singer = r.json()['data']['singer_name']
    # 获取歌曲总数
    songcount = r.json()['data']['total']
    # 根据歌曲总数计算总页数
    pagecount = math.ceil(int(songcount) / 30)
    # 循环页数,获取每一页歌曲信息
    for p in range(pagecount):
        url = 'https://c.y.qq.com/v8/fcg-bin/fcg_v8_singer_track_cp.fcg?loginUin=0&hostUin=0&singermid=%s' \
              '&order=listen&begin=%s&num=30&songstatus=1' % (singermid, p * 30)
        r = session.get(url)
        # 得到每页的歌曲信息
        music_data = r.json()['data']['list']
        # songname-歌名,ablum-专辑,interval-时长,songmid歌曲id,用于下载音频文件
        # 将歌曲信息存放字典song_dict,用于入库
        song_dict = {}
        for i in music_data:
            song_dict['song_name'] = i['musicData']['songname']
            song_dict['song_ablum'] = i['musicData']['albumname']
            song_dict['song_interval'] = i['musicData']['interval']
            song_dict['song_songmid'] = i['musicData']['songmid']
            song_dict['song_singer'] = song_singer
            # 下载歌曲
            info = download(cookie_dict['pgv_pvid'], song_dict['song_songmid'], song_dict['song_name'], cookie_dict)
            # 入库处理,参数song_dict
            if info:
                insert_data(song_dict)
            # song_dict清空处理
            song_dict = {}

            函数get_singer_songs()用于爬取某个歌手的全部歌曲:

  1. 参数singermid代表歌手的唯一标识符,只需传入不同歌手的singermid,就能爬取不同歌手的全部歌曲;
  2. 代码有两个相同的变量url:第一个动态设置歌手的singermid,获取每位歌手的歌曲总数和歌手姓名;第二个动态设置页数,获取当前歌手每一页的歌曲信息;
  3. 下载歌曲调用已实现的download()函数,入库处理是调用入库函数insert_data(),后续小节会介绍。 

(三)分类歌手列表

              通过以上小节内容,现在已经可以下载某一歌手的全部歌曲,只要在这功能基础上遍历输入不同歌手的singermid,就能获取所有不同歌手的全部歌曲信息。用开发者工具对歌手列表进行分析,发现每页有80个歌手,共297页,全站歌手共有23760位。

          将循环次数按字母分类划分。在歌手列表页上使用字母A-Z对歌手进行分类筛选,利用这个分类功能可以将全部歌手分成两层循环。拆分成两层循环主要是为异步编程提供切入点,具体实现方式会在后续小节讲解。首先在网页上单击分类“A”,在开发者模式下JS选项卡下看到相应请求信息:

           点击不同字母和页数,发现参数变化规律:

  1. index表示字母,“A”=1,"B"=2;
  2. sin根据页数计算歌手数量,第一页为0,第二页为80;
  3. cur_page表示当前页,从1开始

           根据上述分析,本章的功能代码如下:

# 获取当前字母下全部歌手
# 修改了请求地址URL以及数据获取
def get_genre_singer(index, page_list, cookie_dict):
    for page in page_list:
        url = 'https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI771604139451213&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":'+str(index)+',"sin":'+str((page-1)*80)+',"cur_page":'+str(page)+'}}}'
        r = session.get(url)
        # 循环每一个歌手
        for k in r.json()['singerList']['data']['singerlist']:
            singermid = k['singer_mid']
            get_singer_songs(singermid, cookie_dict)

         函数get_genre_singer()是获取单个字母分类的歌手列表,函数参数说明如下:

  1. index代表字母      对应index
  2. page_list代表当前字母分类下的总页数        对应(page-1)*80
  3. cookie_dict代表函数getCookies的返回值,即用户的Cookies信息      对应page

(四)全站歌手列表

         现在得到函数get_genre_singer(),只需传入不同的参数index和page_list即可实现26个英文字母分类的歌手列表。在此基础上遍历26个英文字母即可实现,将这个遍历定义在函数get_all_singer(),具体代码如下:

# 单进程单线程
# 获取全部歌手
# 修改了请求地址URL以及数据获取
def get_all_singer():
    # 获取字母A-Z全部歌手
    cookie_dict = getCookies()
    for index in range(1, 28):
        # 获取每个字母分类下总歌手页数
        url = 'https://u.y.qq.com/cgi-bin/musicu.fcg?-=getUCGI771604139451213&g_tk=5381&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":'+str(index)+',"sin":0,"cur_page":1}}}'
        r = session.get(url, headers=headers)
        total = r.json()['singerList']['data']['total']
        pagecount = math.ceil(int(total) / 80)
        page_list = [x for x in range(1, pagecount+1)]
        # 获取当前字母下全部歌手
        get_genre_singer(index, page_list, cookie_dict)

if __name__ == '__main__':
    
    #执行单线程进程
    get_all_singer()

          上述代码是整个项目的程序入口,函数运行顺序如下:

  1. get_all_singer():循环26个字母,构建参数并调用get_genre_singer()
  2. get_genre_singer(index,page_list,cookie_dict):遍历当前分类总页数,获取每页每位歌手的歌曲信息
  3. get_singer_songs(singermid,cookie_dict):实现歌手的歌曲入库和下载
  4. download(guid,songmid,cookie_dict):下载歌曲
  5. getCookies():使用selenium获取用户的cookies
  6. insert_data(song_dict):入库处理

          通过函数层层调用实现整个网站的歌曲下载和信息入库。

猜你喜欢

转载自blog.csdn.net/ghl1390490928/article/details/83384511