Python爬蟲之js加密破解,抓取網易雲音樂評論生成詞雲

Python_sn發表於2020-10-22

js破解歷程

  • 前言
  • 技能點
  • 介面概況
  • 靜態網頁動態網頁
  • 頁面解析
  • step1: 找引數step2:分析js函式step3:分析引數step4: 校驗step5:轉為python程式碼
  • 編寫爬蟲

很多人學習python,不知道從何學起。
很多人學習python,掌握了基本語法過後,不知道在哪裡尋找案例上手。
很多已經做案例的人,卻不知道如何去學習更加高深的知識。
那麼針對這三類人,我給大家提供一個好的學習平臺,免費領取視訊教程,電子書籍,以及課程的原始碼!??¤
QQ群:623406465

前言


網路爬蟲的大障礙,就是各種加密。這其中包過登入的驗證碼以及加密。js混淆、js引數加密等等。其實以前也就瞭解過js加密。但是沒有深入研究,藉著這次實踐研究了一下網易雲音樂的加密方式。

博主通過網易雲音樂評論加密的例項來做個學習過程的分析和分享。

如果有問題或者不懂的地方可以關注我的微信公眾號(bigsai),聯絡我。

技能點

  • 前端:js知識(比較重要)、谷歌瀏覽器debug、抓包、打斷點除錯能力(必須)。以及js各種加密函式(瞭解).
  • python:基礎的請求requests。Crypto.Cipher加密解密模組。
  • 其他:postman(模擬請求使用),良好的思維能力和分析能力。(加密演算法有些亂),還有一點就是js加密轉python的程式碼實現。

介面概況

靜態網頁

對於一般的url隨著頁面的變化而變化的頁面,網易雲還是有的,你只需要抓取網頁進行分析即可。

動態網頁

但隨著前後端分離的流行,以及資料分離好處明顯。越來越多的資料採用ajax渲染。而網易雲的評論即使如此。
在前後端分離剛火,那時很多網站對藉口並沒有太大的防護措施。就使得很多網站輕鬆獲取結果。至今也有很多這樣的藉口存在,這種網站爬去就是傻瓜式爬取。


然而隨著前段技術的發展,介面也變的越來越棘手。就拿網易雲的評論來說:它的引數就讓人很懵逼。


這一串串數字到底是啥。很多人見到這樣的資料就會選擇放棄。那麼讓我為你解開它什麼的面紗。

頁面解析

step1: 找引數

你可以看的到,它的引數有兩個,一個是params,一個是encSecKey並且都是經過加密的,我們就要分析它的源頭。F12開啟source搜尋encSckey.

'在查詢這個js內部的encSecKey,發現原來在這裡,經過斷點除錯發現這裡就是最終引數的結果。

step2:分析js函式

這個js有4w多行,如何能在4w多行js中找到有用的資訊,然後理清楚這裡的思路呢?


這就需要你的抽象和逆向思維了。來,我們麼開始分析。

 var bYc7V = window.asrsea(JSON.stringify(i3x), bkY2x(["流淚", "強"]), bkY2x(VM8E.md), bkY2x(["愛心", "女孩", "驚恐", "大笑"]));
 e3x.data = k4o.cz4D({
 params: bYc7V.encText,
 encSecKey: bYc7V.encSecKey
 })

上面這段程式碼中就是來源,我們先不管這個JSON.stringify(i3x)這些引數是啥,先搞清楚window.asrsea是什麼。在上面不遠處你會發現:


這個就是d函式才是所有資料,方法的根源,d、e、f、g四個引數就是我們剛剛說的不要管的引數。
從這個函式就是分析:encText是經過兩次b()函式,encSecKey是經過c()函式,執行的一個引數。注意其中i引數來源是a(16).網上看看這些函式。

 function a(a) {
 var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
 for (d = 0; a > d; d = 1)
 e = Math.random() * b.length,
 e = Math.floor(e),
 c = b.charAt(e);
 return c
 }
 function b(a, b) {
 var c = CryptoJS.enc.Utf8.parse(b)
 , d = CryptoJS.enc.Utf8.parse("0102030405060708")
 , e = CryptoJS.enc.Utf8.parse(a)
 , f = CryptoJS.AES.encrypt(e, c, {
 iv: d,
 mode: CryptoJS.mode.CBC
 });
 return f.toString()
 }
 function c(a, b, c) {
 var d, e;
 return setMaxDigits(131),
 d = new RSAKeyPair(b,"",c),
 e = encryptedString(d, a)
 }
 function d(d, e, f, g) {
 var h = {}
 , i = a(16);
 return h.encText = b(d, g),
 h.encText = b(h.encText, i),
 h.encSecKey = c(i, e, f),
 h
 }

可以發現a(16)就是一個隨機生成的數,所以我們不需要管他。而b目前來看是AES的cbc模式加密。那麼這個encText生成的規則我們就很清楚了。兩次AES的cbc加密。其中偏移量為0102030405060708固定不變。兩次的key不同。而函式c就是三個引數進行RSA加密。整個演算法大體流程差不多稍微瞭解。

到這裡先停一下,不要在分析函式了,我們在分析分析資料。

step3:分析引數

再回到var bYc7V = window.asrsea(JSON.stringify(i3x), bkY2x(["流淚", "強"]), bkY2x(VM8E.md), bkY2x(["愛心", "女孩", "驚恐", "大笑"]))這個函式。憑直覺能夠感覺得到有些資料一定跟我們的核心引數無關,最多跟時間戳有關。

查詢bky2x源頭,


再找的話其實沒必要,這類函式你找找。可以複製到vscode溯源找到根源。分析,在這裡就不繁瑣介紹。直接打斷點分析吧!看看他是怎麼執行的。


其實多次抓你會發現後三個引數是固定不變的(非互動型資料)。
然而最想要的是第一個引數


你心心年年的引數原來長這個樣,那麼和預想差不多,僅僅第一個引數和我們的引數有關。offset就是頁面*20,R_SO_4_ songid就是當前這首歌的id.其實到這個時候,你的i和encSecKey可以一起儲存了。因為上面分析說過,這個i是隨機生成,而encSecKey也和我們核心引數無關,但是和i相關,所以要記錄一組。用作ESA加密的引數和post請求的引數。

現在的你是不是很激動,因為真想即將浮出水面。

step4: 校驗

這步驟也是很重要的一環,因為你在它的js中會發現。


網易是否會動手腳呢?下載原始的js進行測試。發現哈哈,結果一致。那麼就不需要更改再仔細檢視那段加密演算法的程式碼了。

架構圖為

step5:轉為python程式碼

需要將AES的cbc模式的程式碼用Python克隆。達到加密的效果,測試一下。發現結果一致nice

編寫爬蟲

下面就開始編寫爬蟲。先用postman測試需要那些引數。


沒問題,編寫爬蟲。根據你喜歡的哥。把id輸進去,生成你愛的詞雲!一首光輝歲月送給大家!

import  requests
import urllib.parse
import base64
from wordcloud import WordCloud
import jieba.analyse
import matplotlib.pyplot as plt
from bs4 import BeautifulSoup
from Crypto.Cipher import AES
header={'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.90 Safari/537.36',
        #'Postman-Token':'4cbfd1e6-63bf-4136-a041-e2678695b419',
        "origin":'https://music.163.com',
        #'referer':'https://music.163.com/song?id=1372035522',
        #'accept-encoding':'gzip,deflate,br',
        'Accept':'*/*',
        'Host':'music.163.com',
        'content-lenth':'472',
        'Cache-Control':'no-cache',
        'content-type': 'application/x-www-form-urlencoded',
        'Connection':'keep-alive',
        #'Cookie':'iuqxldmzr_=32; _ntes_nnid=a6f29f40998c88c693bc910331bd6bea,1558011234325; _ntes_nuid=a6f29f40998c88c693bc910331bd6bea; _ga=GA1.2.2120707788.1559308501; WM_TID=pV2C%2BjTrRwBBAAERUVJojniTwk8%2B8Zta; JSESSIONID-WYYY=nvf%2BggodQRfcT%2BTvBRmANqMrsDeQCxRvqwFsxDr3eJvNNWhGYFhfCXKFkfAfOdbHhpCsMzT39mAeJ7ZamBQZbiwwtnSZD%5CPWRqKxD9t6dGKD3bTVjomjgB39DB07RNIWI32bYKa2H4fg1qQgqI%2FR%2B%2Br%2BZXJvgFg1Vh%2FA2XRj9S4p0EMu%3A1560927288799; WM_NI=DthwcEQf5Ew2NbTIZmSNhSnm%2F8VWsg5RxhkYogvs2luEwZ6m5UhdzbHYPIr654ZBWKV4o22%2BEwb9BvdLS%2BFOmOAEUG%2B8xd8az4CX%2FiAL%2BZkz3syA0onCPkhQwCtL4pkUcjg%3D; WM_NIKE=9ca17ae2e6ffcda170e2e6eed2d650989c9cd1dc4bb6b88eb2c84e979f9aaff773afb6fb83d950bcb19ecce92af0fea7c3b92a88aca898e24f93bafba6f63a8ebe9caad9679192a8b4ed67ede89ab8f26df78eb889ea53adb9ba94b168b79bb9bbb567f78ba885f96a8c87a0aaf13ef7ec96a3d64196eca1d3b12187a9aedac17ea8949dccc545af918fa6d84de9e8b885bb6bbaec8db9ae638394e5bbea72f1adb7a2b365ae9da08ceb5bb59dbcadb77ca98bad8be637e2a3'
        }

def pkcs7padding(text):
    """
    明文使用PKCS7填充
    最終呼叫AES加密方法時,傳入的是一個byte陣列,要求是16的整數倍,因此需要對明文進行處理
    :param text: 待加密內容(明文)
    :return:
    """
    bs = AES.block_size  # 16
    length = len(text)
    bytes_length = len(bytes(text, encoding='utf-8'))
    # tips:utf-8編碼時,英文佔1個byte,而中文佔3個byte
    padding_size = length if(bytes_length == length) else bytes_length
    padding = bs - padding_size % bs
    # tips:chr(padding)看與其它語言的約定,有的會使用'\0'
    padding_text = chr(padding) * padding
    return text + padding_text
def encrypt(key, content):
    """
    AES加密
    key,iv使用同一個
    模式cbc
    填充pkcs7
    :param key: 金鑰
    :param content: 加密內容
    :return:
    """
    key_bytes = bytes(key, encoding='utf-8')
    iv = bytes('0102030405060708', encoding='utf-8')
    cipher = AES.new(key_bytes, AES.MODE_CBC, iv)
    # 處理明文
    content_padding = pkcs7padding(content)
    # 加密
    encrypt_bytes = cipher.encrypt(bytes(content_padding, encoding='utf-8'))
    # 重新編碼
    result = str(base64.b64encode(encrypt_bytes), encoding='utf-8')
    return result
def getcomment(songid,page):
    url="https://music.163.com/weapi/v1/resource/comments/R_SO_4_"+songid+"?csrf_token="
    print(url)
    formdata = {
        "params": "",
        "encSecKey": "c81160c64a08feb6cfed91c1619d5bffd05dd278b685c94a748689edf035ee0436b66aa7019927ce0fedd26aee9a22cdc6743e58a120f9db0126ebb2e61dae3f7ee21088eb747f829bceed9a5bbb9ee7a2eecf1a358feac431acaab17c95b8491a6a955f7c17a02a3e7886390c2cb3b981f4ccbd5163a566d27ace95db073401",
    }

    aes_key = '0CoJUm6Qyw8W8jud'## 不變的
    print('aes_key:' + aes_key)
    # 對英文加密
    source_en = '{"rid":"R_SO_4_'+songid+'","offset":"'+str(page*20)+'","total":"false","limit":"20","csrf_token":""}'

    #offset自己該
    print(source_en)
    encrypt_en = encrypt(aes_key, source_en)#第一次加密
    print(encrypt_en)
    aes_key='3Unu7SzdXGctW1vA'
    encrypt_en = encrypt(aes_key, str(encrypt_en))  # 第二次加密
    print(encrypt_en)
    formdata['params']=encrypt_en
    print(formdata['params'])
    formdata = urllib.parse.urlencode(formdata).encode('utf-8')
    print(formdata)
    req = requests.post(url=url, data=formdata, headers=header)
    return req.json()
if __name__ == '__main__':
    songid='346576'
    page=0
    text=''
    for page in range(10):
        comment=getcomment(songid,page)
        comment=comment['comments']
        for va in comment:
             print (va['content'])
             text+=va['content']
    ags = jieba.analyse.extract_tags(text, topK=50)  # jieba分詞關鍵詞提取,40個
    print(ags)
    text = " ".join(ags)
    backgroud_Image = plt.imread('tt.jpg')  # 如果需要個性化詞雲
    wc = WordCloud(background_color="white",
                   width=1200, height=900,
                   mask=backgroud_Image,  # 設定背景圖片

                   #min_font_size=50,
                   font_path="simhei.ttf",
                   max_font_size=200,  # 設定字型最大值
                   random_state=50,  # 設定有多少種隨機生成狀態,即有多少種配色方案
                   )  # 字型這裡有個坑,一定要設這個引數。否則會顯示一堆小方框wc.font_path="simhei.ttf"   # 黑體
    # wc.font_path="simhei.ttf"
    my_wordcloud = wc.generate(text)
    plt.imshow(my_wordcloud)
    plt.axis("off")
    plt.show()  # 如果展示的話需要一個個點
    file = 'image/' + str("aita") + '.png'
    wc.to_file(file)

 

相關文章