Python爬蟲逆向分析某云音樂加密參數(shù)的實例分析
本文轉(zhuǎn)自:https://blog.csdn.net/qq_42730750/article/details/108415551
前言
各大音樂平臺是從何時開始收費的這個問題沒有追溯過,印象中酷狗在16年就已經(jīng)開始收費了,貌似當(dāng)時的收費標準是付費音樂下載一首2元,會員一月8元,可以下載300首。雖然下載收費,但是還可以正常聽歌。陸陸續(xù)續(xù),各平臺不僅收費,而且還更在乎版權(quán)問題,因為缺少版權(quán),酷狗上以前收藏的音樂也不能聽了,更過分的是,有些歌非VIP會員只能試聽60秒(•́へ•́╬)。
版權(quán)問題重視起來當(dāng)然是好事,但只是閑暇時來聽聽音樂放松一下自己的我來說,不會因為想聽音樂而開通各個音樂平臺的VIP的┗( ▔, ▔ )┛,所以漸漸就有了些想法:能不能將這些音樂整合起來,比如我去酷狗音樂聽某一首歌,發(fā)現(xiàn)沒有版權(quán)或只能試聽,能不能自動去網(wǎng)易云音樂搜索下載到本地(干脆直接下載到酷狗對應(yīng)的文件夾里),如果還沒有就去QQ音樂、蝦米音樂、百度音樂等等。
本篇就是在這樣的背景下,通過對網(wǎng)易云音樂進行逆向分析,進而用代碼的方式來*********(此處自己體會哦( ̄︶ ̄)↗)。
目標:
通過輸入歌名或者歌手名,列出相應(yīng)的音樂信息,然后通過選擇某一項,將對應(yīng)的音樂下載到本地指定目錄。
工具:
Google Chrome、PyCharm
這里以我最喜歡的歌手本兮
為例,通過搜索網(wǎng)易云的Web端和PC端發(fā)現(xiàn),Web端不支持下載,PC端需要RMB才能下載(不愧是我兮的歌(✪ω✪)),咳咳咳,OK,F(xiàn)ine,意料之中。
1. 請求分析
如果想要下載一首歌,我們首先要獲取到這首歌所對應(yīng)的 u r l url url。隨機選擇一首歌進行播放,打開Chrome的開發(fā)者工具,刷新看一下對應(yīng)的請求,找到我們想要的歌曲文件的 u r l url url,就是下面這個:
然后找到該請求對應(yīng)的 u r l url url,分析一下該請求:
可知,獲取數(shù)據(jù)的 u r l url url 為https://music.xxx.com/weapi/song/enhance/player/url/v1?csrf_token=
,請求方式為POST
。繼續(xù)往下滑,找到提交的數(shù)據(jù):
POST
提交了兩個參數(shù)params
和encSecKey
,很明顯這兩個參數(shù)都經(jīng)過了加密處理,而且經(jīng)過不斷提交刷新發(fā)現(xiàn),這兩個參數(shù)值會變,可以猜測到加密時應(yīng)該是有隨機操作,但其長度始終不變,即參數(shù)params
的長度為152
,參數(shù)encSecKey
的長度為256
。
需要的 u r l url url 及請求所需要的參數(shù)已經(jīng)找到,下面需要確定一下兩個參數(shù)是如何加密的。
2. 參數(shù)分析
通過Ctrl + Shift + F全局搜索參數(shù)encSecKey
定位到了兩個文件,然后在core_7a734ef25ee51b62727eb55c7f6eb1e8.js
這個文件里通過Ctrl + F定位到了接口函數(shù):
摘取這部分函數(shù)分析一下:
var bVZ8R = window.asrsea(JSON.stringify(i0x), bqN0x(["流淚", "強"]), bqN0x(Wx5C.md), bqN0x(["愛心", "女孩", "驚恐", "大笑"])); e0x.data = j0x.cs1x({ params: bVZ8R.encText, encSecKey: bVZ8R.encSecKey })
函數(shù)window.asrsea()
應(yīng)該就是加密函數(shù),傳入四個參數(shù),將加密后的結(jié)果賦值給變量bVZ8R
,返回的結(jié)果有兩個屬性,即encText
和encSecKey
,也就是我們想要的參數(shù)params
和encSecKey
。在這里設(shè)置一個斷點,看一下這幾個參數(shù):
通過最右邊的變量查看區(qū)Watch
可以看到變量bVZ8R
的值就是我們需要的參數(shù)的值,這證實了函數(shù)window.asrsea()
就是加密函數(shù),然后我們在控制臺Console
打印一下這幾個變量:
>JSON.stringify(i0x) <"{"csrf_token":""}" >bqN0x(["流淚", "強"]) <"010001" >bqN0x(Wx5C.md) <"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" >bqN0x(["愛心", "女孩", "驚恐", "大笑"]) <"0CoJUm6Qyw8W8jud"
即加密函數(shù)window.asrsea()
所需的四個參數(shù)值已經(jīng)確定,分別是字符串"{"csrf_token":""}"
、"010001"
、"00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
、"0CoJUm6Qyw8W8jud"
,如果沒有猜錯的話第三個參數(shù)是十六進制的形式,其實也就是如此。通過幾次刷新,這幾個值不變。
3. 加密分析
百度搜索發(fā)現(xiàn)函數(shù)window.asrsea()
不是JavaScript的原生函數(shù),應(yīng)該是開發(fā)者自己定義的,然后我通過搜索asrsea
定位到了該函數(shù)的初始定義位置:
函數(shù)window.asrsea()
就是函數(shù)d
,它就是我們要找的加密函數(shù),它接收的d、e、f、g
四個參數(shù)對應(yīng)的就是window.asrsea()
函數(shù)的四個參數(shù),即
d = "{\"csrf_token\":\"\"}" e = "010001" f = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" g = "0CoJUm6Qyw8W8jud"
或許已經(jīng)發(fā)現(xiàn)了吧,這里面的函數(shù)名、變量名及參數(shù)都是一個字母,而且它們有的還相同,沒錯,這是一種很常見的反爬蟲手段------JS代碼混淆。
摘取這部分加密函數(shù)分析一下:
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 }
函數(shù)a
的作用是從字符串"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
中隨機生成長度為a
的字符串。
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() }
函數(shù)b
的作用是對數(shù)據(jù)a
進行AES加密,模式為CBC
,最后通過toString()
方法將結(jié)果轉(zhuǎn)成字符串。
function c(a, b, c) { var d, e; return setMaxDigits(131), d = new RSAKeyPair(b,"",c), e = encryptedString(d, a) }
函數(shù)c
的作用是對數(shù)據(jù)a
進行RSA加密,返回的結(jié)果是十六進制形式的字符串。
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 }
函數(shù)d
的作用是對數(shù)據(jù)d
進行加密,得到兩個加密的結(jié)果encText
和encSecKey
,加密流程是通過函數(shù)a
隨機產(chǎn)生一個長度為16
的字符串,然后通過函數(shù)b
進行第一次AES加密,然后再通過函數(shù)b
對第一次的加密結(jié)果進行一次AES加密,得到結(jié)果encText
,即對應(yīng)我們的params
,最后通過函數(shù)c
進行一次RSA加密,得到結(jié)果encSecKey
。
4. 模擬加密
這里使用一個非常強大的加密算法庫-----PyCryptodome,具體使用方法請參考官方文檔。
這里定義了一個EncryptText
類,專門用來模擬JavaScript的加密過程:
class EncryptText: def __init__(self): self.character = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' self.iv = '0102030405060708' self.public_key = '010001' self.modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b' \ '5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417' \ '629ec4ee341f56135fccf695280104e0312ecbda92557c93' \ '870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b' \ '424d813cfe4875d3e82047b97ddef52741d546b8e289dc69' \ '35b3ece0462db0a22b8e7' self.nonce = '0CoJUm6Qyw8W8jud'
在函數(shù)d
中打上斷點,來分析看一下a
、b
、c
三個函數(shù)返回的結(jié)果,方便比對我們模擬的結(jié)果:
程序執(zhí)行到函數(shù)a
處,在最右邊變量作用域區(qū)Scope
可以看到各個變量的值及函數(shù)a
返回的的結(jié)果i: "mEXyqHtNW5dxT5IK"
。
這里先模擬函數(shù)a
來隨機產(chǎn)生長度為16
的字符串,首先使用的是官方提供的API:Crypto.Random.get_random_bytes(N)
,返回長度為N的隨機字節(jié)串。
def create16RandomBytes(self): """ # 產(chǎn)生16位隨機字符, 對應(yīng)函數(shù)a :return: """ generated_string = get_random_bytes(16) return generated_string
我們需要將該字節(jié)串通過decode()
方法轉(zhuǎn)換成字符串,但是隨機產(chǎn)生的字節(jié)串是這樣的:b'\xe0\xda\xf9\x8fd\xb4M\xaa\xa7\x1fW\xaay\x12\x90@'
,在轉(zhuǎn)換字符串時就會產(chǎn)生UnicodeDecodeError
,所以這里就自己寫了一個方法:
def create16RandomBytes(self): """ # 產(chǎn)生16位隨機字符, 對應(yīng)函數(shù)a :return: """ generate_string = random.sample(self.character, 16) generated_string = ''.join(generate_string) return generated_string
該方法產(chǎn)生的結(jié)果就是16
位隨機的字符串:
程序執(zhí)行到函數(shù)b
處,傳入的參數(shù)d
和g
的值我們已經(jīng)知道,看一下加密后的結(jié)果:
加密后的結(jié)果為encText: "eHhjXckqrtZkqcwCalCMx0QuU6Lj9L7Wxouw1iMCnB4="
,下面來用官方的API來模擬一下:
def AESEncrypt(self, clear_text, key): """ AES加密, 對應(yīng)函數(shù)b :param clear_text: 需要加密的數(shù)據(jù) :return: """ # 數(shù)據(jù)填充 clear_text = pad(data_to_pad=clear_text.encode(), block_size=AES.block_size) key = key.encode() iv = self.iv.encode() aes = AES.new(key=key, mode=AES.MODE_CBC, iv=iv) cipher_text = aes.encrypt(plaintext=clear_text) # 字節(jié)串轉(zhuǎn)為字符串 cipher_texts = base64.b64encode(cipher_text).decode() return cipher_texts
我們將需要加密的數(shù)據(jù)"{"csrf_token":""}"
傳入到該函數(shù)中,看一下模擬的結(jié)果:
很nice,結(jié)果一模一樣,然后再進行一次AES加密,因為第二次加密用到了函數(shù)a
產(chǎn)生的16位隨機字符,為了結(jié)果一致,這里也使用相同的隨機字符進行模擬。先看一下原始的結(jié)果:
第二次AES加密產(chǎn)生的結(jié)果為encText: "JWuA4mdNsTdrLdDkD9UWs8ShPCZNK0n4BLpdQEDSAaD/kFKKih8XQp8W/mICYPlN"
,然后對比一下自己模擬的結(jié)果:
哈哈哈哈(⁎˃ᴗ˂⁎)也是OK的,結(jié)果一樣。
AES具體的加密原理這里不做過多的介紹,感興趣的話可以參考相關(guān)的書籍或自行百度,這里只介紹一些基本概念。
高級加密標準 ( A d v a n c e d (Advanced (Advanced E n c r y p t i o n Encryption Encryption S t a n d a r d , A E S ) Standard,AES) Standard,AES)是一種分組密碼算法,又稱 R i j n d a e l Rijndael Rijndael算法,是對稱密鑰加密中最流行的算法之一。AES的分組長度固定為128位,密鑰長度則可以是128、192或256位。
密碼分組鏈模式,即CBC,是分組密碼工作模式之一,它需要一個初始向量 ( I n i t i a l i z a t i o n (Initialization (Initialization V e c t o r , I V ) Vector,IV) Vector,IV)組進行異或運算,而且CBC模式要求數(shù)據(jù)長度必須是密碼分組長度的整數(shù)倍。因此數(shù)據(jù)長度不夠的話需要進行填充。
最后就是RSA加密了,看一下函數(shù)c
返回的結(jié)果:
很長的一串,長度為256:encSecKey: "d58e873a2e908c0599b497456f1842d1734e1d17e834a221ed84d828b06b149d0bac2ddd449e38b7e5e9ce53dcb1aa43a241742a2b273434b67825743fbca6371aa143a4460477704ba3fd33b517619386daf8da4c7fe8d67a604ea0e461aedee5ae2698400a6c7340ab250c97622aa221d871b7352d81ea09262978facf5480"
下面來模擬一下,我首先使用的是官方的API:Crypto.PublicKey.RSA
產(chǎn)生密鑰對,然后使用Crypto.Cipher.PKCS1_OAEP
進行加密,加密后的數(shù)據(jù)長度是256
位,通過它進行請求 u r l url url 時請求狀態(tài)碼是200
,但請求的內(nèi)容為空,由于RSA每次加密得到數(shù)據(jù)都不一樣,所以目前我還沒有好的想法來確定問題出在哪里。
def RSAEncrypt(self, session_key): """ RSA加密的結(jié)果每次都不一樣 :param session_key: :return: """ # n和e構(gòu)成公鑰 # (n, e) # key = RSA.RsaKey(n=int(self.modulus, 16), e=int(self.public_key, 16)) key = RSA.construct(rsa_components=(int(self.modulus, 16), int(self.public_key, 16))) public_key = key.publickey() rsa = PKCS1_OAEP.new(key=public_key) cipher_text = rsa.encrypt(message=session_key).hex() return cipher_text
根據(jù)RSA加密原理,我就自己寫了一個函數(shù)來模擬RSA加密的過程:
def RSAEncrypt(self, i, e, n): """ RSA加密, 對應(yīng)函數(shù)c :param i: :return: """ # num = pow(x, y) % z # 加密C=M^e mod n num = pow(int(i[::-1].encode().hex(), 16), int(e, 16), int(n, 16)) result = format(num, 'x') return result
沒錯,也是一模一樣的(^_^)Y Ya!!
RSA是由美國麻省理工學(xué)院的三名密碼學(xué)者 R i v e s t Rivest Rivest、 S h a m i r Shamir Shamir和 A d l e m a n Adleman Adleman提出的一種基于大合數(shù)因式分解困難性的公開密鑰密碼,簡稱RSA密碼。RSA算法基于一個十分簡單的數(shù)論事實,即將兩個大素數(shù)相乘很容易,但想要對其乘積進行因式分解卻極其困難,因此可以將乘積公開作為加密密鑰。由于這次只用到了加密過程,所以RSA的解密過程不做過多的涉及。
加密運算: C = M e C=M^e C=Me m o d mod mod n n n,其中 C C C是加密后的數(shù)據(jù), M M M是被加密的數(shù)據(jù), e e e是隨機的一個整數(shù), 1 < e < ϕ ( n ) 1<e<\phi (n) 1<e<ϕ(n), ϕ ( n ) \phi (n) ϕ(n)是一個數(shù)論函數(shù),稱為歐拉函數(shù),表示在比 n n n小的正整數(shù)中與 n n n互素的數(shù)的個數(shù), n n n是兩個大素數(shù)的乘積, e e e和 n n n是公開的,它們構(gòu)成了用戶的公鑰。
整個加密流程我們模擬完了,結(jié)果也是正確的,但是,這里還存在一個問題,我們模擬出來的encText
,也就是參數(shù)params
長度不夠。這里可以確定的是加密算法是沒有錯誤的,傳入的參數(shù)中d、e、f、g
后面三個值是固定的,所以問題就基本鎖定了:參數(shù)d
的值不對。
我繼續(xù)debug,然后發(fā)現(xiàn)了一些端倪:函數(shù)d
又接收到了新的參數(shù)d
,它的值是這樣的:
將它進行兩次AES加密后encText
的數(shù)據(jù)長度達到了128
,說明這個還不是正確的,而且Network
面板并沒有出現(xiàn)我們想要的v1?csrf_token=
,然后繼續(xù)debug,最終得到了參數(shù)d
真正的值:d: "{"ids":"[35440198]","level":"standard","encodeType":"aac","csrf_token":""}"
,最后我們看一下最終的結(jié)果:
使用模擬加密獲取到的兩個參數(shù)再次發(fā)起請求,便可以得到我們想要的數(shù)據(jù):
歌曲的文件對應(yīng)的 u r l url url 我們已經(jīng)找到,根據(jù)結(jié)果可知,它是一個字符串,準確來說是個json
格式的,而且里面只有一條數(shù)據(jù)是我們需要的,所以直接提?。?/p>
然后再去用代碼請求該 u r l url url,將請求到的內(nèi)容以二進制形式進行保存,文件名后綴為.mp3
。
5. 獲取ID
上面實現(xiàn)的只是一首歌的下載,如果要實現(xiàn)我們的要求,還需要再修改一些參數(shù)d
,有兩個參數(shù)需要注意,即ids
和level
,一個是歌曲的id
,另一個應(yīng)該是歌曲的質(zhì)量(有標準、無損等,我猜的),這里只關(guān)注一個,那就是歌曲的id
。很容易猜到,一首歌對應(yīng)一個id
,我們選擇哪首歌,就會得到哪首歌的id
,那在哪選擇呢???毫無疑問,肯定是在搜索結(jié)果中選擇的。
正常情況下,我們輸入歌手名,會搜索出來許多歌手的音樂,就像下面這樣:
我們通過代碼直接訪問https://music.xxx.com/#/search/m/?s=本兮&type=1
并不會得到我們想要的信息,該 u r l url url 請求得到的是網(wǎng)站的源代碼,不包含數(shù)據(jù)在里面,很明顯是通過 J a v a S c r i p t JavaScript JavaScript 動態(tài)獲得的,所以我們要找到請求數(shù)據(jù)的 u r l url url。打開Chrome的開發(fā)者工具,刷新看一下對應(yīng)的請求,找到我們想要的數(shù)據(jù),就是下面這個:
然后找到對應(yīng)的 u r l url url,分析一下該請求:
可知,獲取數(shù)據(jù)的 u r l url url 為https://music.xxx.com/weapi/cloudsearch/get/web?csrf_token=
,請求方式為依舊是POST
。繼續(xù)往下滑,找到提交的數(shù)據(jù):
POST
提交了兩個參數(shù)params
和encSecKey
,和我們獲取歌曲 u r l url url 時一樣,但參數(shù)params
的長度變?yōu)榱?code>280,參數(shù)encSecKey
的長度依舊不變,為256
。由此可以確定,又是參數(shù)d
發(fā)生了變化。經(jīng)過幾次debug,最終確定了參數(shù)d
的值:d = "{"hlpretag":"<span class=\"s-fc7\">","hlposttag":"</span>","s":"本兮","type":"1","offset":"0","total":"true","limit":"30","csrf_token":""}"
結(jié)果也是一樣的:
使用模擬加密獲取到的兩個參數(shù)再次發(fā)起請求,發(fā)現(xiàn)得到的結(jié)果是空的,然后改了一下,將字典轉(zhuǎn)為json
格式,AES二次加密后參數(shù)params
長度變?yōu)榱?code>300,然而卻得到了數(shù)據(jù)。和我們在開發(fā)者模式下看到的結(jié)果一樣,里面包含歌曲名、歌曲的id以及歌手名等信息。
從Network更容易看到json
里面的數(shù)據(jù)結(jié)構(gòu):
提取到的結(jié)果如下,分別是歌手名、歌曲名、歌曲id、時長、專輯名、專輯圖片的url:
這里簡單分析一下參數(shù)d
,關(guān)鍵字s
表示你要搜索的內(nèi)容,關(guān)鍵字type
表示搜索的類型(見下面的表格),如果需要下載其他歌手的歌曲,只需要將參數(shù)d
中的關(guān)鍵字s
的值改一下即可,為了方便,可以用input()
方法傳遞這個值。
type | 含義 |
---|---|
1 | 單曲 |
100 | 歌手 |
10 | 專輯 |
1014 | 視頻 |
1006 | 歌詞 |
1000 | 歌單 |
1009 | 主播電臺 |
1002 | 用戶 |
6. 代碼框架
# -*- coding: utf-8 -*- # @Time : 2020/9/2 11:23 # @Author : XiaYouRan # @Email : youran.xia@foxmail.com # @File : wangyiyun_music2.py # @Software: PyCharm import requests from Crypto.Cipher import AES, PKCS1_OAEP from Crypto.Util.Padding import pad from Crypto.PublicKey import RSA from Crypto.Random import get_random_bytes import random import base64 import json import os class EncryptText: def __init__(self): self.character = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' self.iv = '0102030405060708' self.public_key = '010001' self.modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b' \ '5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417' \ '629ec4ee341f56135fccf695280104e0312ecbda92557c93' \ '870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b' \ '424d813cfe4875d3e82047b97ddef52741d546b8e289dc69' \ '35b3ece0462db0a22b8e7' self.nonce = '0CoJUm6Qyw8W8jud' def create16RandomBytes(self): def AESEncrypt(self, clear_text, key): def RSAEncrypt(self, i, e, n): def resultEncrypt(self, input_text): """ 對應(yīng)函數(shù)d :param input_text: :return: """ i = self.create16RandomBytes() encText = self.AESEncrypt(input_text, self.nonce) encText = self.AESEncrypt(encText, i) encSecKey = self.RSAEncrypt(i, self.public_key, self.modulus) from_data = { 'params': encText, 'encSecKey': encSecKey } return from_data class WangYiYunMusic(object): def __init__(self): self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'} def get_html(self, url, method='GET', from_data=None): try: if method == 'GET': response = requests.get(url, headers=self.headers) else: response = requests.post(url, from_data, headers=self.headers) response.raise_for_status() response.encoding = 'utf-8' return response.text except Exception as err: print(err) return '請求異常' def parse_text(self, text): ids_list = json.loads(text)['result']['songs'] count = 0 info_list = [] print('{:*^80}'.format('搜索結(jié)果如下')) print('{0:{5}<5}{1:{5}<20}{2:{5}<10}{3:{5}<10}{4:{5}<20}'.format('序號', '歌名', '歌手', '時長(s)', '專輯', chr(12288))) print('{:-^84}'.format('-')) for id_info in ids_list: song_name = id_info['name'] id = id_info['id'] time = id_info['dt'] // 1000 album_name = id_info['al']['name'] picture_url = id_info['al']['picUrl'] singer = id_info['ar'][0]['name'] info_list.append([id, song_name, singer]) print('{0:{5}<5}{1:{5}<20}{2:{5}<10}{3:{5}<10}{4:{5}<20}'.format(count, song_name, singer, time, album_name, chr(12288))) count += 1 if count == 8: # 為了測試方便, 這里只顯示了9條數(shù)據(jù) break print('{:*^80}'.format('*')) return info_list def save_file(self, song_text, download_info): filepath = './download' if not os.path.exists(filepath): os.mkdir(filepath) filename = download_info[1] + '-' + download_info[2] music_url = json.loads(song_text)['data'][0]['url'] response = requests.get(music_url, headers=self.headers) with open(os.path.join(filepath, filename) + '.mp3', 'wb') as f: f.write(response.content) print("下載完畢!") if __name__ == '__main__': id_url = 'https://music.163.com/weapi/cloudsearch/get/web?csrf_token=' song_url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=' id_d = { "hlpretag": "<span class=\"s-fc7\">", "hlposttag": "</span>", "s": input("請輸入歌名或歌手: "), "type": "1", "offset": "0", "total": "true", "limit": "30", "csrf_token": "" } encrypt = EncryptText() id_from_data = encrypt.resultEncrypt(str(id_d)) wyy = WangYiYunMusic() id_text = wyy.get_html(id_url, method='POST', from_data=id_from_data) info_list = wyy.parse_text(id_text) while True: input_index = eval(input("請輸入要下載歌曲的序號(-1退出): ")) if input_index == -1: break download_info = info_list[input_index] song_d = { "ids": str([download_info[0]]), "level": "standard", "encodeType": "aac", "csrf_token": "" } song_from_data = encrypt.resultEncrypt(str(song_d)) song_text = wyy.get_html(song_url, method='POST', from_data=song_from_data) wyy.save_file(song_text, download_info)
測試結(jié)果如下,等有時間了再做一個GUI٩(๑>◡<๑)۶ :
結(jié)束語
最后,加一個彩蛋吧,這個代碼不僅可以download,還可以搜集用戶的評論、歌曲對應(yīng)的歌詞等信息,只需要改一下參數(shù)d
和請求的 u r l url url 即可。這里給出這些參數(shù):
功能 | 參數(shù)d dd | u r l urlurl |
---|---|---|
搜索信息 | “{“hlpretag”:”<span class=“s-fc7”>",“hlposttag”:"",“s”:"你要搜索的信息",“type”:"1",“offset”:“0”,“total”:“true”,“l(fā)imit”:“30”,“csrf_token”:""}" | https://music.xxx.com/weapi/cloudsearch/get/web?csrf_token= |
下載音樂 | “{“ids”:”[歌曲id]",“l(fā)evel”:"standard",“encodeType”:“aac”,“csrf_token”:""}" | https://music.xxx.com/weapi/song/enhance/player/url/v1?csrf_token= |
下載歌詞 | “{“id”:”歌曲id",“l(fā)v”:-1,“tv”:-1,“csrf_token”:""}" | https://music.xxx.com/weapi/song/lyric?csrf_token= |
搜集用戶評論 | “{“rid”:“R_SO_4_歌曲id”,“threadId”:“R_SO_4_歌曲id”,“pageNo”:“1”,“pageSize”:“20”,“cursor”:”-1",“offset”:“0”,“orderType”:“1”,“csrf_token”:""}" | https://music.xxx.com/weapi/comment/resource/comments/get?csrf_token= |
這些參數(shù)并不是一成不變的,如果網(wǎng)站更新了這些參數(shù),那就需要重新做分析了。
開源代碼倉庫
如果喜歡的話記得給我的GitHub倉庫點個Star哦!ヾ(≧∇≦*)ヾ
到此這篇關(guān)于Python爬蟲逆向分析某云音樂加密參數(shù)的文章就介紹到這了,更多相關(guān)Python爬蟲逆向加密參數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
設(shè)計模式中的原型模式在Python程序中的應(yīng)用示例
這篇文章主要介紹了設(shè)計模式中的原型模式在Python程序中的應(yīng)用示例,文中主要強調(diào)了對淺拷貝和深拷貝在對象復(fù)制時的使用,需要的朋友可以參考下2016-03-03Python設(shè)計模式編程中解釋器模式的簡單程序示例分享
這篇文章主要介紹了Python設(shè)計模式編程中解釋器模式的簡單程序示例分享,解釋器模式強調(diào)用抽象類來表達程序中將要實現(xiàn)的功能,需要的朋友可以參考下2016-03-03Python自動化之UnitTest框架實戰(zhàn)記錄
這篇文章主要給大家介紹了關(guān)于Python自動化之UnitTest框架實戰(zhàn)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09Python如何根據(jù)關(guān)鍵字逐行提取文本內(nèi)容問題
這篇文章主要介紹了Python如何根據(jù)關(guān)鍵字逐行提取文本內(nèi)容問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08Python遠程linux執(zhí)行命令實現(xiàn)
這篇文章主要介紹了Python遠程linux執(zhí)行命令實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11