python爬蟲:瞭解JS加密爬取網易雲音樂

持之以恆_liu發表於2021-08-19

python爬蟲:瞭解JS加密爬取網易雲音樂

前言

大家好,我是“持之以恆_liu”,之所以起這個名字,就是希望我自己無論做什麼事,只要一開始選擇了,那麼就要堅持到底,不管結果如何。接下來,就講一講今天的正題了,運用python爬蟲爬取網易雲音樂,之前小編嘗試了爬取QQ音樂酷狗音樂酷我音樂,但是覺得爬取網易雲音樂是最難的一個。為什麼這樣講呢?除了它是post請求之外,就是它的加密了。原本小編早就打算嘗試爬取它了,但是苦於對瀏覽器斷點操作一直不知怎麼做,現在知道了,並且成功實現爬取網易雲音樂。
小編在這裡提醒讀者一下, 文明爬蟲
文明爬蟲
即:1.不要在網址使用者使用高的時段執行本程式,以免影響網址的正常執行;
2.本程式程式碼僅供學習,且莫用於商業活動,一經被相關人員發現,本小編概不負責!希望讀者牢記!

1.瞭解網易雲音樂的加密

小編通過多次嘗試,個人覺得網易雲音樂的加密原理是這樣的。我們需要爬取的那個網址是post,而post請求需要請求引數,網易雲音樂是先將請求引數進行加密,然後再發起請求(防止反爬),這樣看起來的請求資料也就是讀者看不懂的那一大串字串。
在這裡插入圖片描述
既然它加密請求,我們也可以模擬加密操作呀!不過需要知道開始的請求引數和加密演算法,如果這兩樣都知道,那麼其實爬取這個網址也沒有大家想象的那麼難了。
在這裡插入圖片描述

2.找到發起請求的初始引數

那麼怎樣找到初始請求引數呢?小編以下舉例
一開始搜尋一首歌曲
在這裡插入圖片描述
可以發現這個是動態網址,直接根據這個網址根本無法得到這些歌曲名稱和歌曲id哈!按電腦鍵盤的F12鍵或者滑鼠點選右鍵,來到瀏覽器的開發者模式。
在這裡插入圖片描述
來到以上介面,可以發現這個網址為:https://music.163.com/weapi/cloudsearch/get/web?csrf_token= ,這個網址下面就是這些歌曲的名稱和id,由於這是一個post請求,並且請求引數也是進行了加密,怎樣獲得初始請求引數呢?
在這裡插入圖片描述在這裡插入圖片描述
我們可以進行如上操作,首先斷點,然後重新整理,檢視Scope這個下面是否出現小編上面提到的那個網址。沒有出現的話,一直點小編圖上標的那個符號,直到出現那個網址為止。
在這裡插入圖片描述
可以發現,此時這張圖片上有上面的那個網址,但是那個請求引數是加了密的,之後一直點Call Stack下面的內容,直到請求引數出現沒有加密時為止,此時的請求引數就是開始沒有進行加密的那個引數了(小編推測)。
在這裡插入圖片描述
我們把圖上的小編標明的那兩個加了密的引數發起請求一下,
在這裡插入圖片描述
可以得到請求資料了,現在開始請求的引數有了,具體為
{"hlpretag": "<span class="s-fc7">", "hlposttag": "", "s": 歌曲名, "type": "1", "offset": "0",
"total": "true", "limit": "30", "csrf_token": ""}

3.瞭解加密演算法

那麼它的加密演算法是怎樣的呢?
我們在剛才的那個JS程式碼裡面搜尋 params 這個引數
可以發現這裡有請求引數的兩個key值
在這裡插入圖片描述
在這裡插入圖片描述
它們倆的值分別為 bWv0x.encText,bWv0x.encSecKey,

bWv0x = window.asrsea(JSON.stringify(i0x), bsK6E(["流淚", "強"]), bsK6E(XR7K.md), bsK6E(["愛心", "女孩", "驚恐", "大笑"]));
這個window.asrsea()是一個函式,裡面四個是引數,bsK6E()也是一個函式,但是裡面的引數都是固定的。
在這裡插入圖片描述
在這裡插入圖片描述
而bsK6E()就是返回輸入的字串陣列中字串(key)對應的value值,比如bsK6E(["流淚", "強"])="010001"

在這裡插入圖片描述
我們ctrl+F查詢asrsea這個函式的定義,可以發現在如下這裡
在這裡插入圖片描述
在這裡插入圖片描述

在這裡插入圖片描述
通過分析上面的a、b、c、d函式,可以發現a函式其實就是取隨機長度的字串,b函式小編有點看不懂,猜想應該是加密操作吧!c函式小編看不懂是什麼操作,但是小編覺得如果傳進去的三個引數如果是固定的,那麼它的返回值應該也是固定的,而encSecKey的值等於它的返回值,也就是說encSecKey可以是固定的。而i值是隨機生成的,那麼怎樣才能把它取為固定的呢?其實就是取它的一個返回值即可,讓它代表所有。之後看b函式,小編的確看不懂,於是百度了一下。
在這裡插入圖片描述
發現如果要用python程式碼實現同樣的效果也是可以的,但是需要下載一個包,具體在cmd
上輸入 pip install pycryptodome
即可,具體加密演算法讀者可以看看這位博主這篇文章:python—AES加密
小編改了一下,

def to_16(data):
    len1=16-len(data)%16
    data+=chr(len1)*len1

    return data

def encryption(data,key):
    iv = '0102030405060708'
    aes = AES.new(key=key.encode('utf-8'),IV=iv.encode('utf-8'),mode=AES.MODE_CBC)
    data1 = to_16(data)
    bs=aes.encrypt(data1.encode('utf-8'))

    return str(b64encode(bs),'utf-8')

def get_enc(data):
    param4 = '0CoJUm6Qyw8W8jud'
    #enc='NA5SxhePf6dxIxX7'
    #enc='GLvjERPvSFUw6EVQ'
    enc='g4PXsCuqYE6icH3R'
    first=encryption(data,param4)
    return encryption(first,enc)

程式碼裡面enc值就是我上面說的i值,上面講的b函式就是裡面的encryption函式。
這樣我們就可以顯示上述執行同樣的結果了。
在這裡插入圖片描述
這裡小編只需得到一首歌曲的id即可,為後續做準備。在這裡插入圖片描述
可以發現歌曲下載網址就是上面小編標的那個,它是post請求,並且請求引數也是加了密的,同樣我們也需要得到它的原始請求引數值,操作和上述一樣,這裡小編就不一一講述了,反正這個比較麻煩,小編花費很多時間,終於得到了它的初始請求引數為:
{"ids": "[歌曲id]".format(song_id), "level": "standard", "encodeType": "aac", "csrf_token": ""}
加密演算法和上述一樣的。

4.實現爬取程式碼

from Crypto.Cipher import AES
from base64 import b64encode
import requests
import random
import json
import os


def to_16(data):
    len1=16-len(data)%16
    data+=chr(len1)*len1

    return data

def encryption(data,key):
    iv = '0102030405060708'
    aes = AES.new(key=key.encode('utf-8'),IV=iv.encode('utf-8'),mode=AES.MODE_CBC)
    data1 = to_16(data)
    bs=aes.encrypt(data1.encode('utf-8'))

    return str(b64encode(bs),'utf-8')

def get_enc(data):
    param4 = '0CoJUm6Qyw8W8jud'
    #enc='NA5SxhePf6dxIxX7'
    #enc='GLvjERPvSFUw6EVQ'
    enc='g4PXsCuqYE6icH3R'
    first=encryption(data,param4)
    return encryption(first,enc)

if __name__ == '__main__':
    # 得到搜尋歌曲的列表資訊
    song=input('請輸入歌曲名稱:')
    param1 = {"hlpretag": "<span class=\"s-fc7\">", "hlposttag": "</span>", "s": song, "type": "1", "offset": "0",
              "total": "true", "limit": "30", "csrf_token": ""}
    data=json.dumps(param1)
    # 對請求引數的加密
    params=get_enc(data)
    data={
        'params': params,
        #'encSecKey': 'c2bcf219b2d727ff351d8fc4e5cbb86b09c32055345c098b8a8faf9c1c8b2bc506623ffc2b45db3e72cf040c750848f4408147c881a494c99dc8596415ce27d7b8ff7128e41a2b987bc9b78b3f4d4e0f0f5925b9ae24d99d1923a0d0c5cae5a3ebaf83c1097cfc3fd876f77582f38b79bbd03718cc562c15877abe9628e89ff1'
        #'encSecKey':'cd99d0f0c4210c9dfbd2fafec8640dae914f5d359e593338f699d98c0643dcc385a3889c89c98b3dcbe8f389aa91f47608ec236cd204adbd0236aae23125776c294f28d1753b685710e0173349e71715153e76c93a100ad682eab00033d3ebf3b5001a0046994800332cfc43445e59f28f5e874cb1dc04482d57da9cc67f6e8e'
        'encSecKey':'bb20ee9409e57057e4d1b55e4d77c94bff4d8cbf181c467bbd3fa156e3419665c6c1e643621d5d82c128251fb85f0cb34d4f08c88407b4148924ffa818f59a64b3814784e7e3837bad4f6f9690cb2cf721d9ea1af12c16a32a9df00be710b70ee8ed32036cc6a465b28ef43f4382cbcb4595b3121be75ecba9171876b611b8fc'
    }
    url='https://music.163.com/weapi/cloudsearch/get/web?csrf_token='
    headerList=[   # user-agent列表,用於構造隨機取值
        "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko",
        "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3314.0 Safari/537.36 SE 2.X MetaSr 1.0",
        "Mozilla/5.0 (Windows; U; Windows NT 6.1; ) AppleWebKit/534.12 (KHTML, like Gecko) Maxthon/3.0 Safari/534.12",
        "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
        "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
        'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3861.400 QQBrowser/10.7.4313.400'
    ]
    value=random.choice(headerList)
    headers={'user-agent':value}
    response=requests.post(url=url,data=data,headers=headers)
    dict1=json.loads(response.text)
    lists=dict1['result']['songs']
    for i in range(len(lists)):
        print('[{}]-{}-->{}'.format(i+1,lists[i]['name'],lists[i]['ar'][0]['name']))
    id=int(input('請輸入想下載的歌曲序號:(從1開始)'))
    song_id=lists[id-1]['id']
    song_name=lists[id-1]['name']+"_"+lists[id-1]['ar'][0]['name']   # 歌曲名稱

    # 下面程式碼為下載歌曲的程式碼

    url2='https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token='
    param2= {"ids": "[{}]".format(song_id), "level": "standard", "encodeType": "aac", "csrf_token": ""}  # post請求的引數
    data2=json.dumps(param2)
    params=get_enc(data2)
    data['params']=params
    headers['user-agent']=random.choice(headerList)
    response2=requests.post(url=url2,data=data,headers=headers)
    dict2=json.loads(response2.text)
    downloadUrl=dict2['data'][0]['url']
    downloadDir='./網易雲音樂'
    try:   # 自動建立資料夾
        os.mkdir(downloadDir)
    except Exception as e:
        print(e)

    # 下載歌曲程式碼
    response3=requests.get(url=downloadUrl,headers=headers)
    # 以二進位制的形式寫入到檔案中
    with open(file='{}/{}.mp3'.format(downloadDir,song_name),mode='wb') as f:
        f.write(response3.content)

執行結果:
在這裡插入圖片描述
下載完成之後,可以發現在和執行檔案的同一級目錄下面多出一個網易雲音樂的資料夾,下載的音樂就在這個資料夾裡面。
在這裡插入圖片描述

5.總結

小編覺得或許上述有部分言語沒有講的很清晰,希望有問題的讀者可以下面的評論區裡進行評論,當然讀者如果有興趣的話,可以區看看小編的其他文章。

1.python自動化:實現自動回覆QQ訊息
2.運用Java製作一個屬於自己的音樂播放軟體
3.Python爬蟲經常爬不到資料,或許你可以看一下小編的這篇文章
4.selenium模組太強大了,網易雲音樂都可下載 這篇文章是小編運用selenium模組爬取的網易雲音樂哈!
5.Python多執行緒爬蟲教你如何快速下載表情包,告別鬥圖鬥不贏的煩惱!
6.王者榮耀桌布上面的英雄太酷了,為什麼不把它們下載下來呢?

相關文章