最近在研究文字挖掘相關的內容,所謂巧婦難為無米之炊,要想進行文字分析,首先得到有文字吧。獲取文字的方式有很多,比如從網上下載現成的文字文件,或者通過第三方提供的API進行獲取資料。但是有的時候我們想要的資料並不能直接獲取,因為並不提供直接的下載渠道或者API供我們獲取資料。那麼這個時候該怎麼辦呢?有一種比較好的辦法是通過網路爬蟲,即編寫計算機程式偽裝成使用者去獲得想要的資料。利用計算機的高效,我們可以輕鬆快速地獲取資料。
那麼該如何寫一個爬蟲呢?有很多種語言都可以寫爬蟲,比如Java,php,python 等,我個人比較喜歡使用python。因為python不僅有著內建的功能強大的網路庫,還有諸多優秀的第三方庫,別人直接造好了輪子,我們直接拿過來用就可以了,這為寫爬蟲帶來了極大的方便。不誇張地說,使用不到10行python程式碼其實就可以寫一個小小的爬蟲,而使用其他的語言可以要多寫很多程式碼,簡潔易懂正是python的巨大的優勢。
好了廢話不多說,進入今天的正題。最近幾年網易雲音樂火了起來,我自己就是網易雲音樂的使用者,用了幾年了。以前用的是QQ音樂和酷狗,通過我自己的親身經歷來看,我覺得網易雲音樂最優特色的就是其精準的歌曲推薦和獨具特色的使用者評論(鄭重宣告!!!這不是軟文,非廣告!!!僅代表個人觀點,非喜勿噴!)。經常一首歌曲下面會有一些被點贊眾多的神評論。加上前些日子網易雲音樂將精選使用者評論搬上了地鐵,網易雲音樂的評論又火了一把。所以我想對網易雲的評論進行分析,發現其中的規律,特別是分析一些熱評具有什麼共同的特點。帶著這個目的,我開始了對網易雲評論的抓取工作。
python內建了兩個網路庫urllib和urllib2,但是這兩個庫使用起來不是特別方便,所以在這裡我們使用一個廣受好評的第三方庫requests。使用requests只用很少的幾行程式碼就可以實現設定代理,模擬登陸等比較複雜的爬蟲工作。如果已經安裝pip的話,直接使用pip install requests 即可安裝。中文文件地址在此http://docs.python-requests.org/zh_CN/latest/user/quickstart.html,大家有什麼問題可以自行參考官方文件,上面會有非常詳細的介紹。至於urllib和urllib2這兩個庫也是比較有用的,以後如果有機會我會再給大家介紹一下。
在正式開始介紹爬蟲之前,首先來說一下爬蟲的基本工作原理,我們知道我們開啟瀏覽器訪問某個網址本質上是向伺服器傳送了一定的請求,伺服器在收到我們的請求之後,會根據我們的請求返回資料,然後通過瀏覽器將這些資料解析好,呈現在我們的面前。如果我們使用程式碼的話,就要跳過瀏覽器的這個步驟,直接向伺服器傳送一定的資料,然後再取回伺服器返回的資料,提取出我們想要的資訊。但是問題是,有的時候伺服器需要對我們傳送的請求進行校驗,如果它認為我們的請求是非法的,就會不返回資料,或者返回錯誤的資料。所以為了避免發生這種情況,我們有的時候需要把程式偽裝成一個正常的使用者,以便順利得到伺服器的迴應。如何偽裝呢?這就要看使用者通過瀏覽器訪問一個網頁與我們通過程式訪問一個網頁之間的區別。通常來說,我們通過瀏覽器訪問一個網頁,除了傳送訪問的url之外,還會給服務傳送額外的資訊,比如headers(頭部資訊)等,這就相當於是請求的身份證明,伺服器看到了這些資料,就會知道我們是通過正常的瀏覽器訪問的,就會乖乖地返回資料給我們了。所以我們程式就得像瀏覽器一樣,在傳送請求的時候,帶上這些標誌著我們身份的資訊,這樣就能順利拿到資料。有的時候,我們必須在登入狀態下才能得到一些資料,所以我們必須要模擬登入。本質上來說,通過瀏覽器登入就是post一些表單資訊給伺服器(包括使用者名稱,密碼等資訊),伺服器校驗之後我們就可以順利登入了,利用程式也是一樣,瀏覽器post什麼資料,我們原樣傳送就可以了。關於模擬登入,我後面會專門介紹一下。當然事情有的時候也不會這麼順利,因為有些網站設定了反爬措施,比如如果訪問過快,有時候會被封ip(典型的比如豆瓣)。這個時候我們還得要設定代理伺服器,即變更我們的ip地址,如果一個ip被封了,就換另外一個ip,具體怎麼做,這些話題以後慢慢再說。
最後,再介紹一個我認為在寫爬蟲過程中非常有用的一個小技巧。如果你在使用火狐瀏覽器或者chrome的話,也許你會注意到有一個叫作開發者工具(chrome)或者web控制檯(firefox)的地方。這個工具非常有用,因為利用它,我們可以清楚地看到在訪問一個網站的過程中,瀏覽器到底傳送了什麼資訊,伺服器究竟返回了什麼資訊,這些資訊是我們寫爬蟲的關鍵所在。下面你就會看到它的巨大用處。
————————————————————–正式開始的分割線————————————————————————
首先開啟網易雲音樂的網頁版,隨便選擇一首歌曲開啟它的網頁,這裡我以周杰倫的《晴天》為例。如下圖1
圖1
接下來開啟web控制檯(chrom的話開啟開發者工具,如果是其他瀏覽器應該也是類似),如下圖2
圖2
然後這個時候我們需要點選網路,清除所有的資訊,然後點選重新傳送(相當於是重新整理瀏覽器),這樣我們就可以直觀看到瀏覽器傳送了什麼資訊以及伺服器迴應了什麼資訊。如下圖3
圖3
重新整理之後得到的資料如下圖4所示:
圖4
可以看到瀏覽器傳送了非常多的資訊,那麼哪一個才是我們想要的呢?這裡我們可以通過狀態碼做一個初步的判斷,status code(狀態碼)標誌了伺服器請求的狀態,這裡狀態碼為200即表示請求正常,而304則表示不正常(狀態碼種類非常多,如果要想詳細瞭解可以自行搜尋,這裡不說304具體的含義了)。所以我們一般只用看狀態碼為200的請求就可以了,還有就是,我們可以通過右邊欄的預覽來粗略觀察伺服器返回了什麼資訊(或者檢視響應)。如下圖5所示:
圖5
通過這兩種方法結合一般我們就可以快速找到我們想要分析的請求。注意圖5中的請求網址一欄即是我們想要請求的網址,請求的方法有兩種:get和post,還有一個需要重點關注的就是請求頭,裡面包含了user-Agent(客戶端資訊),refrence(從何處跳轉過來)等多種資訊,一般無論是get還是post方法我們都會把頭部資訊帶上。頭部資訊如下圖6所示:
圖6
另外還需要注意的是:get請求一般就直接把請求的引數以?parameter1=value1¶meter2=value2 等這樣的形式傳送了,所以不需要帶上額外的請求引數,而post請求則一般需要帶上額外的引數,而不直接把引數放在url當中,所以有的時候我們還需要關注引數這一欄。經過仔細尋找,我們終於找到原來與評論相關的請求在http://music.163.com/weapi/v1/resource/comments/R_SO_4_186016?csrf_token= 這個請求當中,如下圖7所示:
圖7
點開這個請求,我們發現它是一個post請求,請求的引數有兩個,一個是params,還有一個是encSecKey,這兩個引數的值非常的長,感覺應該像是加密過的。如下圖8所示:
圖8
伺服器返回的和評論相關的資料為json格式的,裡面含有非常豐富的資訊(比如有關評論者的資訊,評論日期,點贊數,評論內容等等),如下圖9所示:(其實hotComments為熱門評論,comments為評論陣列)
圖9
至此,我們已經確定了方向了,即只需要確定params和encSecKey這兩個引數值即可,這個問題困擾了我一下午,我弄了很久也沒有搞清楚這兩個引數的加密方式,但是我發現了一個規律,http://music.163.com/weapi/v1/resource/comments/R_SO_4_186016?csrf_token= 中 R_SO_4_後面的數字就是這首歌的id值,而對於不同的歌曲的param和encSecKey值,如果把一首歌比如A的這兩個引數值傳給B這首歌,那麼對於相同的頁數,這種引數是通用的,即A的第一頁的兩個引數值傳給其他任何一首歌的兩個引數,都可以獲得相應歌曲的第一頁的評論,對於第二頁,第三頁等也是類似。但是遺憾的是,不同的頁數引數是不同的,這種辦法只能抓取有限的幾頁(當然抓取評論總數和熱門評論已經足夠了),如果要想抓取全部資料,就必須搞明白這兩個引數值的加密方式。以為沒有搞明白,昨天晚上我帶著這個問題去知乎搜尋了一下,居然真的被我找到了答案。在這個問題https://www.zhihu.com/question/36081767 下,@平胸小仙女 這位知友詳細說明了如何破解這兩個引數的加密過程,我研究了一下,發現還是有點小複雜的,按照他寫的方法,我改動了一下,就成功得到了全部的評論。這裡要對@平胸小仙女(個人主頁 https://www.zhihu.com/people/luocaodan/answers)表示感謝。如果大家對於破解引數加密有興趣的話,可以去直接參考原帖,地址是:https://www.zhihu.com/question/36081767。
到此為止,如何抓取網易雲音樂的評論全部資料就全部講完了。按照慣例,最後上程式碼,親測有效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
#!/usr/bin/env python2.7 # -*- coding: utf-8 -*- # <a href="http://www.jobbole.com/members/dai745726163">@Time</a> : 2017/3/28 8:46 # @Author : Lyrichu # <a href="http://www.jobbole.com/members/Email">@Email</a> : 919987476@qq.com # @File : NetCloud_spider3.py ''' @Description: 網易雲音樂評論爬蟲,可以完整爬取整個評論 部分參考了@平胸小仙女的文章(地址:https://www.zhihu.com/question/36081767) post加密部分也給出了,可以參考原帖: 作者:平胸小仙女 連結:https://www.zhihu.com/question/36081767/answer/140287795 來源:知乎 ''' from Crypto.Cipher import AES import base64 import requests import json import codecs import time # 頭部資訊 headers = { 'Host':"music.163.com", 'Accept-Language':"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3", 'Accept-Encoding':"gzip, deflate", 'Content-Type':"application/x-www-form-urlencoded", 'Cookie':"_ntes_nnid=754361b04b121e078dee797cdb30e0fd,1486026808627; _ntes_nuid=754361b04b121e078dee797cdb30e0fd; JSESSIONID-WYYY=yfqt9ofhY%5CIYNkXW71TqY5OtSZyjE%2FoswGgtl4dMv3Oa7%5CQ50T%2FVaee%2FMSsCifHE0TGtRMYhSPpr20i%5CRO%2BO%2B9pbbJnrUvGzkibhNqw3Tlgn%5Coil%2FrW7zFZZWSA3K9gD77MPSVH6fnv5hIT8ms70MNB3CxK5r3ecj3tFMlWFbFOZmGw%5C%3A1490677541180; _iuqxldmzr_=32; vjuids=c8ca7976.15a029d006a.0.51373751e63af8; vjlast=1486102528.1490172479.21; __gads=ID=a9eed5e3cae4d252:T=1486102537:S=ALNI_Mb5XX2vlkjsiU5cIy91-ToUDoFxIw; vinfo_n_f_l_n3=411a2def7f75a62e.1.1.1486349441669.1486349607905.1490173828142; P_INFO=m15527594439@163.com|1489375076|1|study|00&99|null&null&null#hub&420100#10#0#0|155439&1|study_client|15527594439@163.com; NTES_CMT_USER_INFO=84794134%7Cm155****4439%7Chttps%3A%2F%2Fsimg.ws.126.net%2Fe%2Fimg5.cache.netease.com%2Ftie%2Fimages%2Fyun%2Fphoto_default_62.png.39x39.100.jpg%7Cfalse%7CbTE1NTI3NTk0NDM5QDE2My5jb20%3D; usertrack=c+5+hljHgU0T1FDmA66MAg==; Province=027; City=027; _ga=GA1.2.1549851014.1489469781; __utma=94650624.1549851014.1489469781.1490664577.1490672820.8; __utmc=94650624; __utmz=94650624.1490661822.6.2.utmcsr=baidu|utmccn=(organic)|utmcmd=organic; playerid=81568911; __utmb=94650624.23.10.1490672820", 'Connection':"keep-alive", 'Referer':'http://music.163.com/' } # 設定代理伺服器 proxies= { 'http:':'http://121.232.146.184', 'https:':'https://144.255.48.197' } # offset的取值為:(評論頁數-1)*20,total第一頁為true,其餘頁為false # first_param = '{rid:"", offset:"0", total:"true", limit:"20", csrf_token:""}' # 第一個引數 second_param = "010001" # 第二個引數 # 第三個引數 third_param = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7" # 第四個引數 forth_param = "0CoJUm6Qyw8W8jud" # 獲取引數 def get_params(page): # page為傳入頁數 iv = "0102030405060708" first_key = forth_param second_key = 16 * 'F' if(page == 1): # 如果為第一頁 first_param = '{rid:"", offset:"0", total:"true", limit:"20", csrf_token:""}' h_encText = AES_encrypt(first_param, first_key, iv) else: offset = str((page-1)*20) first_param = '{rid:"", offset:"%s", total:"%s", limit:"20", csrf_token:""}' %(offset,'false') h_encText = AES_encrypt(first_param, first_key, iv) h_encText = AES_encrypt(h_encText, second_key, iv) return h_encText # 獲取 encSecKey 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 # 獲得評論json資料 def get_json(url, params, encSecKey): data = { "params": params, "encSecKey": encSecKey } response = requests.post(url, headers=headers, data=data,proxies = proxies) return response.content # 抓取熱門評論,返回熱評列表 def get_hot_comments(url): hot_comments_list = [] hot_comments_list.append(u"使用者ID 使用者暱稱 使用者頭像地址 評論時間 點贊總數 評論內容\n") params = get_params(1) # 第一頁 encSecKey = get_encSecKey() json_text = get_json(url,params,encSecKey) json_dict = json.loads(json_text) hot_comments = json_dict['hotComments'] # 熱門評論 print("共有%d條熱門評論!" % len(hot_comments)) for item in hot_comments: comment = item['content'] # 評論內容 likedCount = item['likedCount'] # 點贊總數 comment_time = item['time'] # 評論時間(時間戳) userID = item['user']['userID'] # 評論者id nickname = item['user']['nickname'] # 暱稱 avatarUrl = item['user']['avatarUrl'] # 頭像地址 comment_info = userID + " " + nickname + " " + avatarUrl + " " + comment_time + " " + likedCount + " " + comment + u"\n" hot_comments_list.append(comment_info) return hot_comments_list # 抓取某一首歌的全部評論 def get_all_comments(url): all_comments_list = [] # 存放所有評論 all_comments_list.append(u"使用者ID 使用者暱稱 使用者頭像地址 評論時間 點贊總數 評論內容\n") # 頭部資訊 params = get_params(1) encSecKey = get_encSecKey() json_text = get_json(url,params,encSecKey) json_dict = json.loads(json_text) comments_num = int(json_dict['total']) if(comments_num % 20 == 0): page = comments_num / 20 else: page = int(comments_num / 20) + 1 print("共有%d頁評論!" % page) for i in range(page): # 逐頁抓取 params = get_params(i+1) encSecKey = get_encSecKey() json_text = get_json(url,params,encSecKey) json_dict = json.loads(json_text) if i == 0: print("共有%d條評論!" % comments_num) # 全部評論總數 for item in json_dict['comments']: comment = item['content'] # 評論內容 likedCount = item['likedCount'] # 點贊總數 comment_time = item['time'] # 評論時間(時間戳) userID = item['user']['userId'] # 評論者id nickname = item['user']['nickname'] # 暱稱 avatarUrl = item['user']['avatarUrl'] # 頭像地址 comment_info = unicode(userID) + u" " + nickname + u" " + avatarUrl + u" " + unicode(comment_time) + u" " + unicode(likedCount) + u" " + comment + u"\n" all_comments_list.append(comment_info) print("第%d頁抓取完畢!" % (i+1)) return all_comments_list # 將評論寫入文字檔案 def save_to_file(list,filename): with codecs.open(filename,'a',encoding='utf-8') as f: f.writelines(list) print("寫入檔案成功!") if __name__ == "__main__": start_time = time.time() # 開始時間 url = "http://music.163.com/weapi/v1/resource/comments/R_SO_4_186016/?csrf_token=" filename = u"晴天.txt" all_comments_list = get_all_comments(url) save_to_file(all_comments_list,filename) end_time = time.time() #結束時間 print("程式耗時%f秒." % (end_time - start_time)) |
我利用上述程式碼跑了一下,抓了兩首周杰倫的熱門歌曲《晴天》(有130多萬評論)和《告白氣球》(有20多萬評論),前者跑了大概有20多分鐘,後者有6600多秒(也就是將近2個小時),截圖如下:
注意我是按照空格來分隔的,每一行分別有使用者ID 使用者暱稱 使用者頭像地址 評論時間 點贊總數 評論內容 這些內容。我將這兩個txt檔案上傳到百度雲了,對資料感興趣的同學可以直接下載進行文字分析,地址為:《晴天》(http://pan.baidu.com/s/1boBglfp),《告白氣球》(http://pan.baidu.com/s/1o8O7k1s),或者自己跑一下程式碼抓取一下也是可以的(注意不要開太多執行緒給網易雲的伺服器太大壓力哦~~中間有一段時間伺服器返回資料特別慢,不知道是不是限制訪問了,後來又好了)。我後面也許會自己去對評論資料進行視覺化分析,敬請期待!