問題描述
這個問題確實讓我困擾了太長時間,今天花了半天時間,並沒有找到非常完滿的解決方案,只是在解決問題的過程中學會了一些其他知識,我最後還是要通過人工來判斷大致的移動距離,然後根據誤差做微調。大致做個總結了,並且認爲網站如果真心願意反爬蟲,完全可以處理到讓爬蟲無懈可擊。
今年四月份時B站的Geetest驗證碼大致情況可以通過https://blog.csdn.net/CY19980216/article/details/89074771中的知悉,當時檢查元素可以發現驗證圖片是由一塊塊div包裹着的切片,並且標籤中註明了該切片在原圖中對應的座標,這就對我們復原圖片並尋找缺塊位置帶來了極大的便利(事實上可以通過抓包拿到有缺塊的驗證圖片與無缺塊的驗證圖片)。
但是現在的情況是下面這樣的,驗證碼是一張canvas畫布👇
雖然在頁面源代碼中並不能找到驗證圖片的鏈接,但是我們通過瀏覽器監聽抓包可以發現有👇
其實抓包依然可以發現被切分並打亂的驗證圖片,並且在JS監聽抓包中可以看到這個圖片鏈接的響應👇
其實看到這裏我是以爲問題已經解決了,顯然是通過JS請求,Geetest響應了圖片鏈接URL,然後訪問圖片鏈接就可以拿到這張驗證圖片。
解決設想
首先在頁面源代碼中可以找到這段JS響應的請求URL,經過對頁源中各個script標籤中的檢測發現上圖返回了圖片鏈接URL的JS請求的URL如下圖所示(我將頁源複製到Notepad++中了):
紅框中的東西就是這個👇
https://api.geetest.com/get.php?is_next=true&type=slide3&gt=b6cc0fc51ec7995d8fd3c637af690de3&challenge=ac60b064d702e4f9cc4d728eeca30764&lang=zh-cn&https=false&protocol=https%3A%2F%2F&offline=false&product=embed&api_server=api.geetest.com&isPC=true&area=%23geetest-wrap&width=100%25&callback=geetest_1575204658911
這顯然是一個帶有查詢字符串的GET請求,它的查詢字符串大致是這樣的👇
要命的問題來了,可以看到這裏有一個chanllenge的字段,事實上這個URL只能被訪問一次(即瀏覽器訪問的這一次),二次訪問就會返回給你old challenge的響應(不信你把這段URL複製到瀏覽器裏看看),challenge字段起到驗證作用,且閱後即焚,是個一次性用品,也就是說這張圖片的鏈接URL只能被請求一次。
於是問題在於如何在利用selenium爬蟲時可以順帶監聽瀏覽器本身的抓包情況,這個便需要用到一個叫作browsermobproxy的庫,這個庫在安裝上有個坑點,首先pip install browsermobproxy後你還需要去Github上去把它的源碼下下來,因爲需要源碼裏面的一個bat文件用來操作java,這個庫具體用法Firefox用戶可以參考https://blog.csdn.net/m0_37618247/article/details/85066272,它可以協作selenium把瀏覽器的包以json格式保存下來,經過實驗,確實是可以拿到了圖片的鏈接的了。
但是我無法復原上面截圖中的那張打亂的圖片,於是我乾脆決定直接在網頁上進行canvas元素截圖。
元素截圖不是什麼難事,元素定位可以定位到它的左上角座標,再根據元素尺寸即可拿到元素的具體位置了。坑點在於截屏的尺寸往往與瀏覽器自身尺寸不一樣,即實際操作時需要將selenium截屏後的圖片進行放縮(我的放縮比例是1.25)
問題轉化爲如何尋找缺塊,我確實沒有能夠找到很好的辦法去處理缺塊,先後嘗試了多種方法(基於矩陣的運算,我先後測試了臨近兩列的列向量相似度變化曲線,缺塊),另外提一句利用selenium執行JS腳本可以從canvas中抓取到圖片👇
JS = 'return document.getElementsByClassName("geetest_canvas_slice geetest_absolute")[0].toDataURL("image/png");'
img_info = browser.execute_script(JS) # 執行js文件得到帶圖片信息的圖片數據
img_base64 = img_info.split(',')[1] # 拿到base64編碼的圖片信息
img_bytes = base64.b64decode(img_base64) # 轉爲bytes類型
with open("1.png","wb") as f:
f.write(img_bytes)
我看了其他人的方法都可以用該方法去找到原圖與有缺塊的圖(事實上頁面上有一個默認不展示display:none的canvas,它理論上包含着的是不含缺塊的原圖),但是這裏我只能拿到如下的圖👇
毫無用處。。。
最後用PIL庫中的ImageContrast方法進行圖片對比度增強,可以使得圖片中的缺塊變爲幾乎純黑,於是可以近似找到邊界👇
但是這個方法並不對所有的圖片都生效,比如下面這張👇
對比度增強後並不會出現黑團(不展示增強對比度後的圖片了,實在是太驚悚了,我可憐的2233娘)。
所以最後我也懶得處理這個問題了,我就直接用肉眼看一下大概缺塊的位置所佔整張圖片寬度的比例,然後手動輸入給程序了。。。確實比較蠢,但也不想動腦子了。
最後代碼大致是這樣的👇
def login_20191130(self,): # 用戶登錄(20190712更新)
"""
目前登錄方式相對於20190408的區別在於一下幾點:
- 次序上是先輸入用戶名密碼, 點擊登錄後纔會出現驗證碼圖片;
- 驗證碼圖片的元素結構變化, 沒有小切片因爲是一個canvas畫布標籤, 圖片鏈接仍然可以找到, 但是不在頁面源代碼中, 抓包可得, 抓包還能抓到geetest的返回結果;
- 滑動按鈕並未改變, 因此看起來是極驗自身升級了;
"""
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.maximize_window() # 窗口最大化
browser.get(self.loginURL) # 訪問登錄頁面
interval = 2. # 初始化頁面加載時間(如果頁面沒有加載成功,將無法獲取到下面的滑動驗證碼按鈕,林外我意外的發現有時候竟然不是滑動驗證,而是驗證圖片四字母識別,個人感覺處理滑動驗證更有意思)
while True: # 由於可能未成功加載,使用循環確保加載成功
browser.find_element_by_id("login-username").send_keys(self.username)
browser.find_element_by_id("login-passwd").send_keys(self.password)
time.sleep(interval)
browser.find_element_by_xpath("//a[@class='btn btn-login']").click()
xpath = "//canvas[@class='geetest_canvas_slice geetest_absolute']"
#WebDriverWait(browser,15).until(lambda driver: driver.find_element_by_xpath(xpath).is_displayed())
try:
time.sleep(interval) # 等待加載
canvas = browser.find_element_by_xpath(xpath)
except:
browser.refresh() # 刷新頁面
interval += .5 # 每失敗一次讓interval增加0.5秒
print("驗證圖片加載失敗!頁面等待時間更新爲{}".format(interval))
continue
browser.save_screenshot("{}/screen.png".format(self.tempFolder))
location = canvas.location
size = canvas.size
left = location["x"] # 獲取圖片左邊界
top = location["y"] # 獲取圖片上邊界
right = left + size["width"] # 獲取圖片右邊界
bottom = top + size["height"] # 獲取圖片下邊界
image = Image.open("{}/screen.png".format(self.tempFolder))
image = image.crop(( # 縮放及對右下邊界要多取一格像素, python向來都是包左不包右
left*self.layout,
top*self.layout,
right*self.layout+1,
bottom*self.layout+1
))
image.save("{}/canvas.png".format(self.tempFolder)) # 保存圖片
break
while True:
div = browser.find_element_by_class_name("geetest_slider_button")
ActionChains(browser).click_and_hold(on_element=div).perform()
ratio = input("請輸入肉眼可見的比例: ")
xoffset = 260*float(ratio) # 尋找缺塊位置的橫座標
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)
html = browser.page_source
time.sleep(1.)
soup = BeautifulSoup(html,"lxml")
title = soup.find("title")
if str(title.string[4])=="彈":
print("登錄失敗!準備重新登錄!")
else:
print("登錄成功!")
return browser
這是個類函數,原類在https://blog.csdn.net/CY19980216/article/details/89074771
總之這次經歷之後我覺得只要WEB開發者願意,絕對可以做到讓你什麼東西也拿不到。
推薦一篇別人做成功的https://blog.csdn.net/z434890/article/details/93631888,但是我用這個方法確實不管用,唉算了,沒必要跟自己過不去。
分享學習,共同進步!
20191202更新
好吧,我承認是我蠢了,頁面上其實有三個canvas,其中一個是默認不展示的(即原圖),一個是展示的有缺塊的圖,一個是滑塊的圖,我之所以用browser.execute_script()拿到的只有滑塊是我太蠢了,這樣問題其實就轉化爲前一篇模擬登錄裏面的情況了,很容易就可以找到邊界了:
重新附上代碼
def login_20191202(self,):
"""
頁面上有三個canvas, 原圖與缺塊圖都可以很容易拿到的;
這三個canvas的class屬性分別爲:
"geetest_canvas_fullbg geetest_fade geetest_absolute" # 無缺塊的圖片
"geetest_canvas_slice geetest_absolute" # 滑塊圖片
"geetest_canvas_bg geetest_absolute" # 有缺塊的圖片
問題由此轉化爲20190408的情況;
"""
def download_verifying_picture(canvas_class,name): # 下載滑動驗證圖片(從canvas標籤中轉化得到)
JS = 'return document.getElementsByClassName("{}")[0].toDataURL("image/png");'.format(canvas_class)
image_data = browser.execute_script(JS) # 執行js代碼得到帶圖片信息的圖片數據
image_base64 = image_data.split(",")[1] # 摘取base64編碼的圖片信息
image_bytes = base64.b64decode(image_base64) # 將base64代碼轉爲bytes類型
with open("{}\\{}\\{}.png".format(self.workspace,self.tempFolder,name),"wb") as f: f.write(image_bytes)
def find_block_space(width=64,zoo=1.0,plot=True): # 尋找缺塊位置(默認參數width爲缺塊的列寬像素,zoo這邊用1.15基本上大概率能過了,但是我查了一下兩個圖片的屬性應該是1.2,設爲1.2應該要改常數項了)
image1 = numpy.asarray(Image.open("{}\\{}\\1.png".format(self.workspace,self.tempFolder)))
image2 = numpy.asarray(Image.open("{}\\{}\\2.png".format(self.workspace,self.tempFolder)))
Xaxis,Yaxis,Zaxis = image1.shape # 獲取圖片三維信息(160×15×3)
errors = [] # 記錄260列寬上每個列向量的非相似度值
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.maximize_window() # 窗口最大化
browser.get(self.loginURL) # 訪問登錄頁面
interval = 2. # 初始化頁面加載時間(如果頁面沒有加載成功,將無法獲取到下面的滑動驗證碼按鈕,林外我意外的發現有時候竟然不是滑動驗證,而是驗證圖片四字母識別,個人感覺處理滑動驗證更有意思)
class_bg = "geetest_canvas_bg geetest_absolute"
class_fullbg = "geetest_canvas_fullbg geetest_fade geetest_absolute"
xpath_slice = "//canvas[@class='geetest_canvas_slice geetest_absolute']"
xpath_bg = "//canvas[@class='{}']".format(class_bg)
xpath_fullbg = "//canvas[@class='{}']".format(class_fullbg)
while True: # 由於可能未成功加載,使用循環確保加載成功
browser.find_element_by_id("login-username").send_keys(self.username)
browser.find_element_by_id("login-passwd").send_keys(self.password)
time.sleep(interval)
browser.find_element_by_xpath("//a[@class='btn btn-login']").click()
try: # 我擔心加載不出來
time.sleep(interval) # 等待加載
canvas = browser.find_element_by_xpath(xpath_slice)
break
except: # 驗證圖片canvas未加載出來
browser.refresh() # 刷新頁面
interval += .5 # 每失敗一次讓interval增加0.5秒
print("驗證圖片加載失敗!頁面等待時間更新爲{}".format(interval))
continue
html = browser.page_source
soup = BeautifulSoup(html,"lxml")
download_verifying_picture(class_bg,1) # 有缺塊
download_verifying_picture(class_fullbg,2) # 無缺塊
xoffset = find_block_space() # 尋找缺塊位置的橫座標
tracks = get_track(xoffset) # 生成軌跡
div = browser.find_element_by_class_name("geetest_slider_button")
ActionChains(browser).click_and_hold(on_element=div).perform()
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)
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