爬取嗶哩嗶哩評論區(包含對評論的回復)並保存在xlsx中


第一次寫爬蟲,作業要求寫報告,那就修改一下順便發到這里啦。最后成型的代碼大量參考了這里

代碼地址在這里

要干什么

通過python爬蟲抓取嗶哩嗶哩彈幕視頻網任一視頻下的評論內容並保存為表格(.xlsx)

主要的問題

獲取請求URL

  1. 一開始沒有查看api文檔、直接嘗試獲取URL時已知出現問題,后來才知道要刪除中間的jQuery段

存儲

  1. 因為爬取的是評論區,常常有大段的文字,常用'/n'換行,常用的csv存儲可能會因此結構混亂,所以想通過xlsx存儲
  2. 但是常用的幾種操作xlsx的庫對於“添加”這一操作都非常困難,看了一會兒總覺得頭大
  3. 尋找了很久發現pandas的數據表操作非常實用,格式規范、合並簡單
  4. 但是json格式跟pandas常用的寫入方式還是不太一樣,不過還好python轉換鍵值對比較方便,具體實現可以看下方

子評論

  1. 對評論的回復(下稱“子評論”)常常被無視,它的請求URL和參數值都和父評論本身不一樣
  2. 遍歷他們並提取數據花費了一定的時間

爬取間隔

  1. 不是異步代碼。沒有header池的話一定要有時間間隔啊!!!!!!!!!!!!

網頁分析

隨意打開一個視頻的評論區,按下f12打開控制台搜索相關內容,找到評論數據的保存格式,並在標頭中找到對應的請求URL

例如:https://api.bilibili.com/x/v2/reply/main?callback=jQuery172005471583828573401_1649559130979&jsonp=jsonp&next=0&type=1&oid=297721627&mode=3&plat=1&_=1649563615266

查閱github上總結的api文檔了解到各必要參數意義:

  • next:頁碼
  • type:默認為1
  • oid:av號
  • mode:查詢模式(樓層、時間、熱度)
  • plat:默認為1

需要注意的是,中間的jQuery段需要刪除才能獲取請求

隨便一個視頻

找到了目標地址和格式就可以開始寫代碼了

代碼實現

沒有太多特別的地方,函數互相調用導致拆開了分析不太方便,一次性附上了

"""
主要參考:https://blog.csdn.net/mlyde/article/details/118936871
query說明:https://www.bilibili.com/read/cv8325021/
通過向API發送請求獲得json文件
請求地址:
https://api.bilibili.com/x/v2/reply?pn={1}&type={2}&oid={3}&sort={4}
"""

import requests
import os
import time
import json
import pandas as pd
from bilibili_api import video, sync # https://bili.moyu.moe/#/


# 全局變量
cookie = "最好修改成自己的,保留bili_jct、buvid3和SESSDATA,在終端查詢或者直接點擊瀏覽器地址欄的小鎖找找都能找到"
file_dir = "./comment_data/"
bv = "BV號,鏈接也成"
comment_mode = 3 # mode是需要傳入的api,規定了排序模式: 1:評論(樓層);2:最新評論(時間);3:熱門評論(熱度),不過1已經失效了


def b2a(bv_num):
    """
    調用現成的bilibili庫將用戶輸入的嗶哩嗶哩地址轉為真正用於識別視頻的oid(av號)
    :param bv_num: 用戶設定的嗶哩嗶哩視頻BV號,每個bv參數名都不一樣是因為pycharm一直提示我要從外部隱藏該名稱……
    :return: oid(av號)
    """
    v = video.Video(bvid=bv_num)
    info = sync(v.get_info())
    return info.get('aid', "None")


def response_f(bv_id, next=0, mode=3):
    """
    request網頁提取出的返回父評論的函數,以json格式傳輸
    :param bv_id: bv號
    :param next: json中用來標注頁碼
    :param mode: 所用的額排序模式
    :return: 返回提取后的json
    """

    api_url = 'https://api.bilibili.com/x/v2/reply/main'
    url = 'https://www.bilibili.com/video/' + bv_id
    av = b2a(bv_id) # 先轉av號
    # 復制的headers,user-agent和cookie是我自己的
    headers = {
        'accept': '*/*',
        'accept-encoding': 'gzip, deflate, br',
        'accept-language': 'zh-CN,zh;q=0.9',
        'cache-control': 'no-cache',
        'cookie': cookie,
        'pragma': 'no-cache',
        'referer': url,
        'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
        'sec-ch-ua-mobile': '?0',
        'sec-fetch-dest': 'script',
        'sec-fetch-mode': 'no-cors',
        'sec-fetch-site': 'same-site',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36 Edg/100.0.1185.36',
    }
    # 定義所拆解內容
    data = {
        'jsonp': 'jsonp',
        'next': next,  # 頁碼
        'type': '1',
        'oid': av,  # av號
        'mode': mode,  # 1:樓層大前小后, 2:時間晚前早后, 3:熱門評論
        'plat': '1',
        '_': str(time.time() * 1000)[:13],  # 時間戳
    }
    response = requests.get(api_url, headers=headers, params=data)
    # 中文,不定義編碼格式大概率會亂碼
    response.encoding = 'utf-8'

    # 將得到的json文本轉化為可讀json,這段是復制的
    if 'code' in response.text:
        c_json = json.loads(response.text)
    else:
        c_json = {'code': -1}
    if c_json['code'] != 0:
        print('json error!')
        print(response.status_code)
        print(response.text)
        return 0  # 讀取錯誤
    return c_json


def response_r(bv, rpid, pn=1):
    """
    返回子評論json,和父評論方式基本相同但是參數不同,重寫了一個,這里其實復用程度不是很夠,可以寫個循環+判斷省略的,因為是作業就偷懶了
    :param bv: bv號
    :param rpid: 父評論的id
    :param pn: 子評論的頁碼是通過pn判斷的
    :return: json格式的子評論
    """

    r_api_url = 'https://api.bilibili.com/x/v2/reply/reply'
    url = 'https://www.bilibili.com/video/' + bv
    av = b2a(bv)
    headers = {
        'accept': '*/*',
        'accept-encoding': 'gzip, deflate, br',
        'accept-language': 'zh-CN,zh;q=0.9',
        'cache-control': 'no-cache',
        'cookie': cookie,
        'pragma': 'no-cache',
        'referer': url,
        'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
        'sec-ch-ua-mobile': '?0',
        'sec-fetch-dest': 'script',
        'sec-fetch-mode': 'no-cors',
        'sec-fetch-site': 'same-site',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36',
    }
    data = {
        'jsonp': 'jsonp',
        'pn': pn,  # page number
        'type': '1',
        'oid': av,
        'ps': '10',
        'root': rpid,  # 父評論的rpid
        '_': str(time.time() * 1000)[:13],  # 時間戳
    }
    response = requests.get(r_api_url, headers=headers, params=data)
    response.encoding = 'utf-8'

    # 加載得到的json
    if 'code' in response.text:
        r_cjson = json.loads(response.text)
    else:
        r_cjson = {'code': -1}
    if r_cjson['code'] != 0:
        print('error!')
        print(response.status_code)
        print(response.text)
        return 0  # 讀取錯誤
    return r_cjson


def parse_comment_r(bv, rpid, df):
    """
    解析子評論json
    :param bv: bv號
    :param rpid: 父評論的id
    :param df: pandas datagram,作為本程序的數據傳遞方式
    :return: 返回修改后的df
    """

    cr_json = response_r(bv, rpid)['data']
    count = cr_json['page']['count']

    for pn in range(1, count // 10 + 2):
        time.sleep(0.1)
        print('p%d %d  ' % (pn, count), end='\r')
        cr_json = response_r(bv, rpid, pn=pn)['data']
        cr_list = cr_json['replies']
        if cr_list:  # 有時'replies'為'None'
            for i in range(len(cr_list)):
                comment_temp = [{
                    'time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(cr_list[i]['ctime'])),  # 時間
                    'like': cr_list[i]['like'],  # 贊數
                    'uid': cr_list[i]['member']['mid'],  # uid
                    'name': cr_list[i]['member']['uname'],  # 用戶名
                    'sex': cr_list[i]['member']['sex'],  # 性別
                    'content': '"' + cr_list[i]['content']['message'] + '"',  # 子評論
                }]  # 保留需要的內容
                df2 = pd.DataFrame(comment_temp)
                df = pd.concat([df, df2], axis=0, ignore_index=True)

    return df


def parse_comment_f(bv, df):
    """
    解析父評論json
    :param bv: bv號
    :param df: pandas datagram,作為本程序的數據傳遞方式
    :return: 返回修改后的df
    """

    c_json = response_f(bv, mode=comment_mode)
    if c_json:
        # 父評論總數
        try:
            count_all = c_json['data']['cursor']['all_count']
            print('comments:%d' % count_all)
        except KeyError:
            print('KeyError, 該視頻可能沒有評論!')
            return '0', '2'  # 找不到鍵值
    else:
        print('json錯誤')
        return '1', '0'  # json錯誤

    # 開始序號
    count_next = 0

    # 存放原始json
    all_json = ''

    for page in range(min((count_all // 20 + 1),150)):
        time.sleep(1)
        print('page:%d' % (page + 1))

        c_json = response_f(bv, count_next, mode=comment_mode)
        all_json += str(c_json) + '\n'
        if not c_json:
            return 1  # json錯誤
        count_next = c_json['data']['cursor']['next']  # 下一個的序號

        # 評論列表
        c_list = c_json['data']['replies']

        # 有評論,就進入下面的循環保存
        if c_list:
            for i in range(len(c_list)):
                comment_temp = [{
                    # 'floor': c_list[i]['floor'],			# 樓層
                    'time': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(c_list[i]['ctime'])),  # 時間
                    'like': c_list[i]['like'],  # 贊數
                    'uid': c_list[i]['member']['mid'],  # uid
                    'name': c_list[i]['member']['uname'],  # 用戶名
                    'sex': c_list[i]['member']['sex'],  # 性別
                    'content': c_list[i]['content']['message'],  # 評論內容
                }]  # 保留需要的內容
                # 若有子評論,記錄rpid,爬取子評論
                replies = False
                replies = False
                if c_list[i]['rcount'] or ('replies' in c_list[i] and c_list[i]['replies']):
                    replies = True
                    rpid = c_list[i]['rpid']
                    df = parse_comment_r(bv, rpid, df) # 如果有回復評論,爬取子評論

                df2 = pd.DataFrame(comment_temp)
                df = pd.concat([df, df2], axis=0, ignore_index=True)

            if c_json['data']['cursor']['is_end']:
                print('讀取完畢,結束')
                # 為最后一個json,結束爬取
                break
        else:
            print('評論為空,結束!')
            break
        time.sleep(0.2)
    return df, all_json


def main():
    global file_dir

    global bv
    if '/' in bv or '?' in bv:
        # 分解鏈接
        bv = bv.split('/')[-1].split('?')[0]

    # 處理存儲路徑
    if file_dir == '':
        file_dir = './'
    elif file_dir[-1] != '/' or file_dir[-1] != '\\':
        file_dir += '/'
    if not os.path.exists(file_dir):
        print('存儲路徑不存在', end='')
        os.mkdir(file_dir)
        print('已創建')

    data0 = [{'time': '', 'like': '', 'uid': '', 'name': '', 'sex': '', 'content': ''}]  # 行首
    df = pd.DataFrame(data0)
    df, all_json = parse_comment_f(bv, df)
    df = df.drop(index=0) # 這種方式會有空行,把它干掉

    # 保存
    while True: # 使用while以便占用時可以關掉文件后繼續操作而非必須從頭執行
        try:
            df.to_excel("./%s/%s.xlsx" % (file_dir, bv))
            break
        except PermissionError:
            input('文件被占用(關閉占用的程序后,回車重試)')


if __name__ == "__main__":
    main()
    print('== over! ==')


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM