某寶評論區已經成功爬取了,jd的也是差很少的方法,說實話也沒什麼好玩的,我是看上它們分析簡單,又沒加密纔拿來試手的。若是真的要看些有趣的評論的話,我會選擇網易雲音樂,裏面匯聚了哲學家,小說家,story-teller,皮皮蝦等各類人才,某些評論很是值得收藏(甚至開了一個歌單專門收藏它們)。居然這麼好玩,何不嘗試把他們爬取下來呢?html
因此這個(大規模)網易雲音樂評論爬取project就成型了python
整個過程並不順利,網上找到的解決方案清一色用的是pycrypto模塊(已經沒人維護,且還要裝一個臃腫的VS14才能安裝),很是麻煩。而少數用pycryptodome模塊的也出現了報錯/不可行的結果。最後是看了不少github大佬的源碼,結合網上的思路,才從新寫了出來。在此分享出來,提供一個少走彎路的解決方案git
這個實戰將詳細的展現一次手動分析解決動態爬取,同時還會接觸加密形post請求,更好的解決一些刁難的動態包github
網易雲音樂新版WebAPI分析
網易雲音樂經常使用API淺析
參考代碼算法
可選:fiddler 捉包工具 (官網下載)
可選:瞭解一點AES,RSA加密
任一瀏覽器
pycryptodome模塊 (直接pip安裝)
base64及binascii模塊 (直接導入)
可選是指:若是你要深刻了解如何找到加密方法,就選json
開始以前感謝網易雲給我帶來的音樂和歡樂,記住爬取需適度api
咱們要爬的是歌曲的評論,而歌曲的來源有多種,有的來源於專輯,有的來源於歌單,有的來源於歌手頁;而歌單和專輯的來源又有多種。因此爬取多個歌曲的評論以前,咱們要分析一下信息的結構,最好寫下來,這樣頭腦會更清晰減小代碼修改量。這裏放出一張我本身整理的結構,並選擇一條線路來實現(發現音樂→→歌單→→歌曲→→評論)
瀏覽器
至於上圖所列的其餘信息,讀者能夠過完這個實戰後本身動手實現,可是要注意的是:某些信息是沒法直接經過網頁源碼提取出來的,須要經過加密的動態包(實際上是API)得到,若是有須要的話我可能會出一篇文章總結網易雲音樂的API緩存
每一個歌單都有惟一的id,經過http://music.163.com/playlist... 這個連接就能夠找到歌單,因此第一步咱們要收集發現音樂下的多個歌單id服務器
首先進入官網的「發現音樂」的「歌單」一欄,這裏能夠看到不少高分歌單,先處處點一下,能夠發現連接是在改變的,說明部分數據不是動態加載的,可經過網頁源碼得到。最後發現連接有cat,order,offset,和limit四個對咱們有用的參數,cat是分類,order是排序,offset=(頁數-1)*35,limit=35。還有注意使用前要把連接的井號和一個斜槓去掉,否者會致使網頁源碼缺失。
先隨便找一條連接requests一下先,能夠發現目標信息是完整的,和F12看到的源碼同樣,那歌單id就能夠放心提取了,具體用什麼方法取決於讀者。參考代碼:
def get_playlists(pages,order,cat):#頁數(一頁獲取35個歌單id),排序,分類 playlist_ids = [] for page in range(pages): url = 'http://music.163.com/discover/playlist/?order={}&cat={}&limit=35&offset={}'.format(order,cat,str(page*35)) print(url) r = requests.get(url,headers=headers) playlist_ids.extend(re.findall(r'playlist\?id=(\d+?)" class="msk"',r.text)) return playlist_ids
每一個歌單都有多首歌曲,因此第二步咱們要獲取每一個歌單下的全部歌曲id順便把歌單名也獲取。
歌單連接是http://music.163.com/playlist...,先隨便找一個requests一下先,目標沒缺失可是requests結果是和F12源碼是不一樣的,篩選時請照着requests結果寫(requests結果只有id和歌名,暫時夠用那就這樣吧)
另外一種方法是經過API(http://music.163.com/weAPI/v3...)獲取,包含更全的信息(包括歌手,所屬專輯,歌單介紹等),因涉及加密和js調試較麻煩就不先介紹了(讀者能夠根據本文的加密算法詳解自行調試),之後會寫篇文章介紹各類API。
參考代碼:
def get_songs(playlist_id='778462085'): r = requests.get('http://music.163.com/playlist?id={}'.format(playlist_id),headers=headers) song_ids = re.findall(r'song\?id=(\d+?)".+?</a>',r.text)#歌id列表 song_titles = re.findall(r'song\?id=\d+?">(.+?)</a>',r.text)#歌名列表 list_title = re.search(r'>(.+?) - 歌單 - 網易雲音樂',r.text).group(1)#歌單名 list_url = 'http://music.163.com/playlist?id='+playlist_id #歌單連接 return [song_ids, song_titles, list_title, list_url]#一次性返回這些信息給評論爬取器
進入某首歌http://music.163.com/song?id=...,很天然就想到requests一下,然而這不會獲得任何評論信息,由於評論區是動態加載的(翻頁連接不變,動態標誌),因此打開F12捉包吧,在xhr中查看response很快找到
搗弄事後發現,請求連接中「R_SO_4_」後接的是歌曲的id,同一首歌下不一樣頁數的動態包的請求連接除csrf_token外是相同的。
請求類型爲post,須要兩個參數,不管是刷新仍是評論翻頁這兩個參數都會變,應該是加密過的。
先不理加密先,嘗試把第一頁的兩個參數傳給請求連接是能得到數據的,對應第一頁的評論,嘗試把csrf_token參數去除,仍是能獲取數據,因此csrf_token參數能夠不要。咱們大膽一點,繼續把這對參數傳給不一樣歌曲的請求連接,發現都能獲取對應的第一頁評論;而把第二頁的兩個參數傳給不一樣歌曲的請求連接,就會獲得對應第二頁評論,以此類推。因此得出結論,任一頁數的兩個參數對不一樣歌曲是通用的,第n頁的參數post過去會獲得第n頁的評論。這樣就成功繞過了加密問題。
然而仍是存在缺點的,請看下面對話
A:哈哈哈——這樣就不用理會怎樣加密了!!!
B:只爬前幾頁的話確實是的,可是若是你要爬不少頁或所有爬取怎麼辦,那些10W+評論的歌曲難道你要手動複製粘貼5000+對參數嗎?
C(對着A):你不知道網易雲音樂的API是共用一套加密算法的嗎?若是你想爬評論之外的信息怎麼辦?
因此若是你要大量爬取評論/各類信息時,加密算法就顯得很重要。具體怎樣加密能夠不用瞭解,直接套用就可(代碼在最後),想了解的話繼續往下看。
這裏簡單提供一下獲取評論的參考思路,交給讀者補全
def get_comments(arg): # 接收get_songs方法返回的數據,爬取頁數等 post_urls = [......] # 經過get_songs方法返回的數據構造每首歌的請求連接列表 data = [{}] # 手動寫入或加密算法生成 for i in range(len(post_url)): # 爬每首歌評論 #for j in range(pages): # 若是每首歌要爬多頁,那要再設一個循環 r = requests.post(post_urls[i],data=data,headers=headers) print(r.json()) # 剩下解析json數據並寫入容器。其中json數據可能會有坑,詳看github中的代碼。 ''''''
最終帶加密算法的爬評論代碼:github(代碼笨了,應該一次性生成多組params和encSecKey再索引使用而不是每首歌都加密一次,懶得改了....)(暫時是單線程,比較慢,有時間加個多線程下去)
能夠肯定params和encSecKey這兩個參數是加密過的了,裏面包含着頁數信息,服務器收到參數,解密後根據內容返回信息。一般這種加密都是經過js加密的,因此首先要找到這個有加密算法的js。
經過F12查看包的initiator能夠得知其發起者是core.js,立刻去JS包那裏找。
其內容是巨量堆砌在一塊兒的,丟去排版一下後拷貝到本地文件中,代碼量20000+,先用搜索一下params和encSecKey看看可否定位到加密算法那裏。
結果是可行的,看到這個熟悉的data就知道加密函數是window.asrsea(),接收了4個參數!!!又加大了分析難度,根本不知道這些參數是什麼。這時就要上fiddler了來調試js了,能實現本地js覆蓋原來的js,讓瀏覽器執行本地的js。(使用fiddler前請配置好代理,網上查)
選到autoresponder,把三個選項全勾上,而後按add rule,添加要替換的js,如圖添加rule,第一欄是待替換的js(就是那個core.js包的連接),第二欄是替換物的絕對路徑(就是拷貝回來修改過的js文件的絕對路徑),而後按save
對剛纔找到的代碼塊進行修改,添加5條語句讓它分別輸出四個參數和params,經過比較包和輸出的params肯定成組的4個參數。
注意:①拷貝回來的js必定要趁熱修改趁熱使用,原來的core.js一段時間後會變更(如上面兩幅圖第一個參數中的j3x變成了j5o),因此不要照抄個人,以你拷貝回來的爲準
②若是修改後的js沒在瀏覽器中加載,fiddler也捉不到這個core.js的話,請清空瀏覽器的緩存再嘗試
配置好fiddler修改好js後立刻運行fiddler,而後立刻打開瀏覽器,開啓F12選擇console控制檯監測輸出,打開測試歌曲連接http://music.163.com/#/song?i...,能夠看到有不少組輸出,咱們能夠經過比較評論包的params參數和輸出的params參數找到評論對應的那組參數(以下圖紅色圈着的那組)
咱們能夠看到,不一樣組的第二第三第四個輸出值都是同樣的,因此window.asrsea()除第一個參數是會變外,其他三個參數是定值。研究對象一會兒減到一個。對評論來講,第一個參數是'{rid: "R_SO_4_411907742", offset: "0", total: "true", limit: "20", csrf_token: "f15b016ca1e43812f78a260998917527"}'
,是json object,爲了搞清其變化規律,咱們把評論翻到第二頁看看會變成怎樣。第二頁評論獲得'{rid: "R_SO_4_411907742", offset: "20", total: "false", limit: "20", csrf_token: "f15b016ca1e43812f78a260998917527"}'
......
按多幾頁,多切幾首歌後就會總結出第一個參數的規律,這個object包含了歌曲id,頁數等信息,應該是被加密以前的原始數據。
window.asrsea()接收的第一個參數還通過JSON.stringify()處理,讓其變成了json數據,這個過程咱們能夠用python的json.dumps(dict)實現
#window.asrsea()接收參數 '{......}'#第一參數,那個json數據 '010001'#第二參數 '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'#第三參數 '0CoJUm6Qyw8W8jud'#第四參數
參數是搞明白了,可是如何加密仍是不清楚,因而回到剛纔的js文件中。追蹤window.asrsea()函數,發現它指向一個叫d的函數,仔細研究許久後大概知道加密算法
params經兩次aes加密得到,模式爲CBC,偏移量爲b'0102030405060708'。第一次aes加密的明文是處理後的第一參數(具體處理方法看代碼),密匙爲第四參數;第二次aes加密的明文是第一次加密得到的密文,密匙是那個隨機數,以後得到params,一些具體處理看代碼
encSecKey通過rsa加密,明文和aes第二次加密同一個隨機數,公匙是(第二參數,第三參數)。
參數處理細節處理請看代碼
import json from Crypto.Cipher import AES #新的加密模塊只接受bytes數據,否者報錯,密匙明文什麼的要先轉碼 import base64 import binascii import random secret_key = b'0CoJUm6Qyw8W8jud'#第四參數,aes密匙 pub_key ="010001"#第二參數,rsa公匙組成 modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" #第三參數,rsa公匙組成 #生成隨機長度爲16的字符串的二進制編碼 def random_16(): return bytes(''.join(random.sample('1234567890DeepDarkFantasy',16)),'utf-8') #aes加密 def aes_encrypt(text,key): pad = 16 - len(text)%16 #對長度不是16倍數的字符串進行補全,而後在轉爲bytes數據 try: #若是接到bytes數據(如第一次aes加密獲得的密文)要解碼再進行補全 text = text.decode() except: pass text = text + pad * chr(pad) try: text = text.encode() except: pass encryptor = AES.new(key,AES.MODE_CBC,b'0102030405060708') ciphertext = encryptor.encrypt(text) ciphertext = base64.b64encode(ciphertext)#獲得的密文還要進行base64編碼 return ciphertext #rsa加密 def rsa_encrypt(ran_16,pub_key,modulus): text = ran_16[::-1]#明文處理,反序並hex編碼 rsa = int(binascii.hexlify(text), 16) ** int(pub_key, 16) % int(modulus, 16) return format(rsa, 'x').zfill(256) #返回加密後內容 def encrypt_data(data):#接收第一參數,傳個字典進去 ran_16 = random_16() text = json.dumps(data) params = aes_encrypt(text,secret_key)#兩次aes加密 params = aes_encrypt(params,ran_16) encSecKey = rsa_encrypt(ran_16,pub_key,modulus) return {'params':params.decode(), 'encSecKey':encSecKey }
剛纔分析加密算法進行fiddler調試的時候,有沒有注意到有不少組輸出,本文只選了評論那組進行分析。細心的讀者能夠發現,不一樣組的第一個輸出都是一個json object,內容具備可讀性且輸出的params都能找到對應的xhr包。沒錯,那就是明文,不一樣的明文對應不一樣的API,明文經過加密算法獲得的params和encSecKey由對應API接收,而後返回對應的信息。
API有不少個,除了獲取評論的API外,還有歌詞API,歌單API,專輯API,搜索API,mp3API等等,甚至還有簽到API(這個要登陸先,因此也有登陸API)
API的連接很好找,捉下包就能夠了;因此要利用這些API,重點是找到對應明文的規律,就像上面分析json object同樣,要屢次採樣屢次試驗。在面對數十個API時,無疑是很是耗時的,讀者能夠自行探索。