【項目小結】某B視頻網站的爬蟲實踐

最近忽來興致,準備做評論數據的NLP項目。選定了某B視頻網站的評論數據,順帶準備把某B視頻網站的數據爬蟲也一起做了。關於登錄驗證的問題可以看我的博客https://blog.csdn.net/CY19980216/article/details/89074771,不過目前登錄方式稍微有點不同,因爲驗證圖片不太方便獲取了,我嘗試了後覺得只能通過截圖的方式才能拿到,如此魯棒性較差。而且由於也無法獲取到原圖的鏈接,還原的難度也增大了。然而目前數據爬蟲不需要登錄,暫時不喫力不討好地去突破當前的驗證碼。

基本上難度不大,目前先做了基於視頻的數據獲取。主要邏輯是遍歷一個個av號視頻,然後獲取視頻頁面上的關於視頻的各個信息以及視頻下面的評論信息。仍然使用了selenium驅動,主要問題是很多元素的xpath定位可能在網頁更新後會失效,不過我已經盡力寫得魯棒性較好了。

# -*- coding:UTF-8 -*-
# 作者: 囚生CY
# 最後更新: 20190715
# 轉載請註明原作者, 禁止用於商業用途

import re
import os
import sys
import time
import json
import numpy
import pandas
import random

from PIL import Image
from requests import Session
from bs4 import BeautifulSoup
from selenium import webdriver
from matplotlib import pyplot as plt
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains


class BiliBili():
	def __init__(self,
		username="用戶名",
		password="密碼",
		userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0",
	):																	 # 構造函數
		""" 類構造參數 """
		self.username = username
		self.password = password
		self.userAgent = userAgent
		""" 類常用參數 """
		self.workspace = os.getcwd()									 # 類工作目錄
		self.date = time.strftime("%Y%m%d")								 # 類構造時間
		self.labelCompiler = re.compile(r"<[^>]+>",re.S)				 # 標籤正則編譯
		self.tempFolder = "Temp"										 # 存儲臨時文件的文件夾
		self.videoFolder = "Video"										 # 存儲視頻數據的文件夾
		self.userFolder = "User"										 # 存儲用戶數據的文件夾
		self.commentFolder = "Comment"									 # 存儲評論數據的文件夾
		self.log = "{}.log".format(self.date)							 # 記錄文件
		self.videoPath = "{}\\{}\\{}".format(self.workspace,self.videoFolder,self.date)
		self.userPath = "{}\\{}\\{}".format(self.workspace,self.userFolder,self.date)
		self.commentPath = "{}\\{}\\{}".format(self.workspace,self.commentFolder,self.date)
		self.mainURL = "https://www.bilibili.com/"						 # BiliBili主頁
		self.loginURL = "https://passport.bilibili.com/login"			 # 用戶登錄頁面
		self.videoURL = "https://www.bilibili.com/video/av{}/"			 # 視頻網址鏈接
		self.userURL = "https://space.bilibili.com/{}"					 # 用戶空間鏈接
		self.options = webdriver.FirefoxOptions()						 # 火狐驅動配置
		self.headers = {"User-Agent": userAgent}
		self.session = Session()
		self.videoField = [												 # 視頻數據庫字段
			"av",														 # 視頻av號
			"title",													 # 視頻標題
			"up",														 # UP主暱稱
			"follower",													 # UP粉絲數
			"playback_volume",											 # 播放量
			"barrage",													 # 彈幕數
			"like",														 # 點贊數
			"coin",														 # 硬幣數
			"collect",													 # 收藏數
			"comment",													 # 評論數
			"comment_page",												 # 評論頁數
			"category",													 # 視頻類別
			"tags",														 # 視頻標籤(用|隔開)
			"timestamp",												 # 爬取數據的時間戳
		]
		self.userField = [												 # 用戶數據庫字段
			"id",														 # 用戶ID
			"name",														 # 用戶暱稱
			"gender",													 # 性別
			"level",													 # 用戶等級
			"signature",												 # 個性簽名
			"is_member",												 # 是否爲大會員
			"fans_icon",												 # 是否開通粉絲勳章
			"follower",													 # 關注TA的人
			"followee",													 # TA關注的人
			"playback_volume",											 # 總播放量
			"reading_volume",											 # 總閱讀數
			"contribution",												 # 投稿數
			"timestamp",												 # 爬取數據的時間戳
		]
		self.commentField = [											 # 評論數據庫的字段
			"av",														 # 視頻av號
			"id",
			"name",														 # 用戶暱稱
			"level",													 # 用戶等級
			"text",														 # 評論內容
			"like",														 # 點贊數
			"reply",													 # 回覆數
			"date",														 # 評論日期
			"timestamp",												 # 爬取數據的時間戳
		]
		""" 類初始化 """
		self.session.headers = self.headers.copy()
		self.options.add_argument("--headless")							 # 設定無頭瀏覽器的配置

		if not os.path.exists("{}\\{}".format(self.workspace,self.tempFolder)):
			string = "正在新建文件夾以存儲臨時文件..."
			print(string)
			os.mkdir("{}\\{}".format(self.workspace,self.tempFolder))

		if not os.path.exists(self.videoPath):							 # 視頻數據文件初始化
			string = "正在新建文件夾以存儲視頻數據{}...".format(self.date)
			print(string)
			if not os.path.exists("{}\\{}".format(self.workspace,self.videoFolder)): os.mkdir("{}\\{}".format(self.workspace,self.videoFolder))
			os.mkdir(self.videoPath)
			with open("{}\\video{}.csv".format(self.videoPath,self.date),"w") as f:
				count = -1
				for field in self.videoField:
					count += 1
					if count: f.write(",{}".format(field))
					else: f.write(field)
				f.write("\n")

		if not os.path.exists(self.userPath):							 # 用戶數據文件初始化
			string = "正在新建文件夾以存儲用戶數據{}...".format(self.date)
			print(string)
			if not os.path.exists("{}\\{}".format(self.workspace,self.userFolder)): os.mkdir("{}\\{}".format(self.workspace,self.userFolder))
			os.mkdir(self.userPath)
			with open("{}\\user{}.csv".format(self.userPath,self.date),"w") as f:
				count = -1
				for field in self.userField:
					count += 1
					if count: f.write(",{}".format(field))
					else: f.write(field)
				f.write("\n")

		if not os.path.exists(self.commentPath):						 # 評論數據文件初始化
			string = "正在新建文件夾以存儲評論數據{}...".format(self.date)
			print(string)
			if not os.path.exists("{}\\{}".format(self.workspace,self.commentFolder)): os.mkdir("{}\\{}".format(self.workspace,self.commentFolder))
			os.mkdir(self.commentPath)
			with open("{}\\comment{}.csv".format(self.commentPath,self.date),"w") as f:
				count = -1
				for field in self.commentField:
					count += 1
					if count: f.write(",{}".format(field))
					else: f.write(field)
				f.write("\n")

	def login_20190408(self,):											 # 用戶登錄(20190408更新, 20190712檢驗已失效)

		def download_verifying_picture(divs,name):						 # 下載滑動驗證圖片	
			style = divs[0].attrs["style"]
			index1 = style.find("(")
			index2 = style.find(")")
			url = eval(style[index1+1:index2])
			html = self.session.get(url).content
			with open("{}\\{}\\{}.webp".format(self.workspace,self.tempFolder,name),"wb") as f: f.write(html)

		def recover_picture(divs,name):									 # 設法復原下載好的圖片(該函數默認切片是兩行)
			index = []
			for div in divs:											 # 遍歷所有切片(52片)
				style = div.attrs["style"]
				index1 = style.find("background-position")				 # 尋找背景圖的切片座標
				temp = style[index1+21:-1].strip().replace("px","").replace("-","").split()
				temp = [int(i) for i in temp]				
				index.append(temp)
			image = Image.open("{}\\{}\\{}.webp".format(self.workspace,self.tempFolder,name))
			image = numpy.asarray(image)								 # 圖片轉矩陣
			imageRe = numpy.zeros(image.shape)							 # 初始化復原圖片矩陣
			total = len(index)											 # 獲取總切片數
			Xaxis,Yaxis,Zaxis = image.shape								 # 獲取圖片三維信息(116×312×3)
			X = int(2*Yaxis/total)										 # 每個切片的列寬(12px)
			Y = int(Xaxis/2)											 # 每個切片的行高(58px)
			index = [[int((indice[0]-1)/X),int(indice[1]>0)] for indice in index]
			for i in range(total):										 # 遍歷切片復原
				x1 = index[i][0]*X										 # 切片實際左座標
				x2 = x1+X												 # 切片實際右座標
				y1 = index[i][1]*Y										 # 切片實際上座標
				y2 = y1+Y												 # 切片實際下座標
				a = int(Y)												 # 切片原橫座標
				b1 = int((i%(total/2))*X)								 # 切片原上座標
				b2 = int((i%(total/2))*X+X)								 # 切片原下座標
				""" 判斷當前切片是第幾行(目前按照默認是前26個爲第一行切片,後26個爲第二行切片來做的) """
				if i<total/2: imageRe[:a,b1:b2,:] = image[y1:y2,x1:x2,:] # 第一行
				else: imageRe[a:,b1:b2,:] = image[y1:y2,x1:x2,:]		 # 第二行
			imageRe = Image.fromarray(imageRe.astype("uint8"))			 # 圖片格式的文件矩陣元素一定爲uint8
			imageRe.save("{}\\{}\\test{}.webp".format(self.workspace,self.tempFolder,name))

		def find_block_space(width=53,zoo=1.15,plot=True):				 # 尋找缺塊位置(默認參數width爲缺塊的列寬像素,zoo這邊用1.15基本上大概率能過了,但是我查了一下兩個圖片的屬性應該是1.2,設爲1.2應該要改常數項了)
			"""
			這裏的方法非常簡單:
			我本來是想可能需要用到opencv,
			但是我發現因爲已知復原圖片的數據,
			所以直接將圖片數據的列向量計算相似度即可,
			相似度最差的地方即爲缺塊;
			另外觀察發現圖片的像素爲行高59&列寬53,
			共312px列中前53小的相似度列取中間位置應該即可;
			"""
			image1 = numpy.asarray(Image.open("{}\\{}\\test1.webp".format(self.workspace,self.tempFolder)))
			image2 = numpy.asarray(Image.open("{}\\{}\\test2.webp".format(self.workspace,self.tempFolder)))
			Xaxis,Yaxis,Zaxis = image1.shape							 # 獲取圖片三維信息(116×312×3)
			errors = []													 # 記錄312列寬上每個列向量的非相似度值
			for i in range(Yaxis):
				total = 0
				for j in range(Zaxis):
					X = numpy.array([image1[:,i,j]]).astype("int64")
					Y = numpy.array([image2[:,i,j]]).astype("int64").T
					normX = numpy.linalg.norm(X,2)
					normY = numpy.linalg.norm(Y,2)
					dotXY = numpy.dot(X,Y)[0,0]
					error = 1.- (dotXY/normX/normY)						 # 這裏我選擇累積RGB在(1-餘弦相似度)上的值
					total += error
				errors.append(total)
			tempErrors = errors[:]
			tempErrors.sort(reverse=True)
			index = [errors.index(i) for i in tempErrors[:width]]		 # 計算排序後對應的索引排序(根據圖像的結果來看應該前width的索引是至少近似連續的自然數)
			if plot:
				plt.plot([i for i in range(len(errors))],errors)
				plt.savefig("{}\\{}\\error.jpg".format(self.workspace,self.tempFolder))
			return min(index[:10])/zoo-10

		def get_track(xoffset):											 # 獲取一條路徑
			tracks = []
			x = int(xoffset/2)											 # 先走一半(很關鍵,不走不給過)
			tracks.append(x)
			xoffset -= x
			while xoffset>=10:
				x = random.randint(5,9)
				tracks.append(x)
				xoffset -= x
			for i in range(int(xoffset)): tracks.append(1)				 # 最後幾步慢慢走
			return tracks

		while True:
			browser = webdriver.Firefox()								 # 驅動火狐瀏覽器
			browser.get(self.loginURL)									 # 訪問登錄頁面
			interval = 1.												 # 初始化頁面加載時間(如果頁面沒有加載成功,將無法獲取到下面的滑動驗證碼按鈕,林外我意外的發現有時候竟然不是滑動驗證,而是驗證圖片四字母識別,個人感覺處理滑動驗證更有意思)
			while True:													 # 由於可能未成功加載,使用循環確保加載成功
				browser.find_element_by_id("login-username").send_keys(self.username)
				browser.find_element_by_id("login-passwd").send_keys(self.password)
				xpath = "//div[@class='gt_slider_knob gt_show']"		 # 滑動驗證碼最左邊那個按鈕的xpath定位
				try:
					time.sleep(interval)								 # 等待加載
					div = browser.find_element_by_xpath(xpath)
					break
				except:
					browser.refresh()
					interval += .5										 # 每失敗一次讓interval增加0.5秒
					print("頁面加載失敗!頁面加載時間更新爲{}".format(interval))

			ActionChains(browser).click_and_hold(on_element=div).perform()
			html = browser.page_source									 # 此時獲取的源代碼中將包含滑動驗證圖片以及存在缺塊的滑動驗證圖片
			soup = BeautifulSoup(html,"lxml")							 # 解析頁面源代碼
			div1s = soup.find_all("div",class_="gt_cut_fullbg_slice")	 # 找到沒有缺塊的驗證圖片52個切片
			div2s = soup.find_all("div",class_="gt_cut_bg_slice")		 # 找到存在缺塊的驗證圖片52個切片
			div3 = soup.find("div",class_="gt_slice gt_show gt_moving")	 # 找到那個傳說中的缺塊						
			download_verifying_picture(div1s,1)							 # 下載無缺塊
			download_verifying_picture(div2s,2)							 # 下載有缺塊
			recover_picture(div1s,1)									 # 復原無缺塊
			recover_picture(div2s,2)									 # 復原有缺塊
			xoffset = find_block_space()								 # 尋找缺塊位置的橫座標
			tracks = get_track(xoffset)
			total = 0
			for track in tracks:
				print(track)
				total += track
				ActionChains(browser).move_by_offset(xoffset=track,yoffset=random.randint(-5,5)).perform()
				time.sleep(random.randint(50,100)/100)
			ActionChains(browser).move_by_offset(xoffset=5,yoffset=random.randint(-5,5)).perform()
			ActionChains(browser).move_by_offset(xoffset=-5,yoffset=random.randint(-5,5)).perform()
			time.sleep(0.5)
			ActionChains(browser).release(on_element=div).perform()
			time.sleep(3)
			xpath = "//a[@class='btn btn-login']"						 # 登錄按鈕的xpath定位
			browser.find_element_by_xpath(xpath).click()				 # 點擊登錄按鈕
			html = browser.page_source
			time.sleep(1.)
			soup = BeautifulSoup(html,"lxml")
			title = soup.find("title")
			if str(title.string[4])=="彈":
				print("登錄失敗!準備重新登錄!")
				browser.quit()
			else:
				print("登錄成功!")
				return browser

	def login_20190712(self,):											 # 用戶登錄(20190712更新)
		"""
		目前登錄方式相對於20190408的區別在於一下幾點:
		 - 次序上是先輸入用戶名密碼, 點擊登錄後纔會出現驗證碼圖片;
		 - 驗證碼圖片的元素結構變化, 沒有小切片, 並且無法獲取原圖鏈接, 這大大增加了復原的難度(而且我還找不到);
		 - 滑動按鈕並未改變, 因此看起來是極驗自身升級了, 因爲近期無登錄需求, 不打算攻破這種驗證碼, 認爲在識別上有一定難度;
		"""
		pass

	def parse_video(self,av,driver,
		isVedioString="player-wrap",									 # 用於判斷視頻是否失效的字符串
		maxpage=50,														 # 最多獲取maxpage頁的評論(按熱度排序)
	):																	 # 給定av號與瀏覽器驅動, 獲取視頻數據
		driver.get(self.videoURL.format(av))							 # 訪問視頻鏈接
		html = driver.page_source										 # 立即獲取源碼
		if not isVedioString in html: return False						 # 確認視頻是否存在: 如果源碼中有isVedioString則認爲視頻未失效			 
		while True:														 # 加載需要時間: 之前我使用WebDriverWait方法, 但是發現只要加載成功, 頁面會回到頂部, 結果就又找不到底部的元素
			driver.execute_script("window.scrollBy(0,500)")				 # 滾屏找到評論
			try: divs = driver.find_element_by_xpath('//div[@class="baffle"]')
			except: continue											 # 找不到元素繼續滾屏
			break														 # 走到這一步當然是找到元素咯
		html = driver.page_source										 # 獲取完整的源代碼
		timestamp = int(time.time())									 # 即刻獲取timestamp

		soup = BeautifulSoup(html,"lxml")								 # 解析頁面源代碼
		"""
		生成視頻數據字段:
		 - title: "//span[@class='tit']";
		 - up: 有點硬寫得;
		 - follower: 寫得也很硬;
		 - playback_volume: 第一種從title屬性中獲取,過濾掉的前4個字符是總播放數(精確到個位), 第二種直接拿string(精確到千位);
		 - barrage: 第一種從title屬性中獲取, 過濾掉前7個字符是歷史累計彈幕數(精確到個位), 第二種直接拿string(精確到千位);
		 - like: 第一種從title屬性中獲取, 過濾掉前3個字符是點贊數(精確到個位), 第二種直接拿string(精確到千位);
		 - coin: "//span[@class='tit']";
		 - collect: "//span[@class='tit']";
		 - comment: 利用先定位到"//div[@class='common']", 降低容錯率;
		 - comment_page: 利用先定位到"//div[@class='common']", 降低容錯率;
		 - category: 目前來看只有早期部分視頻沒有分類, 幾乎所有視頻是有分類的, 因此元素可能定位不到;
		 - tags: 少數視頻無tag, 不同的tag用"|"分開;
		 - timestamp: 時間戳;
		"""
		title = str(soup.find("span",class_="tit").string)
		up = str(soup.find("div",class_="u-info").find("div",class_="name").find("a").string)
		follower = str(soup.find("i",class_="van-icon-general_addto_s").find_next_sibling().string)

		playback_volume = soup.find("span",class_="view")
		playback_volume1 = playback_volume.attrs["title"][4:]
		playback_volume2 = str(playback_volume.string)
		playback_volume2 = playback_volume2[:playback_volume2.find("播放")]

		barrage = soup.find("span",class_="dm")
		barrage1 = barrage.attrs["title"][7:]		
		barrage2 = str(barrage.string)					
		barrage2 = barrage2[:barrage2.find("彈幕")]
		
		temp = soup.find("div",class_="ops")
		like = temp.find("span",class_="like")
		like1 = like.attrs["title"][3:]
		like2 = self.labelCompiler.sub("",str(like)).replace("\n","").replace(" ","")
		coin = temp.find("span",class_="coin")
		coin = self.labelCompiler.sub("",str(coin)).replace("\n","").replace(" ","")
		collect = temp.find("span",class_="collect")
		collect = self.labelCompiler.sub("",str(collect)).replace("\n","").replace(" ","")

		temp = soup.find("div",class_="common")
		comment = temp.find("span",class_="b-head-t results").string
		comment = 0 if comment is None else int(comment)
		comment_page = 0 if comment==0 else int(str(temp.find("span",class_="result").string).replace(" ","")[1:-1])

		try: category = str(soup.find("span",class_="a-crumbs").find("a").string)
		except: category = str()										 # 無分類: 使用異常測試儘管不會報錯, 但是出問題也將盡快發現20190714;

		temp = soup.find("div",id="v_tag").find_all("li",class_="tag")	 # 該temp包含了所有tag
		tags = str()
		for tag in temp: tags += "{}|".format(tag.find("a").string)
		tags = tags[:-1]												 # 去掉最後一個"|"符號

		string = str()
		for item in [av,title,up,follower,playback_volume1,barrage1,like1,coin,collect,comment,comment_page,category,tags,timestamp]: string += "{},".format(item)
		string = "{}\n".format(string[:-1])
		with open("{}\\video{}.csv".format(self.videoPath,self.date),"a",encoding="UTF-8") as f: f.write(string)

		if comment==0: return 											 # 無評論就告辭了
		""" 以下開始獲取評論信息 """
		driver.find_element_by_xpath("//li[@class='hot-sort  on']").click()
		page = 0														 # 記錄當前頁數
		while page<maxpage:
			timestamp = int(time.time())
			page += 1
			string = " - 正在獲取第{}頁的評論信息...".format(page)
			print(string)
			with open("{}\\{}".format(self.tempFolder,self.log),"a") as f: f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))
			html = driver.page_source									 # 按照熱門度排序後的html
			soup = BeautifulSoup(html,"lxml")							 # 重新解析html
			div = soup.find("div",class_="comment-list")				 # 定位到評論表
			if not div.find("div",class_="no-more-reply") is None: break # 某些視頻最後一頁評論無數據: 如在av32的最後一頁評論上是沒有數據的20190714
			""" 因爲採取了預定位因此基本上不應該找錯 """
			for child in div.children:									 # 通常來說div有20個寶寶(一頁20條評論)
				temp = child.find("a",class_="name")
				uid = temp.attrs["data-usercard-mid"]					 # id
				name = str(temp.string)									 # name
				level = child.find("i").attrs["class"][1]				 # level
				text = self.labelCompiler.sub("",str( child.find("p",class_="text")))
				text = text.replace("\n","|")							 # text: 因爲評論往往有@符號導致出現<a>標籤, 不方便直接獲取string, 因此選擇去標籤正則, 有些評論有換行, 目前先用"|"符號替代\n
				like = child.find("span",class_="like").string
				like = 0 if like is None else int(like)					 # like: 當無點贊時string位置是空
				reply = child.find("div",class_="reply-box")
				reply = len(list(reply.children))						 # reply
				date = str(child.find("span",class_="time").string)		 # date
				string = str()
				for item in [av,uid,name,level,text,like,reply,date,timestamp]: string += "{},".format(item)
				string = "{}\n".format(string[:-1])			
				with open("{}\\comment{}.csv".format(self.commentPath,self.date),"a",encoding="UTF-8") as f: f.write(string)
			try: driver.find_element_by_xpath("//a[@class='next']").click()
			except: break												 # 找不到下一頁的按鈕了

	def parse_user(self,uid,driver,
		xpath_flag="//div[@id='app']"									 # 用於判定頁面是否加載完成: 未登錄狀態時或不爲TA的粉絲時爲<div id="app" class="vistor">, 登錄狀態時且爲TA的粉絲爲<div id="app" class="fans">, 總之只看id屬性差不多夠了
	):																	 # 給定用戶ID與瀏覽器驅動, 獲取用戶數據
		driver.get(self.userURL.format(uid))
		WebDriverWait(driver,15).until(lambda driver: driver.find_element_by_xpath(xpath_flag).is_displayed())
		html = driver.page_source										 # 相對來說用戶空間的html加載很快
		timestamp = int(time.time())
		soup = BeautifulSoup(html,"lxml")								 # 解析起來也較爲容易
		
		self.userField = [												 # 用戶數據庫字段
			"id",														 # 用戶ID
			"name",														 # 用戶暱稱
			"gender",													 # 性別
			"level",													 # 用戶等級
			"signature",												 # 個性簽名
			"is_member",												 # 是否爲大會員
			"fans_icon",												 # 是否開通粉絲勳章
			"follower",													 # 關注TA的人
			"followee",													 # TA關注的人
			"playback_volume",											 # 總播放量
			"reading_volume",											 # 總閱讀數
			"contribution",												 # 投稿數
			"timestamp",												 # 爬取數據的時間戳
		]
		with open("log_{}.html".format(uid),"w",encoding="UTF-8") as f: f.write(html)	 
		temp = soup.find("div",class_="h-basic")						 # 定位到左上部用戶信息區域
		name = str(temp.find("span",id="h-name").string)				 # name: 用戶暱稱應該不會有什麼問題
		gender = temp.find("span",id="h-gender").attrs["class"]			 # 在性別標籤的class屬性下包含了性別信息
		gender = gender[2] if len(gender)==3 else str()					 # gender: 性別爲男女不定, class標籤爲["icon","gender","male"/"female"], 沒有填寫性別的用戶沒有第三個class, 且不展示
		level = temp.find("a",class_="h-level m-level").attrs["lvl"][0]	 # level: level1~level6, 應該也不會有什麼問題 
		signature = str(temp.find("div",class_="h-basic-spacing").find("h4",class_="h-sign").string)
		signature = str() if signature=="None" else signature.strip()	 # signature: 個性簽名爲空處理爲空字符串, 不展示
		is_member = temp.find("a",class_="h-vipType").string
		is_member = False if is_member is None else True				 # is_member: 開通年度大會員的用戶string字段是"年度大會員", 未開通的該字段不展示且爲空
		fans_icon = temp.find("span",class_="h-fans-icon")
		fans_icon = False if fans_icon is None else True				 # fans_icon: 與上面的不展示的標籤不同, 未開通粉絲勳章的用戶是沒有該標籤的

		temp = soup.find("div",class_="n-statistics")					 # 定位到右上部用戶數據統計區域
		follower = temp.find("a",class_="n-data n-fs").attrs["title"]	 # follower: 這個應該沒有太多問題, title屬性裏是精確的個數, string部分裏精確到千位
		followee = temp.find("a",class_="n-data n-gz").attrs["title"]	 # followee: 這個應該沒有太多問題, title屬性裏是精確的個數, string部分裏精確到千位
		volumes = temp.find_all("a",class_="n-data n-bf")				 # 這部分是流量區域: 目前我只找到播放數與閱讀數兩種, 沒有投稿的人不會有播放數, 沒有動態的人不會有閱讀數
		if len(volumes)==0: playback_volume = reading_volume = 0		 # 無播放數, 無閱讀數(大部分邊緣用戶)
		elif len(volumes)==1:											 # 播放數閱讀數二選一(代表人物:papi醬, 1532165)
			string = str(volume[0].find("p",class_="n-data-k").string)
			if string=="播放數":
				reading_volume = 0
				playback_volume = volumes[0].attrs["title"].replace(",","")				
			elif string=="閱讀數":
				playback_volume = 0
				reading_volume = volumes[0].attrs["title"].replace(",","")						
			else:														 # 異常記錄
				with open("{}\\{}".format(self.tempFolder,self.log),"a") as f: f.write("Error1: 無法確定流量類別!UID{}\t{}\n".format(uid,time.strftime("%Y-%m-%d %H:%M:%S")))	
		elif len(volumes)==2:											 # 有播放數, 有閱讀數(代表人物:lexburner, 777536)
			playback_volume = volumes[0].attrs["title"].replace(",","")	 # playback_volume: 播放量超過1000則title屬性裏的精確播放數會有","符號
			reading_volume = volumes[1].attrs["title"].replace(",","")	 # reading_volume: 閱讀量超過1000則title屬性裏的精確閱讀數會有","符號
		else:															 # 如果超過2個我決定拋出異常
			with open("{}\\{}".format(self.tempFolder,self.log),"a") as f: f.write("Error2: 流量數量超過2!UID{}\t{}\n".format(uid,time.strftime("%Y-%m-%d %H:%M:%S")))	
		temp = soup.find("a",class_="n-btn n-video n-audio n-article n-album")
		contribution = int(soup.find("span",class_="n-num").string)
		string = str()
		for item in [uid,name,gender,level,signature,is_member,fans_icon,follower,followee,playback_volume,reading_volume,contribution,timestamp]: string += "{},".format(item)
		string = "{}\n".format(string[:-1])		
		with open("{}\\user{}.csv".format(self.userPath,self.date),"a",encoding="UTF-8") as f: f.write(string)

	def parse(self,
		headless=False,
	):		
		av = 0															 # 記錄當前av號
		driver = webdriver.Firefox(options=self.options) if headless else webdriver.Firefox()
		driver.implicitly_wait(10)										 # 設置等待超時
		while True:
			av += 1
			string = "正在獲取av{}的信息...".format(av)
			print(string)
			with open("{}\\{}".format(self.tempFolder,self.log),"a") as f: f.write("{}\t{}\n".format(string,time.strftime("%Y-%m-%d %H:%M:%S")))
			self.parse_video(av,driver)
		driver.quit()

	def test(self,):													 # 測試代碼
		driver = webdriver.Firefox()
		uids = [777536,1532165,281317955]
		driver.implicitly_wait(10)	
		for uid in uids:
			self.parse_user(uid,driver)

if __name__ == "__main__":
	bilibili = BiliBili()
	#bilibili.parse()
	bilibili.test()

先寫這麼多吧,之後再寫基於用戶的爬蟲,可能會更新吧。測試下來速度尚可,24小時應該可以遍歷掉5000~10000個視頻的樣子,目前某B的評論數據給出了基於時間排序與基於熱度排序,這裏已經切換爲基於熱度排序,且至多獲取50頁的評論(即每個視頻最多獲取1000條按熱度排序的評論,全獲取有點慢)。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章