NO.35——qq音樂全站爬蟲(一)
近日,在gitchat訂閱了一篇文章,關於Hyx做的全站爬蟲開發,進行學習和復刻,學習筆記如下:
一、目的
qq音樂提供免費線上試聽,但是下載需要付費,通過開發爬蟲,繞過付費環節,直接下載我們需要的歌曲。
二、方法
爬取物件是web端qq音樂,爬取範圍是全站的歌曲資訊,爬取方式是在歌手列表下獲取每一位歌手的全部歌曲。
由於爬取量過大,採用非同步程式設計的方式實現分散式爬蟲開發,提高爬蟲效率。
整個爬蟲專案按功能分為爬蟲規則和資料入庫。
爬蟲規則:
在歌手列表https://y.qq.com/portal/singer_list.html
5、遍歷每個歌手的每個頁面的歌曲資訊。
4、遍歷每個歌手的歌曲頁數;
3、遍歷每個字母分類下的每個頁面的歌手資訊;
2、遍歷每個字母分類下的歌手總頁數;
1、遍歷26個字母分類的歌手列表;
理論設計上至少需要五次遍歷,實際開發中遍歷次數要多得多。整個開發過程採用模組化設計思想,劃分模組如下:
- 歌曲下載
- 歌手資訊和歌曲資訊
- 字母分類下的歌手列表
- 全部歌手列表
(一)歌曲下載
下載歌曲前,首先要找到某個歌手某個歌曲的下載連結。
在網頁中,點選播放某歌曲,開啟谷歌的開發者模式,在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地址很長,並且有複雜的請求引數,請求引數分為三大型別:
- 整個引數可以直接去掉;
- 引數值固定不變;
- 引數值從其他請求資訊獲取。
複製整個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¬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%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¬ice=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()用於爬取某個歌手的全部歌曲:
- 引數singermid代表歌手的唯一識別符號,只需傳入不同歌手的singermid,就能爬取不同歌手的全部歌曲;
- 程式碼有兩個相同的變數url:第一個動態設定歌手的singermid,獲取每位歌手的歌曲總數和歌手姓名;第二個動態設定頁數,獲取當前歌手每一頁的歌曲資訊;
- 下載歌曲呼叫已實現的download()函式,入庫處理是呼叫入庫函式insert_data(),後續小節會介紹。
(三)分類歌手列表
通過以上小節內容,現在已經可以下載某一歌手的全部歌曲,只要在這功能基礎上遍歷輸入不同歌手的singermid,就能獲取所有不同歌手的全部歌曲資訊。用開發者工具對歌手列表進行分析,發現每頁有80個歌手,共297頁,全站歌手共有23760位。
將迴圈次數按字母分類劃分。在歌手列表頁上使用字母A-Z對歌手進行分類篩選,利用這個分類功能可以將全部歌手分成兩層迴圈。拆分成兩層迴圈主要是為非同步程式設計提供切入點,具體實現方式會在後續小節講解。首先在網頁上單擊分類“A”,在開發者模式下JS選項卡下看到相應請求資訊:
點選不同字母和頁數,發現引數變化規律:
- index表示字母,“A”=1,"B"=2;
- sin根據頁數計算歌手數量,第一頁為0,第二頁為80;
- 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¬ice=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()是獲取單個字母分類的歌手列表,函式引數說明如下:
- index代表字母 對應index
- page_list代表當前字母分類下的總頁數 對應(page-1)*80
- 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¬ice=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()
上述程式碼是整個專案的程式入口,函式執行順序如下:
- get_all_singer():迴圈26個字母,構建引數並呼叫get_genre_singer()
- get_genre_singer(index,page_list,cookie_dict):遍歷當前分類總頁數,獲取每頁每位歌手的歌曲資訊
- get_singer_songs(singermid,cookie_dict):實現歌手的歌曲入庫和下載
- download(guid,songmid,cookie_dict):下載歌曲
- getCookies():使用selenium獲取使用者的cookies
- insert_data(song_dict):入庫處理
通過函式層層呼叫實現整個網站的歌曲下載和資訊入庫。