1. 程式人生 > >Python3爬蟲抓取《曾經我也想過一了百了》熱評-因為像你這樣的人生於這世上,我稍稍喜歡這個世界了。

Python3爬蟲抓取《曾經我也想過一了百了》熱評-因為像你這樣的人生於這世上,我稍稍喜歡這個世界了。

作為初學者,懷著緊張的心情來分享我的第一個爬蟲小程式。看到很多網易雲音樂熱評的爬蟲,我也來試試。我這次要爬取的是《曾經我也想過一了百了》中島美嘉的這首歌。
在這裡插入圖片描述

首先進行抓包分析

首先用瀏覽器開啟網易雲音樂的網頁版,進入歌曲頁面,可以看到下面有評論。接著 F12 進入開發者控制檯,接下來就要做的是,找到歌曲評論對應的 URL“R_SO_4_26830207?csrf_token=”,並分析驗證其資料跟網頁現實的資料是否吻合。
在這裡插入圖片描述
在Preview的介面,可以看到全部評論是comments,熱門評論是hotComments。在Headers頁面,可以看到請求方式是POST,以及請求所需要的各種引數。發現表單中需要填兩個資料,名稱為 params 和 encSecKey。後面緊跟的是一大串字元,換幾首歌會發現,每首歌的 params 和 encSecKey 都是不一樣的,因此,這兩個資料可能經過一個特定的演算法進行加密過的。
在此感謝大佬解析過網易雲音樂的加密原理,直接拿來用就可以啦。

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

    # 進行aes加密
    def aesEncrypt(self,text, secKey):
        pad = 16 - len(text) % 16
        #print("leix")
        #print(type(text))
        #print(type(pad * chr(pad)))
        if isinstance(text,bytes):
            #print("type(text)=='bytes'")
            text=text.decode('utf-8')
        text = text + str(pad * chr(pad))
        encryptor = AES.new(secKey, AES.MODE_CBC, '0102030405060708')

        ciphertext = encryptor.encrypt(text)
        ciphertext = base64.b64encode(ciphertext)
        return ciphertext

    # 進行rsa加密
    def rsaEncrypt(self,text, pubKey, modulus):
        text = text[::-1]
        #rs = int(text.encode('hex'), 16) ** int(pubKey, 16) % int(modulus, 16)
        rs = int(codecs.encode(text.encode('utf-8'),'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16)
        return format(rs, 'x').zfill(256)
    # 將明文text進行兩次aes加密獲得密文encText
    def encrypted_request(self,text):
        modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
        nonce = '0CoJUm6Qyw8W8jud'
        pubKey = '010001'
        text = json.dumps(text)
        secKey = self.createSecretKey(16)
        encText = self.aesEncrypt(self.aesEncrypt(text, nonce), secKey)
        encSecKey = self.rsaEncrypt(secKey, pubKey, modulus)
        data = {
            'params': encText,
            'encSecKey': encSecKey
        }
        return data

還有一個offset偏移量的引數,來限制顯示不同頁的評論。

def get_offset(self, offset):
    if offset == 0:
        text = {'rid': '', 'offset': '0', 'total': 'true', 'limit': '20', 'csrf_token': ''}
    else:
        text = {'rid': '', 'offset': '%s' % offset, 'total': 'false', 'limit': '20', 'csrf_token': ''}
    return text

爬取資料

接著我們可以向網頁發出request請求,然後開始爬取資料啦。根據要求,需要爬取歌曲的熱評以及全部評論,且熱評只在第一頁出現。因此寫了兩個函式來分別爬取熱評以及全部評論,且由於後續需要對資料進行視覺化分析,因此根據需要生成了txt檔案以及csv檔案。
爬取熱門評論

def get_hot_comment(self):
    hot_comments_list = []
    hot_comments_list.append(u"使用者ID,使用者暱稱,使用者頭像地址,評論時間,點贊總數,評論內容\n")
    req = self.get_json_data(self.comment_url, offset=0)
    print("已連線熱評")
    hot_comments = req['hotComments']  # 熱門評論
    print("共有%d條熱門評論!\n" % len(hot_comments))
    for item in hot_comments:
        comment = item['content']  # 評論內容
        comments = comment.replace('\n','').replace('\t','').replace(' ','')
        print(comments)
        likedCount = int(item['likedCount'])  # 點贊總數
        comment_time = item['time']# 評論時間(時間戳)
        comment_times = comment_time/1000
        publicTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(comment_times))
        # print(publicTime)
        userID = item['user']['userId']  # 評論者id
        nickname = item['user']['nickname']  # 暱稱
        avatarUrl = item['user']['avatarUrl']  # 頭像地址
        comment_info = str(userID) + u"," + nickname + u"," + avatarUrl + u"," + str(
            publicTime) + u"," + str(likedCount) + u"," + comments + u"\n"
        # print(comment_info)
        hot_comments_list.append(comment_info)
    with codecs.open("%s_hotcomments.csv"%(song_id), 'a', encoding='utf-8') as f:
        f.writelines(hot_comments_list)

爬取全部評論

def get_song_comment(self):
    '''某首歌下全部評論 '''
    all_comments_list = []  # 存放所有評論
    # all_comments_list.append(u"使用者ID,使用者暱稱,使用者頭像地址,評論時間,點贊總數,評論內容\n")  # 頭部資訊
    comment_info = []
    data = self.get_json_data(self.comment_url, offset=0)
    comment_count = data['total']
    if comment_count < 20:
        comments = data['comments']
        for item in comments:
            comment = item['content']  # 評論內容
            comments = comment.replace('\n', '').replace('\t', '').replace(' ', '')
            likedCount = int(item['likedCount'])  # 點贊總數
            comment_time = item['time']  # 評論時間(時間戳)13位 要除以1000
            comment_times = comment_time / 1000
            publicTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(comment_times))
            # print(type(comment_time)) int
            userID = item['user']['userId']  # 評論者id
            nickname = item['user']['nickname']  # 暱稱
            avatarUrl = item['user']['avatarUrl']  # 頭像地址
            comment_info = str(userID) + u"," + nickname + u"," + avatarUrl + u"," + str(
                publicTime) + u"," + str(likedCount) + u"," + comments + u"\n"
            all_comments_list.append(comment_info)
        with codecs.open("%s_allcomments.csv"%(song_id), 'a', encoding='utf-8') as f:
            f.writelines(all_comments_list)
    if comment_count > 20:
        for offset in range(20, int(comment_count), 20):
            print("開始爬取第{}頁".format(offset/20))
            comment = self.get_json_data(self.comment_url, offset=offset)
            _comments = comment['comments']
            for item in _comments:
                comment = item['content']  # 評論內容
                comments = comment.replace('\n', '').replace('\t', '').replace(' ', '')#處理換行符以及空格
                likedCount = int(item['likedCount'])  # 點贊總數
                comment_time = item['time']  # 評論時間(時間戳)
                comment_times = comment_time / 1000
                publicTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(comment_times))
                userID = item['user']['userId']  # 評論者id
                nickname = item['user']['nickname']  # 暱稱
                avatarUrl = item['user']['avatarUrl']  # 頭像地址
                all_comments_list = str(userID) + u"," + nickname + u"," + avatarUrl + u"," + str(
                    publicTime) + u"," + str(likedCount) + u"," + comments + u"\n"
                # all_comments_list.append(comment_info)
                with codecs.open("%s_allcomments.csv"%(song_id), 'a', encoding='utf-8') as f:
                    f.writelines(all_comments_list)
    return all_comments_list

時間戳轉換
爬取出的資料中,時間是以13位的時間戳呈現,因此要進行轉換,通過網上的方法,13位的時間戳在轉換時要先除以1000。

comment_time = item['time']  # 評論時間(時間戳)
comment_times = comment_time / 1000
publicTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(comment_times))

結巴中文分詞

為了單獨對評論內容進行分析,因此,對評論內容進行了單獨的爬取。
中文文字的預處理過程可以分為分詞->去停用詞(包含空格、回車、標點符號等都算作停用詞)->詞頻統計。
全模式會對已經分詞出來的詞再進行分詞,一般情況下使用精確模式。如果對分詞的結果不滿意,可以新增自定義詞典。使用停用詞時,採用的是哈工大的停用詞表。

from collections import Counter
import jieba
import io

import sys

sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
song_id = input('please input song_id:')
# 新增需要自定以的分詞
jieba.add_word("wifi")
jieba.add_word("小眾")
jieba.add_word("天冷")
jieba.add_word("這首歌")
# 建立停用詞list
def stopwordslist(filepath):
    stopwords = [line.strip() for line in open(filepath, 'r').readlines()]
    return stopwords

# 對句子進行分詞
def seg_sentence(sentence):
    sentence_seged = jieba.cut(sentence.strip())
    stopwords = stopwordslist('stopwords.txt')  # 這裡載入停用詞的路徑
    outstr = ''
    for word in sentence_seged:
        if word not in stopwords:
            if len(word) > 1 and word != '\r\n':
                outstr += word
                outstr += " "
    return outstr

inputs = open('%s_allcomments_content.txt'%(song_id), 'r',encoding='utf-8')  # 載入要處理的檔案的路徑
outputs = open('jieba_%s.txt'%(song_id), 'w',encoding='utf-8')  # 載入處理後的檔案路徑
for line in inputs:
    line_seg = seg_sentence(line)  # 這裡的返回值是字串
    outputs.write(line_seg)
outputs.close()
inputs.close()
# WordCount
with open('jieba_%s.txt'%(song_id), 'r',encoding='utf-8') as fr:  # 讀入已經去除停用詞的檔案
    data = jieba.cut(fr.read())
data = Counter(data)

with open('%s_jieba.txt'%(song_id), 'w',encoding='utf-8') as fw:  # 讀入儲存wordcount的檔案路徑
    for k, v in data.most_common(500):
        fw.write('%s,%d\n' % (k, v))

視覺化

詞雲圖採用的是wordcloud包。
由於wordcloud本身的分配的顏色不太美觀,因此我選擇了對其新增背景圖,根據背景圖的顏色展示詞雲圖:使用numpy包開啟背景圖並轉化為數字矩陣,通過ImageColorGenerator從背景圖片生成顏色值,從而使詞雲圖顏色變為背景圖顏色。
在這裡插入圖片描述
也可以使用tableau進行資料視覺化。下面是我對評論時間以及熱門評論的分析,以及對全部評論進行結巴分詞後的詞雲圖展示。
在這裡插入圖片描述

最後

曾經有人說過,如果你也曾在深夜哭泣第二天照常打起精神上班上課,如果你也曾被陰霾重重包圍卻對生活留了一絲縫隙讓陽光照進,如果你曾迷惘,如果你曾受挫,如果你曾也大罵去他的生活,如果你曾也與看不見的敵人戰鬥過,如果你曾也想過一了百了,推薦中島美嘉的《曾經我也想過一了百了》。
從詞雲圖中,可以看到,出現最多的詞語就是“加油”、“好好活著”。可見評論者更多的是從這首歌中得到力量。熱評第一的是“我曾想死是因為,被說成是冷酷的人。想要被愛而哭泣,是因為嚐到了人的溫暖。我曾想死是因為,你美麗的笑了。一味想著死的事,一定是因為太過認真地活。我曾想死是因為,還未和你相遇。因為像你這樣的人生於這世上,我稍稍喜歡這個世界了。” 新的和年輕的,總是美好的,哪怕充滿未知。人能越活越年輕越活越自如想想真是概率太低且太難了。不過既然以後只有越來越老越來越沉重的份兒,可能真的要更珍惜現在吧。