【日常】某B視頻網站模擬登錄嘗試


2019.04.07 更新

清明把手頭事情大致也辦得差不多了,接下來就要開始忙課程上的項目了。本來打算今晚開始把tensorflow跟pytorch撿起來趕緊複習一下的了,可惜有時候人就是賤骨頭,還是想要做一些自己“更感興趣”的事情。選擇了某B視頻網站的登錄嘗試模擬登錄。

爲什麼會選擇去對付某B視頻網站,主要想試試看能不能解決掉滑動驗證碼這個一直想嘗試的問題。考慮到現在登錄驗證的方式往往更新得很快,因此這篇博客相對來說的時效性較強,我決定開始做一個持續更新的博客。

下面這段代碼可以完成模擬滑動的操作,雖然並不能恰好補掉缺塊。因爲是從類中摘出,需要修正?

from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains

def login(self,):
	browser = webdriver.Firefox()
	browser.get(self.loginURL)
	#browser.switch_to_frame('x-URS-iframe')
	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 = "//li[@class='item gc clearfix']"
		#li = browser.find_element_by_xpath(xpath)
		#ActionChains(browser).click_and_hold(on_element=li).perform()
		#input("已經點擊了li標籤了...")
		xpath = "//div[@class='gt_slider_knob gt_show']"
		try:
			time.sleep(1)
			div = browser.find_element_by_xpath(xpath)
			break
		except:
			browser.refresh()
			continue
	ActionChains(browser).click_and_hold(on_element=div).perform()
	html = browser.page_source
	with open("3.txt","w") as f:
		f.write(html)
	input()
	#input()
	#time.sleep(0.15)
	time.sleep(0.5)
	ActionChains(browser).move_to_element_with_offset(to_element=div,xoffset=220,yoffset=22).perform()
	"""
	html = browser.page_source
	with open("1.txt","w") as f:
		f.write(html)
	"""
	time.sleep(0.5)
	input("點擊登錄...")
	xpath = "//a[@class='btn btn-login']"
	browser.find_element_by_xpath(xpath).click()					 # 點擊登錄按鈕
	input("點擊退出調試...")
	browser.quit()

現在問題在於怎麼處理那張缺了一塊的圖片,這個問題相當有趣,某B的網站設計員還動了點歪腦筋,居然把圖片給切成幾十塊給打散再拼起來了,暫時我還沒有找到輔原圖片的JS代碼,只能拿到被打散的圖片?

如果解決了圖片復原的工作,接下來就是開始判斷缺塊在哪裏了。這件事情我感覺註定很有意思。


2019.04.08

剛了一晚上,真的是浪費時間,還好給這玩意兒剛掉了。

簡單說一下思路了,在頁面上審查元素就可以看到其實原圖片也被劃分爲多個切片,而每個切片對應一個div標籤,該div標籤中的style屬性下的background-position字段下的兩個數字表明瞭這個切片在昨天那張碎碎圖中的位置,因此復原圖片的事情就一目瞭然了?

簡單貼一張復原好的圖片,全部代碼在今天更新的結尾?

同理可以復原有缺塊的圖片?

接下來就是如何確定缺塊位置。這兩張圖片都是116×312×3的矩陣,當然其實這兩個圖片沒有缺塊的部分的RGB值也不完全相同,因爲我需要知道缺塊在列寬上的位置,因此我選擇將圖片的RGB矩陣按列索引,在RGB三維上依次計算兩張圖片的餘弦相似度SR、SG、SB,然後統計(3-SR-SG-SB)的值,得到了如下的圖表?

多試幾次可以知道這張圖的樣子都差不了太多,高峯自然是缺塊,另外最右邊還有點突起的就是迷惑性缺塊(雖然並沒有什麼用)。這裏需要確定缺塊的左邊緣,多次嘗試後結論是取數值前10大的橫座標,然後取他們中最小的橫座標即可;

當然問題還是有的,我一開始這麼做始終對不上,後來終於發現這復原的圖片跟網站上原圖還是有區別的,顯然長寬都被拉伸或壓縮了。我的測試結果是移動min(index[:10])/1.15 - 10基本可以以較大的概率通過測試(1.15是放縮比例,移動的初始點距離圖片左邊緣的距離)。

事情當然沒有這麼簡單,儘管這樣可以完美補掉缺塊,但是滑動驗證碼的精髓就在於它還能識別是否爲真人操作。你一下子移過去重合缺塊顯然暴露了你開掛的本質,我後來又嘗試到了之後來回抖一抖,還是不行。

查了一下大家的方法千奇百怪,甚至有用先勻加速再勻減速的方法去搞,然而我試了都不行,最後我經過多次嘗試得到的方法是:先一下子劃一半的距離,然後random.randint(5,9)px一步步往前走,距離目標少於10px時一步步1px走到終點。到了終點再來回5px抖一下就完事了。注意每次操作之間的時間間隔設置爲random.uniform(0.5,1)s即可。

時間很急,寫的也很急,直接給代碼了吧?

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.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.tempFolder = "Temp"										 # 存儲臨時文件的文件夾
		self.mainURL = "https://www.bilibili.com/"						 # BiliBili主頁
		self.loginURL = "https://passport.bilibili.com/login"			 # 用戶登錄頁面
		self.options = webdriver.FirefoxOptions()						 # 火狐驅動配置
		self.session = Session()
		self.headers = {"User-Agent": userAgent}
		""" 類初始化 """
		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))	 # 新建文件夾存儲臨時文件		

	def login(self,):													 # 用戶登錄

		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爲缺塊的列寬像素)
			"""
				這裏的方法非常簡單:
				我本來是想可能需要用到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()
			"""
				這裏有個相當細節的地方:
				我不知道爲什麼,如果連續兩次使用move_by_offset,
				後一次的move_by_offset會先將前一次的move_offset再執行一次,
				然後纔會執行當次move_offset,
				所以這個軌跡要動點腦子纔行;
			"""
			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

if __name__ == "__main__":
	bilibili = BiliBili()
	bilibili.login()

接下來可能就是要做後續的爬蟲部分了,說實話很想一直做做這些事情,但是生活裏總有其他事情不得不去做。這邊就告一段落了,什麼時候有時間再回來接着寫了。

附一張登錄成功的圖片,希望之後回來這代碼還能用吧【小電視 苦惱】

 分享學習,共同進步!


2019.05.22

前幾天去網頁端看了看居然又改了,原先點出驗證圖片的按鈕一開始就在頁面上,現在需要輸入用戶名密碼後點擊登錄纔會彈出圖片。雖然區別並不是很大,但是這改得也太不走心了,沒什麼區別好像。。。


2019.07.15

近期偷閒準備再做一些。

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

主要忽然想要做一個NLP方向的事情,針對評論信息做點事情,第一步開始爬取評論信息,順帶就把B站的爬蟲開始做起來了。因爲事情稍微與標題有區別,將要重新寫一篇博客。

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