前景介紹## 標題 最近小伙伴們聽歌的興趣大漲,網抑雲綜合症已經遍佈各地。 咱們再來抬高一波QQ音樂的熱度吧。 爬它! 目標:歌手列表 任務:將A到Z的歌手以及全部頁數的歌存到本地和資料庫 觀察網頁url結構 當我們進入網頁時發現此時是一個無參數的html網頁載入。 尋找我們想要拿到的位置尋找變化, ...
前景介紹## 標題
最近小伙伴們聽歌的興趣大漲,網抑雲綜合症已經遍佈各地。
咱們再來抬高一波QQ音樂的熱度吧。
爬它!
目標:歌手列表
任務:將A到Z的歌手以及全部頁數的歌存到本地和資料庫
觀察網頁url結構
當我們進入網頁時發現此時是一個無參數的html網頁載入。
尋找我們想要拿到的位置尋找變化,但我們點擊A開頭的網頁跳轉時,發現 url 改變了,index 參數應該是首字母,page 參數應該
是頁數變化。
這樣的話就減少一個找參數的時間啦。
找到XML
還是習慣的點開檢查按鍵,找到首字母的作者提供的XML都需要什麼參數,隨便點點A-Z發現 XML 有一個請求蹦出來,裡面返回
了是個 json 數據集,都點開看看發現找到了每個作者的參數了。成功了一小半!
既然拿到了XML的網站,POST請求是一定的啦,接下來就該分析分析網站所需要的參數都是什麼了,大致猜測一下,這麼多數
據中sign和data參數有點詭異,不像是正常的參數,加密參數也找到了。
破解sign加密參數
search 找一下sign都在哪裡。因為sign應該是個變數,所以說在他後面加個=會查找的更精確一些。找到一個sign參數的位置,這
應該是個JavaScript代碼,那就應該是這裡面了。點進去!
在JavaScript中找到了 sign 的位置,找到了一個JavaScript調用的網站,我們進去看看是什麼代碼在裡面。
當我們進入網站之後,代碼有點亂我們線上格式化一下看看裡面是什麼。
格式化代碼後發現這個裡面含有sign參數。這個應該是sign的加密演算法,但是其中傳了一個參數進入,我們需要瞭解一下他傳了什
麽參數,這樣我們的sign就出來了。
回到我們調用JavaScript網站的文件中,繼續往下看,我們發現 sign 傳數值就在他的下部。我們看到了傳了個data給這個sign加
密函數,點斷點看看data參數是什麼。這data值看的好眼熟,這不是一開始很奇怪那個參數data麽。原來data弄出來了sign就能給
出來的。
我們繼續找剛纔的sign。點擊下一次跳轉我們發現我們找到另一個函數在裡面,產生了一種疑惑,這個函數是做什麼的,他為什
麽會跳向這裡。
那我們先看看我們格式化出來的JavaScript到底能不能運行吧。說不定能運行呢。
哦吼,能運行,那就好辦了,看看他返回的參數到底是啥,在開頭定義一個sign=null,將t返回給sign,再把sign列印出來。這是
剛纔的那個跳轉過去的方法。
原來如此,那我們將sign調用一下加個(),我們就能發現sign的值就出現了。但我們重覆運行發現我們無法得到他的加密值,因為
這些都是一樣的。
回想我們剛纔說的話,他需要加一個data參數才可以獲得加密參數,那好再改一下。
首先我們先下載一個python調用JavaScript的庫。pip install PyExecJS
開始寫python代碼
Python學習交流Q群:906715085### import execjs def get_sign(data): with open('a.js','r',encoding='utf-8') as f: text = f.read() js_data = execjs.compile(text) sign = js_data.call('get_sign',data) return sign if __name__ == "__main__": data = '{"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":2,"sin":0,"cur_page":1}}}' sign = get_sign(data) print(data) print(sign)
這是多次調用代碼的結果,發現data傳入成功了。
獲取歌手個數以及頁數
個數其實一開始我們已經拿到了,只不過那時候沒介紹,仔細的童鞋們應該是看到了總數到底為多少個。我們點開剛纔的返回
json結果就能看到total已經給出來當前的個數了。
現在該分析一下data參數,盲猜一通估計page和index都在data裡面,要不然這個參數傳不上去呀。好的分析一下data到底有啥,
咱拿過來看看。data裡面看到了get_singer_list這個應該是主要的東西。
#這個是 A開頭 第一頁 #{"comm":{"ct":24,"cv":0},"singerList": {"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":1,"sin":0,"cur_page":1}}} #這個是 B開頭 第一頁 #{"comm":{"ct":24,"cv":0},"singerList": {"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":2,"sin":0,"cur_page":1}}} #這個是 B開頭 第二頁 #{"comm":{"ct":24,"cv":0},"singerList": {"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":2,"sin":80,"cur_page":2}}} #這個是 B開頭 第三頁 #{"comm":{"ct":24,"cv":0},"singerList": {"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":2,"sin":160,"cur_page":3}}}
大致我們能分析出來。
•字母的變化在 index 處,也就是A到Z以及後面的# 應該是一共27個在裡面,也就是index從1到27我們需要傳給他。
•頁數的變化在 sin 這裡,第一頁是0,第二頁就是80,第三頁是160,冷靜分析一下應該是從0開始以80為公差的等差數列。這個
八十應該是代表每一頁都含有八十個歌手。
•cur_page應該就是當前頁數的意思。那咱們跟著sin一起改變。
那在這我們拿到了總數,加上每一頁總共能展示多少,因為多出來的個數需要占一頁才可以,我們使用向下取整。
獲取作者名字以及id號
我們根據上述寫出來爬蟲代碼後,就可以成功獲取 json 的返回值了,在裡面我們能看到一個歌手的參數一共有五個,其中
singer_mid 和 singer_name 是我們所需要的。拿到這兩個值後可以進入網站下載當前歌手的歌曲。
尋找歌手的歌曲
我們隨意點進去一個歌手,進去後尋找XML的網站,我在這裡找好了是 getSingerSong 變數。
在這裡能獲取歌手的每首歌的所能拿到的結果。
我們看一下裡面都需要什麼參數,好像和上次的差不多哦。sign已經獲取到了,data是給定的變數。單純的data有點變化,但問
題不大。那說明還是能正常訪問這個XML的。
data中有點變化的位置就是 singerMid ,這個參數我們在剛纔已經獲取到了。直接在裡面引用一下就好了。begin的參數是一個歌
手歌的頁數,num是一頁中包括多少歌曲。其實我們傳參數可以將這個參數改一下的。把num的值改到一個很大的值,我們就可
以不需要改變begin的參數就能拿到所有的歌曲結果。
我們仔細找一下 json 裡面的參數,點擊音樂鏈接進入發現是https://y.qq.com/n/yqq/song/002MQlds19S8qy.html,我們能發現,
在這個歌曲裡面的 mid 參數就是每首歌的格式化位置。
尋找下載歌曲的m4a鏈接
我們點入播放中。尋找裡面的m4a鏈接看看都包含什麼參數,發現存在七個鏈接都是。但我們仔細一看歌曲的大小我們就會發
現,前幾個都是有問題的發包,一首歌怎麼可能只有幾kb呢。毫不猶豫點進去最後一個。
哦吼,這回參數不一樣了哦。那我們在重新分析一下下吧別懶了。
我們先查找一下他給出來的一些包,看看能不能找到一個非加密參數出來。
果然,功夫不負有心人。vkey 就不是個加密參數!
破解參數前先學會"投機取巧"
我不知道剛纔有沒有仔細看這個位置,發現這個也是個很長的字元串,但是他很特殊,特殊到它和m4a的url是一樣的。
為了讓你們看到,我在這裡把這倆寫出來,發現到他們差了什麼。不變的字元串首碼。。
#C400002wiewH40saXM.m4a?guid=9232644380&vkey=A6F8B706468C0ECFE0F8B6E5E8AAD783D5F852ED0CA66692EB1033B209080BE61208609BEBC2EAF66FA86AC887C8C9F03C02A152E2EF4E24&uin=0&fromtag=66 # https://isure.stream.qqmusic.qq.com/C400002wiewH40saXM.m4a?guid=9232644380&vkey=A6F8B706468C0ECFE0F8B6E5E8AAD783D5F852ED0CA66692EB1033B209080BE61208609BEBC2EAF66FA86AC887C8C9F03C02A152E2EF4E24
那我們先看看vkey到底需要什麼參數給進去。其他參數還是都那些,還是差了一個data需要給進去的。咱們分析一下data都需要
給啥吧。
#{"req":{"module":"CDN.SrfCdnDispatchServer","method":"GetCdnDispatch","param":{"guid":"9232644380","calltype":0,"userip":""}},"req_0":{"module":"vkey.GetVkeyServer","method":"CgiGetVkey","param":{"guid":"9232644380","songmid":["002MQlds19S8qy"],"songtype":[0],"uin":"0","loginflag":1,"platform":"20"}},"comm":{"uin":0,"format":"json","ct":24,"cv":0}}
大致分析了一下
•guid是個無用參數。
•songmid 是歌曲的 mid,我們剛纔已經獲取了
•uin 需要加入一個qq號才可以獲取,如果未登陸預設為0
•其他都是定死的參數
m4a文件是一個二進位文件。所以說我們寫代碼一定要寫入二進位文件才可以。
代碼優化
1.因為數據量過大,日常存入資料庫
2.因為數據下載量大,使用多進程爬取。將A-Z及#各開一個進程
3.防止存入資料庫在多線程階段同時占用,上鎖
全部代碼
crawl.py
db.py
Python交流Q群:906715085### #Python3.7 #encoding = utf-8 import execjs,requests,math,os,threading from urllib import parse from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor from db import SQLsession,Song lock = threading.Lock() headers = { 'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36', 'Referer':'https://y.qq.com/portal/singer_list.html', } session = SQLsession() def get_sign(data): with open('./QQ音樂/get_sign.js','r',encoding='utf-8') as f: text = f.read() js_data = execjs.compile(text) sign = js_data.call('get_sign',data) return sign def myProcess(): #把歌手按照首字母分為27類 with ProcessPoolExecutor(max_workers = 2) as p:#創建27個進程 for i in range(1,28): p.submit(get_singer_mid,i)def get_singer_mid(index): #index = 1-----27 #打開歌手列表頁面,找出singerList,找出所有歌手的數目,除於80,構造後續頁面獲取page歌手 #找出mid, 用於歌手詳情頁 data = '{"comm":{"ct":24,"cv":0},"singerList":'\ '{"module":"Music.SingerListServer","method":"get_singer_list","param":'\ '{"area":-100,"sex":-100,"genre":-100,"index":%s,"sin":0,"cur_page":1}}}'%(str(index)) sign = get_sign(data) url = 'https://u.y.qq.com/cgi-bin/musics.fcg?-=getUCGI6720748185279282&g_tk=5381'\ '&sign={}'\ '&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8'\ '¬ice=0&platform=yqq.json&needNewCode=0'\ '&data={}'.format(sign,parse.quote(data)) html = requests.get(url,headers = headers).json() total = html['singerList']['data']['total']#多少個歌手 pages = int(math.floor(int(total)/80))#向下取整 thread_number = pages Thread = ThreadPoolExecutor(max_workers = thread_number) sin = 0 #分頁迭代每一個字母下的所有頁面歌手 for page in range(1,pages+2): data = '{"comm":{"ct":24,"cv":0},"singerList":{"module":"Music.SingerListServer","method":"get_singer_list","param":{"area":-100,"sex":-100,"genre":-100,"index":%s,"sin":%s,"cur_page":%s}}}'%(str(index),str(sin),str(page)) sign = get_sign(data) url = 'https://u.y.qq.com/cgi-bin/musics.fcg?-=getUCGI6720748185279282&g_tk=5381'\ '&sign={}'\ '&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8'\ '¬ice=0&platform=yqq.json&needNewCode=0'\ '&data={}'.format(sign,parse.quote(data)) html = requests.get(url,headers = headers).json() sings = html['singerList']['data']['singerlist'] for sing in sings: singer_name = sing['singer_name'] #獲取歌手名字 mid = sing['singer_mid'] #獲取歌手mid Thread.submit(get_singer_data,mid = mid, singer_name = singer_name,) sin+=80#獲取歌手信息def get_singer_data(mid,singer_name): #獲取歌手mid,進入歌手詳情頁,也就是每一個歌手歌曲所在頁面 #找出歌手的歌曲信息頁 data = '{"comm":{"ct":24,"cv":0},"singerSongList":{"method":"GetSingerSongList","param":'\ '{"order":1,"singerMid":"%s","begin":0,"num":10}'\ ',"module":"musichall.song_list_server"}}'%(str(mid)) sign = get_sign(data) url = 'https://u.y.qq.com/cgi-bin/musics.fcg?-=getSingerSong4707786209273719'\ '&g_tk=5381&sign={}&loginUin=0'\ '&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0'\ '&data={}'.format(sign,parse.quote(data)) html = requests.get(url,headers = headers).json() songs_num = html['singerSongList']['data']['totalNum'] for number in range(0,songs_num,100): data = '{"comm":{"ct":24,"cv":0},"singerSongList":{"method":"GetSingerSongList","param":'\ '{"order":1,"singerMid":"%s","begin":%s,"num":%s}'\ ',"module":"musichall.song_list_server"}}'%(str(mid),str(number),str(songs_num)) sign = get_sign(data) url = 'https://u.y.qq.com/cgi-bin/musics.fcg?-=getSingerSong4707786209273719'\ '&g_tk=5381&sign={}&loginUin=0'\ '&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0'\ '&data={}'.format(sign,parse.quote(data)) html = requests.get(url,headers = headers).json() datas = html['singerSongList']['data']['songList'] for d in datas: sing_name = d['songInfo']['title'] song_mid = d['songInfo']['mid'] try: lock.acquire() session.add(Song(song_name = sing_name, song_singer = singer_name, song_mid = song_mid)) session.commit() lock.release() print('commit') except: session.rollback() print('rollbeak') print('歌手名字:{}\t歌曲名字:{}\t歌曲ID:{}'.format(singer_name,sing_name,song_mid)) download(song_mid,sing_name,singer_name)def download(song_mid,sing_name,singer_name): qq_number = '請在這裡寫你的qq號' try:qq_number = str(int(qq_number)) except:raise 'qq號未填寫' data = '{"req":{"module":"CDN.SrfCdnDispatchServer","method":"GetCdnDispatch"'\ ',"param":{"guid":"4803422090","calltype":0,"userip":""}},'\ '"req_0":{"module":"vkey.GetVkeyServer","method":"CgiGetVkey",'\ '"param":{"guid":"4803422090","songmid":["%s"],"songtype":[0],'\ '"uin":"%s","loginflag":1,"platform":"20"}},"comm":{"uin":%s,"format":"json","ct":24,"cv":0}}'%(str(song_mid),str(qq_number),str(qq_number)) sign = get_sign(data) url = 'https://u.y.qq.com/cgi-bin/musics.fcg?-=getplaysongvkey27494207511290925'\ '&g_tk=1291538537&sign={}&loginUin={}'\ '&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0'\ '&platform=yqq.json&needNewCode=0&data={}'.format(sign,qq_number,parse.quote(data)) html = requests.get(url,headers = headers).json() try: purl = html['req_0']['data']['midurlinfo'][0]['purl'] url = 'http://119.147.228.27/amobile.music.tc.qq.com/{}'.format(purl) html = requests.get(url,headers = headers) html.encoding = 'utf-8' sing_file_name = '{} -- {}'.format(sing_name,singer_name) filename = './QQ音樂/歌曲' if not os.path.exists(filename): os.makedirs(filename) with open('./QQ音樂/歌曲/{}.m4a'.format(sing_file_name),'wb') as f: print('\n正在下載{}歌曲.....\n'.format(sing_file_name)) f.write(html.content) except: print('查詢許可權失敗,或沒有查到對應的歌曲')if __name__ == "__main__": myProcess()
from sqlalchemy import
Column,Integer,String,create_engine from sqlalchemy.orm import sessionmaker,scoped_session from sqlalchemy.ext. declarative import declarative_base #此處沒有使用pymysql的驅動 #請安裝pip install mysql-connector-python #engine中的 mysqlconnector 為 mysql官網驅動engine = create_engine ('mysql+mysqlconnector://root:root@localhost:3306/test?charset=utf8', max_overflow = 500,#超過連接池大小外最多可以創建的鏈接 pool_size = 100,#連接池大小 echo = False,#調試信息展示 )Base = declarative_base()class Song(Base): __ tablename__ = 'song' song_id = Column(Integer,primary_key = True,autoincrement = True) song_name = Column(String(64)) song_ablum = Column(String(64)) song_mid = Column(String(50)) song_singer = Column(String(50))Base.metadata.create_all(engine)DBsession = sessionmaker(bind = engine)SQLsession = scoped_session(DBsession)
get_sign.js
this.window = this; var sign = null; !function(n, t) { "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (n = n || self). getSecuritySign = t() } (this,function() { "use strict"; var n = function() { if ("undefined" != typeof self) return self; if ("undefined" != typeof window) return window; if ("undefined" != typeof global) return global; throw new Error("unable to locate global object") } (); n.__sign_hash_20200305 = function(n) { function l(n, t) { var o = (65535 & n) + (65535 & t); return (n >> 16) + (t >> 16) + (o >> 16) << 16 | 65535 & o } function r(n, t, o, e, u, p) { return l((i = l(l(t, n), l(e, p))) << (r = u) | i >>> 32 - r, o); var i, r } function g(n, t, o, e, u, p, i) { return r(t & o | ~t & e, n, t, u, p, i) } function a(n, t, o, e, u, p, i) { return r(t & e | o & ~e, n, t, u, p, i) } function s(n, t, o, e, u, p, i) { return r(t ^ o ^ e, n, t, u, p, i) } function v(n, t, o, e, u, p, i) { return r(o ^ (t | ~e), n, t, u, p, i) } function t(n) { return function(n) { var t, o = ""; for (t = 0; t < 32 * n.length; t += 8) o += String.fromCharCode(n[t >> 5] >>> t % 32 & 255); return o } (function(n, t) { n[t >> 5] |= 128 << t % 32, n[14 + (t + 64 >>> 9 << 4)] = t; var o, e, u, p, i, r = 1732584193, f = -271733879, h = -1732584194, c = 271733878; for (o = 0; o < n.length; o += 16) r = g(e = r, u = f, p = h, i = c, n[o], 7, -680876936), c = g(c, r, f, h, n[o + 1], 12, -389564586), h = g(h, c, r, f, n[o + 2], 17, 606105819), f = g(f, h, c, r, n[o + 3], 22, -1044525330), r = g(r, f, h, c, n[o + 4], 7, -176418897), c = g(c, r, f, h, n[o + 5], 12, 1200080426), h = g(h, c, r, f, n[o + 6], 17, -1473231341), f = g(f, h, c, r, n[o + 7], 22, -45705983), r = g(r, f, h, c, n[o + 8], 7, 1770035416), c = g(c, r, f, h, n[o + 9], 12, -1958414417), h = g(h, c, r, f, n[o + 10], 17, -42063), f = g(f, h, c, r, n[o + 11], 22, -1990404162), r = g(r, f, h, c, n[o + 12], 7, 1804603682), c = g(c, r, f, h, n[o + 13], 12, -40341101), h = g(h, c, r, f, n[o + 14], 17, -1502002290