第一次寫爬蟲,作業要求寫報告,那就修改一下順便發到這里啦。最后成型的代碼大量參考了這里
代碼地址在這里
要干什么
通過python爬蟲抓取嗶哩嗶哩彈幕視頻網任一視頻下的評論內容並保存為表格(.xlsx)
主要的問題
獲取請求URL
- 一開始沒有查看api文檔、直接嘗試獲取URL時已知出現問題,后來才知道要刪除中間的jQuery段
存儲
- 因為爬取的是評論區,常常有大段的文字,常用'/n'換行,常用的csv存儲可能會因此結構混亂,所以想通過xlsx存儲
- 但是常用的幾種操作xlsx的庫對於“添加”這一操作都非常困難,看了一會兒總覺得頭大
- 尋找了很久發現pandas的數據表操作非常實用,格式規范、合並簡單
- 但是json格式跟pandas常用的寫入方式還是不太一樣,不過還好python轉換鍵值對比較方便,具體實現可以看下方
子評論
- 對評論的回復(下稱“子評論”)常常被無視,它的請求URL和參數值都和父評論本身不一樣
- 遍歷他們並提取數據花費了一定的時間
爬取間隔
- 不是異步代碼。沒有header池的話一定要有時間間隔啊!!!!!!!!!!!!
網頁分析
隨意打開一個視頻的評論區,按下f12打開控制台搜索相關內容,找到評論數據的保存格式,並在標頭中找到對應的請求URL
查閱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! ==')
