1. 程式人生 > >NO.35——qq音樂全站爬蟲(一)

NO.35——qq音樂全站爬蟲(一)

        近日,在gitchat訂閱了一篇文章,關於Hyx做的全站爬蟲開發,進行學習和復刻,學習筆記如下:

一、目的

       qq音樂提供免費線上試聽,但是下載需要付費,通過開發爬蟲,繞過付費環節,直接下載我們需要的歌曲。

二、方法

       爬取物件是web端qq音樂,爬取範圍是全站的歌曲資訊,爬取方式是在歌手列表下獲取每一位歌手的全部歌曲。

由於爬取量過大,採用非同步程式設計的方式實現分散式爬蟲開發,提高爬蟲效率。

      整個爬蟲專案按功能分為爬蟲規則和資料入庫。

      爬蟲規則:

       在歌手列表https://y.qq.com/portal/singer_list.html

按姓氏字母類別對歌手進行分類,遍歷每個分類下的每個歌手頁面,然後獲取每個歌手頁面下的全部歌曲資訊。設計遍歷方案(由內迴圈到外迴圈):

      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資訊,具體程式碼如下:

          

"""
Created on Fri Oct 26 16:34:47 2018

@author: Macx
"""

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 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36'
        }
session=requests.session()

#下載歌曲
def download(guid,songmid,cookie_dict):
    #引數guid來自cookies的pgv_pvid
    #提取JS選項卡下的URL,包含歌曲的各種請求引數
    url='https://u.y.qq.com/cgi-bin/musicu.fcg?callback=getplaysongvkey5554871952050533&'\
        'g_tk=5381&jsonpCallback=getplaysongvkey5554871952050533&loginUin=0&hostUin=0&'\
        'format=jsonp&inCharset=utf8&outCharset=utf-8&notice=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%22'+guid+'%22%2C%22'\
        'calltype%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%22'\
        'guid%22%3A%22'+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%3A20'\
        '%2C%22cv%22%3A0%7D%7D'
    r=session.get(url,headers=headers,cookies=cookie_dict)
    purl=r.json()['req_0']['data']['midrulinfo']['0']['purl']
    #下載歌曲
    if purl:
        url='http://dl.stream.qqmusic.qq.com/%s' %(purl)
        print(url)
        r=requests.get(url,headers=headers)
        f=open('song/'+songmid+'.m4a','wb')
        f.write(r.content)
        f.close
        return True
    else:
        return False

        對於cookies資訊的獲取,需要使用selenium實現,並且進行兩次操作,才能獲得cookies資訊:第一次先訪問qq音樂首頁,第二次訪問歌手頁面,在JS選項卡下的請求中能找到cookie資訊:

       生成cookies資訊還需要將其轉化成字典格式,因此cookies的獲取過程如下:

#guid請求引數來自cookies,這是常見的反爬措施。採用selenium獲取cookies
def getCookies():
    #某個歌手的歌曲資訊,用於獲取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=001'\
    'fNHEf1SFEFN&order=listen&begin=0&num=30&songstatus=1'
    #配置chrome啟動選項
    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(5)
    driver.get(url)
    time.sleep(5)
    #呼叫chromeDriver自帶的方法獲取cookies
    one_cookie=driver.get_cookies()
    driver.quit()
    print(one_cookie)
    #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']
    #獲取歌曲總數
    song_count=r.json()['data']['total']
    #根據歌曲總數計算總頁數
    pagecount=math.ceil(int(song_count)/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--歌名,albumname--專輯,interval--時長,songmid--歌曲id用於下載
        #將歌曲資訊存放到字典song_dict,用於入庫
        song_dict={}
        for i in music_data:
            song_dict['song_name']=i['musicData']['songname']
            song_dict['song_album']=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'],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開始

           根據上述分析,本章的功能程式碼如下:

#獲取當前字母下全部歌曲
def get_genre_singer(index,page_list,cookie_dict):
    #引數guid來自cookies的pgv_pvid
    for page in page_list:
        url='https://u.y.qq.com/cgi-bin/musicu.fcg?callback=getUCGI8089630466192212&'\
            'g_tk=5381&jsonpCallback=getUCGI8089630466192212&loginUin=0&hostUin=0&format=jsonp'\
            '&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0&data=%7B%22'\
            'comm%22%3A%7B%22ct%22%3A24%2C%22cv%22%3A10000%7D%2C%22singerList%22%3A%7B%22module'\
            '%22%3A%22Music.SingerListServer%22%2C%22method%22%3A%22get_singer_list%22%2C%22'\
            'param%22%3A%7B%22area%22%3A-100%2C%22sex%22%3A-100%2C%22genre%22%3A-100%2C%22'\
            'index%22%3A'+str(index)+'%2C%22sin%22%3A'+str((page-1)*80)+'%2C%22cur_page%22%3A'+str(page)+'%7D%7D%7D'
        r=session.get(url)
        #迴圈每一個歌手
        for k in r.json()['singerList']['data']['singerList']:
            singermid=k['singer_mid']
            #傳入不同的singermid來獲取不同歌手的全部歌曲
            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(),具體程式碼如下:

#單程序單執行緒
#獲取全部歌手
def get_all_singer():
    #獲取字母A-Z已經#的所有歌手
    for index in range(1,28):
        #獲取每個字母分類下總歌手頁數
        url='https://u.y.qq.com/cgi-bin/musicu.fcg?callback=getUCGI06463872642677693&'\
            'g_tk=5381&jsonpCallback=getUCGI06463872642677693&loginUin=0&hostUin=0&format=jsonp'\
            '&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq&needNewCode=0&data=%7B%22'\
            'comm%22%3A%7B%22ct%22%3A24%2C%22cv%22%3A10000%7D%2C%22singerList%22%3A%7B%22module'\
            '%22%3A%22Music.SingerListServer%22%2C%22method%22%3A%22get_singer_list%22%2C%22'\
            'param%22%3A%7B%22area%22%3A-100%2C%22sex%22%3A-100%2C%22genre%22%3A-100%2C%22'\
            'index%22%3A'+str(index)+'%2C%22sin%22%3A0%2C%22cur_page%22%3A1%7D%7D%7D'
        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):入庫處理

          通過函式層層呼叫實現整個網站的歌曲下載和資訊入庫。