如何爬網易雲音樂的評論數?

首先表明寫這個代碼純粹是想用來找一下評論數多的歌曲,然後排個序依次聽而已,並沒有任何商業用途的意思。
下面是我的嘗試:
比如爬see you again這首歌的評論數http://music.163.com/#/song?id=30953009
用python requests來實現
設置好了header之後
如果直接用網頁的地址作為url地址,然後進行requests.get()會得原始網頁的代碼,幾乎沒有我們所需要的頁面信息。
參考github上有關網易雲音樂爬蟲代碼,去爬http://music.163.com/api/song/detail/?id=30953009ids=[30953009],會發現可以得到數據,但是偏偏沒有評論數這個數據,也就是說網易雲音樂的api並不提供這個數據。
也嘗試過用urllib,結果得到的數據是str類型的,輸出是亂碼(網頁編碼是unicode的),decode("utf-8")會得到UnicodeDecodeError: "utf8" codec can"t decode byte 0x8b in position 1: invalid start byte。所以不知道怎麼辦了。


說下自己的爬評論分析過程吧(其實是為了爬評論才研究的,但是評論數和評論的數據在一個json中,雖然有點文不對題,但還是希望能對題主有幫助)
首先因為我們點擊下一頁看後面的評論時是只有評論刷新的,所以這裡獲得評論數據是發xhr(XMLHTTPRequest),那麼打開瀏覽器的網路面板,找出所有類型為xhr的數據包,評論數據就在他們之中,挨個看一下可以確認是紅圈內的這個。

然後會發現這個請求是一個post,有兩個參數,params和encSecKey

那麼這兩個參數是怎麼獲得的呢?這模樣當然是經過js加密的,從initiator一欄里可以看到這個請求的「發起人」是core.js,一般這樣的js都是沒法看的,下載下來美化過後發現有兩萬多行,但是沒關係,我們需要的只是部分數據。
在這個js文件中搜索params和encSecKey,可以找到這裡

那麼問題就變成得到這個bua,它是由window.asrsea這個函數得到的,可以看到有4個參數,如果研究每個參數肯定是痛苦的,也沒有必要,可以先把它們輸出來看一下,這時候就需要線上調試js,我選擇了Fiddler,在Fiddler的AutoResponder頁添加Rule,大概長這樣

之後網頁載入使用的core.js文件就是我們本地的這個js文件了,而我們可以修改本地的這個文件來獲得想要的數據
比如對於第一個參數,就是bl變成字元串,我們可以直接輸出bl,同時輸出params參數,然後根據正確的params找到正確的bl

然後就成功找到了bl,如圖(這裡csrf_token本來是有值的,但是你會發現沒有它也沒關係,這裡就省略掉好了)

可以根據不同的歌曲和翻譯頁數多試幾次,可以發現rid就是R_SO_4_加上歌曲的id(其實這個參數也是可以沒有的),offset就是(評論頁數-1) * 20,total在第一頁是true,其餘是false。
按這樣的方式可以得到其餘三個參數
第二個參數:
010001
第三個參數:
00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7
第四個參數:
0CoJUm6Qyw8W8jud
現在我們只要知道函數window.asrsea如何處理的就可以了,定位到這個函數發現它其實是一個叫d的函數

這裡的i研究之後你會發現就是一個長度為16的隨機字元串,既然是隨機的,我就直接讓他等於16個F了。這個encText明顯就是params,encSecKey明顯就是encSecKey。而b函數就是一個AES加密,經過了兩次加密,第一次對d也就是那個json加密,key是第四個參數,第二次對第一次加密結果進行加密,key是i。在b函數中我們可以看到

密鑰偏移量iv是0102030405060708,模式是CBC,那麼就不難寫出對於這個json的加密了。
接下來是第二個參數encSecKey,你會發現在我們這種情境下,這裡傳入c的三個參數i是16個F,e是第二個參數,f是第三個參數,全部是固定的值,那麼無論歌曲id或評論頁數如何變化,這個encSecKey都不隨之發生變化,所以這個encSecKey對我們來說就是個常量,抄一個下來就是可以使用的。至此,我們得到了所有的兩個參數。
代碼如下(AES加密部分參考了 @洛克的代碼)

#coding = utf-8
from Crypto.Cipher import AES
import base64
import requests
import json

headers = {
"Cookie": "appver=1.5.0.75771;",
"Referer": "http://music.163.com/"
}

first_param = "{rid:"", offset:"0", total:"true", limit:"20", csrf_token:""}"
second_param = "010001"
third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
forth_param = "0CoJUm6Qyw8W8jud"

def get_params():
iv = "0102030405060708"
first_key = forth_param
second_key = 16 * "F"
h_encText = AES_encrypt(first_param, first_key, iv)
h_encText = AES_encrypt(h_encText, second_key, iv)
return h_encText

def get_encSecKey():
encSecKey = "257348aecb5e556c066de214e531faadd1c55d814f9be95fd06d6bff9f4c7a41f831f6394d5a3fd2e3881736d94a02ca919d952872e7d0a50ebfa1769a7a62d512f5f1ca21aec60bc3819a9c3ffca5eca9a0dba6d6f7249b06f5965ecfff3695b54e1c28f3f624750ed39e7de08fc8493242e26dbc4484a01c76f739e135637c"
return encSecKey

def AES_encrypt(text, key, iv):
pad = 16 - len(text) % 16
text = text + pad * chr(pad)
encryptor = AES.new(key, AES.MODE_CBC, iv)
encrypt_text = encryptor.encrypt(text)
encrypt_text = base64.b64encode(encrypt_text)
return encrypt_text

def get_json(url, params, encSecKey):
data = {
"params": params,
"encSecKey": encSecKey
}
response = requests.post(url, headers=headers, data=data)
return response.content

if __name__ == "__main__":
url = "http://music.163.com/weapi/v1/resource/comments/R_SO_4_30953009/?csrf_token="
params = get_params();
encSecKey = get_encSecKey();
json_text = get_json(url, params, encSecKey)
json_dict = json.loads(json_text)
print json_dict["total"]
for item in json_dict["comments"]:
print item["content"].encode("gbk", "ignore")


根據我的搜索,用這個帖子(http://kevinsfork.info/2015/07/23/nwmusicboxapi/)算出來的的參數去post這個地址 (http://music.163.com/weapi/v1/resource/comments/R_SO_4_30953009/?csrf_token=) 就應該能得到評論的json數據

代碼大概這樣子

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import requests
import json
import os
import base64
from Crypto.Cipher import AES
from pprint import pprint

def aesEncrypt(text, secKey):
pad = 16 - len(text) % 16
text = text + pad * chr(pad)
encryptor = AES.new(secKey, 2, "0102030405060708")
ciphertext = encryptor.encrypt(text)
ciphertext = base64.b64encode(ciphertext)
return ciphertext

def rsaEncrypt(text, pubKey, modulus):
text = text[::-1]
rs = int(text.encode("hex"), 16)**int(pubKey, 16) % int(modulus, 16)
return format(rs, "x").zfill(256)

def createSecretKey(size):
return ("".join(map(lambda xx: (hex(ord(xx))[2:]), os.urandom(size))))[0:16]

url = "http://music.163.com/weapi/v1/resource/comments/R_SO_4_30953009/?csrf_token="
headers = {
"Cookie": "appver=1.5.0.75771;",
"Referer": "http://music.163.com/"
}
text = {
"username": "郵箱",
"password": "密碼",
"rememberLogin": "true"
}
modulus = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
nonce = "0CoJUm6Qyw8W8jud"
pubKey = "010001"
text = json.dumps(text)
secKey = createSecretKey(16)
encText = aesEncrypt(aesEncrypt(text, nonce), secKey)
encSecKey = rsaEncrypt(secKey, pubKey, modulus)
data = {
"params": encText,
"encSecKey": encSecKey
}

req = requests.post(url, headers=headers, data=data)
pprint(req.json())
for content in req.json()["comments"]:
print content["content"].encode("utf-8")
print
print req.json()["total"]

最後那個數字就是評論總數, 其餘是最新評論.


查詢評論數的WebAPI介面鏈接:

# 如果你通過歌單API獲取裡面的歌曲,每一首歌曲有commentThreadId:R_SO_4_4875911
http://music.163.com/weapi/v1/resource/comments/R_SO_4_歌曲id

對用戶名密碼進行加密是件麻煩事,但可以通過瀏覽器在已登陸狀態下訪問任意歌曲頁面,有些鏈接會提交params和encSecKey參數。這時只需要向代碼傳遞一個歌曲id就可以查詢出歌曲的總評論數和最近十條評論。

一個簡單的例子:

# 畢竟是API,對headers檢驗不是太嚴格
headers = {
"Cookie": "appver=1.5.0.75771",
"Referer": "http://music.163.com",
}
# 為了方便,這裡直接使用AES加密過後的用戶名密碼數據
# 跟截圖不一樣,無所謂了
user_data = {
"params": "vRlMDmFsdQgApSPW3Fuh93jGTi/ZN2hZ2MhdqMB503TZaIWYWujKWM4hAJnKoPdV7vMXi5GZX6iOa1aljfQwxnKsNT+5/uJKuxosmdhdBQxvX/uwXSOVdT+0RFcnSPtv",
"encSecKey": "46fddcef9ca665289ff5a8888aa2d3b0490e94ccffe48332eca2d2a775ee932624afea7e95f321d8565fd9101a8fbc5a9cadbe07daa61a27d18e4eb214ff83ad301255722b154f3c1dd1364570c60e3f003e15515de7c6ede0ca6ca255e8e39788c2f72877f64bc68d29fac51d33103c181cad6b0a297fe13cd55aa67333e3e5"
}
song_id = 420513460
url = "http://music.163.com/weapi/v1/resource/comments/R_SO_4_%s/?csrf_token=" % (song_id,)
r = requests.post(url, headers=headers, data=user_data)
if r.status_code == 200 and r.text.find("comments") != -1:
last_comments = json.loads(r.text)["comments"]
total_comments = json.loads(r.text)["total"]
print(last_comments) # 太長,不貼了
print(total_comments) # 69

下面是用Scrapy+MongoDB爬取得數據(2017/3/2)

{ "total" : 1278759, "id" : 186016, "m_name" : "晴天" }
{ "total" : 664915, "id" : 411214279, "m_name" : "雅俗共賞" }
{ "total" : 234160, "id" : 418603077, "m_name" : "告白氣球" }
{ "total" : 206226, "id" : 436514312, "m_name" : "成都" }
{ "total" : 176112, "id" : 412902689, "m_name" : "初學者" }
{ "total" : 169683, "id" : 32507038, "m_name" : "演員" }
{ "total" : 136540, "id" : 139774, "m_name" : "The truth that you leave" }
{ "total" : 133762, "id" : 417859631, "m_name" : "我好像在哪見過你" }
{ "total" : 131923, "id" : 443277013, "m_name" : "火星人來過" }
{ "total" : 118533, "id" : 31445772, "m_name" : "理想三旬" }
{ "total" : 118393, "id" : 439915614, "m_name" : "剛好遇見你" }
{ "total" : 113379, "id" : 27759600, "m_name" : "Five Hundred Miles" }
{ "total" : 110830, "id" : 412902950, "m_name" : "最佳歌手" }
{ "total" : 108745, "id" : 186001, "m_name" : "七里香" }
{ "total" : 106381, "id" : 2526613, "m_name" : "Booty Music" }
{ "total" : 104987, "id" : 417833348, "m_name" : "超越無限" }
{ "total" : 101300, "id" : 33211676, "m_name" : "Hello" }
{ "total" : 100584, "id" : 32922450, "m_name" : "IF YOU" }
{ "total" : 99476, "id" : 415792881, "m_name" : "剛剛好" }
{ "total" : 98114, "id" : 30953009, "m_name" : "See You Again" }

github地址:GitHub - Lovecanon/mongodb_project


為了找一個人的評論,爬了她聽過的所有歌(這是一個悲傷的故事)

總的來說前人之述備矣,借鑒了 github 上的分析,把request的構成方式給理清,但是因為個人比較傾向於用 java 寫爬蟲,又實在不想搞明白很長很亂的加密演算法,所以直接調用 Java 內置的 ScriptEngine.

首先,把網易雲音樂的core.js給 down 下來,常規手段進行解密和格式化,之後我們發現加密演算法在其中已經寫的很完備了,初步想法是通過 Java 的 ScriptEngine 來運行加密演算法對參數進行加密,就能得到encSecKey和 params

但是 ScriptEngine 中調用 js 引擎,內置對象是有限的,也就是說,原來core.js中的 window 對象在 ScriptEngine 中是沒有的,但是經過分析,我發現它的加密演算法並沒有用到 window 對象,所以可以愉快的進行一波刪除無用代碼操作了.

將無關加密的演算法刪除之後,core.js 已經由先前的24862(格式化之後)行縮減為不到1000行,很好,接下來對代碼進行改造.

我們看到最終的加密演算法的調用只有這麼幾行(後稱 bzl6f),被包裹在一個(function(){})()匿名名稱空間內,外部無法訪問,我們試著將它抽離到全局名稱空間內.觀察到

三次blb2x方法用了魔幻值進行三次調用,觀察函數發現每次的調用與其他可變值無關,所以對原有 js 進行稍微更改,構造在瀏覽器控制台進行輸出.

剩下的就只需要著眼 window.asrsea方法

這裡可以看出其實就是 d 方法,因為要把加密的入口方法 bzl6f 給抽離到全局,又要保證對 abcde(奇怪的方法命名)等方法的可訪問,所以我們把 abcde 們也去掉匿名函數包裹,

注意到我在最後留了一個名為 myFunc 的鉤子,用來在 js 引擎中入參並輸出結果.

public class JSSecret {
private static Invocable inv;
public static final String encText = "encText";
public static final String encSecKey = "encSecKey";

/**
* 從本地載入修改後的 js 文件到 scriptEngine
*/
static {
try {
Path path = Paths.get("core.js");
byte[] bytes = Files.readAllBytes(path);
String js = new String(bytes);
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("JavaScript");
engine.eval(js);
inv = (Invocable) engine;
System.out.println("Init completed");
} catch (Exception e) {
e.printStackTrace();
}
}

public static ScriptObjectMirror get_params(String paras) throws Exception {
ScriptObjectMirror so = (ScriptObjectMirror) inv.invokeFunction("myFunc", paras);
return so;
}

public static HashMap& getDatas(String paras) {
try {構造
ScriptObjectMirror so = (ScriptObjectMirror) inv.invokeFunction("myFunc", paras);
HashMap& datas = new HashMap&<&>();
datas.put("params", so.get(JSSecret.encText).toString());
datas.put("encSecKey", so.get(JSSecret.encSecKey).toString());
return datas;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

實際的業務邏輯便很簡單了,從本地讀取修改過的 core.js 文件,傳入待加密paramaters 代碼,得到一個打包的 datas

Connection.Response
response = Jsoup.connect("http://music.163.com/weapi/user/playlist?csrf_token=")
.userAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:57.0) Gecko/20100101 Firefox/57.0")
.header("Accept", "*/*")
.header("Cache-Control", "no-cache")
.header("Connection", "keep-alive")
.header("Host", "music.163.com")
.header("Accept-Language", "zh-CN,en-US;q=0.7,en;q=0.3")
.header("DNT", "1")
.header("Pragma", "no-cache")
.header("Content-Type", "application/x-www-form-urlencoded")
.data(JSSecret.getDatas(req_str))
.method(Connection.Method.POST)
.ignoreContentType(true)
.timeout(10000)
.execute();

構造參數以及確定介面 url 參考了這裡.

分析我的方法和別人方法的區別在於,沒有考慮很多加密演算法的實現機制,使用 js 引擎直接調用處理後的js 代碼,比較簡潔,無須引入其他依賴包;缺點在於沒有考慮效率問題,不過實測加密速度很快.

詳細代碼已經開源,歡迎指摘代碼倉庫


wenhaoliang/netease-music-spider

爬取網易雲音樂評論,

這裡我上傳了一個github倉庫,

裡面有我的爬取全過程,

代碼有很全的注釋說明,

使用步驟也寫的很詳細,

已經獲得了50個star了,

各位可以去github一看,

記得給個star啊!


只爬評論數的話

GET http://music.163.com/song?id=歌曲ID

提取 id="cnt_comment_count" 下的內容,即為評論數,可以先用 Postman 測試下。


網易雲音樂就不能自己通過評論數來進行一個排序么。


TypeError: can"t concat bytes to str

這個問題解決了

看到高贊 @平胸小仙女 的答案,感覺好棒啊。按照其方法試了一下,因為是編程小白,所以那個Fiddle那個地方真心沒看懂。不過照著做下來依然出現了和很多人一樣的上述錯誤。

這時候只要把原來的解密函數模塊,加上一行就好了啊,如下

def AES_encrypt(text, key, iv):
pad = 16 - len(text) % 16
text = text + pad * chr(pad)
encryptor = AES.new(key, AES.MODE_CBC, iv)
encrypt_text = encryptor.encrypt(text)
encrypt_text = base64.b64encode(encrypt_text)
encrypt_text = str(encrypt_text, encoding="utf-8") #注意一定要加上這一句,沒有這一句則出現錯誤
return encrypt_text

另附上我爬取網易雲音樂的代碼,部分位置只要替換成自己的header和歌曲鏈接就好啦

https://github.com/kunkun1230/Python-/tree/master/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%20%E4%B8%8D%E5%86%8D%E7%8A%B9%E8%B1%AB%20%E8%AF%84%E8%AE%BA%E5%88%86%E6%9E%90


感謝 @平胸小仙女 @洛克 的分析。

心酸的要死,windows下pycrypto一直沒裝好,折騰半天后下了個anaconda,不想換IDE的我把裡面的包copy出來用。

安好之後照著@平胸小仙女的代碼跑了下,python3.6下會報錯,主要原因是加密的時候用的AES_encrypt這個函數返回的是一個bytes類型的數據,但是接受的參數是str類型,所以在第二次加密的時候就會出錯。修改如下:

def AES_encrypt(text, key, iv):
pad = 16 - len(text) % 16
text = text + pad * chr(pad)
encryptor = AES.new(key, AES.MODE_CBC, iv)
encrypt_text = encryptor.encrypt(text)
encrypt_text = str(base64.b64encode(encrypt_text))[2:-1]
return encrypt_text

在Fiddler的AutoResponder頁添加Rule分析的時候卡住了,已經解決這個問題了。如果你也遇見了添加代碼沒效果的問題,下面幾點或許能幫到你:

①:js已經被瀏覽器緩存了,可以新建一個隱私窗口,這樣就不用清除緩存了。

②:貌似新版的firebug不能用console,可以換別的瀏覽器試一試(我就是firebug沒反應,用chrome可以看到)。

③更加詳細的了解,可以百度console ,Fiddler的AutoResponder等關鍵詞


最近剛剛爬取了差不多10W歌曲的熱門評論, paramsencSecKey 如何生成上面的大神都已經分析的差不多了。

說說我的做法。

F12 之後,找到 xhr 這兩個 參數。

Ctrl+c

建個 properties,Ctrl+v

發送請求的時候,讀出來,加進去。


完活~~~~~


網易雲音樂介面最新加密演算法有人知道嗎?


from selenium import webdriver
import re

browser = webdriver.PhantomJS( )
browser.set_window_size(1120, 550)
#song_id
match_music[0] = "428095913"
song_url = r"網易雲音樂 聽見好時光" % match_music[0]
# 打開網頁
browser.get(song_url)
browser.switch_to.frame(browser.find_element_by_xpath("//iframe"))
#保存源代碼
print(browser.page_source,file=open("C:/Users/welwel/Desktop/%s.txt" % match_music[0],"w",encoding="utf-8"))
browser.quit()

上面是源碼把網頁下載下來慢慢分析就可以看到

和原網頁的

一樣了,就是這麼簡單


我想爬客戶端或者PC端的歌單收藏者,這個也是這樣的但是演算法好像不一樣,求大神幫忙,我Google半天發現我是第一個怕收藏者的。。。小菜鳥不會求幫忙,QQ1032026822.可以付費學習


推薦閱讀:

如何開發高級Python爬蟲?
python多線程爬蟲設計?
學習爬蟲應該從哪裡學起?
如何用八爪魚採集器提取新浪微博的數據呢?
如何利用python asyncio編寫非同步爬蟲?

TAG:Python | 爬蟲計算機網路 | 網易雲音樂 |