爬蟲反爬系列之破解雪碧圖反爬
一、雪碧圖
1.前言
我們都知道,HTTP 協議即超文字傳輸協議,是 Web 應用的基礎,HTTP 協議又是基於 TCP 協議的,而 TCP 連線的建立是需要時間和資源的。當網頁載入時,會需要下載圖片資源,如果有非常多的小圖片,就需要建立很多 TCP 連線。
但勤勞勇敢的前端工作者們,想到把所有小圖片放到一張圖片裡面去,這樣就可以通過一次 TCP 連線,下載所有的小圖片,再通過前端的奇技淫巧,來展示正確的圖片。這種由很多小圖片組成的圖片,被稱為雪碧圖,雪碧圖在節約 TCP 連線的同時,也為爬取帶來了難度。
2.定義
CSS 雪碧圖即 CSS Sprite,也被稱為 CSS 精靈,是一種 CSS 影象合成技術,該技術是將小圖示和背景影象合成到一張圖片中,然後利用 CSS 的背景定位來顯示需要顯示的影象部分。
CSS 雪碧的基本原理是把你的網站上用到的一些圖片整合到一張單獨的圖片中,然後使用 CSS 中的 background 和 background-position 屬性進行渲染,但當圖片數量更多更復雜時,定位就需要更加準確,可能就會用到更多的數值來到達更準的定位。
3.雪碧圖的優缺點
通過前面的描述,可以很清楚地知道雪碧圖有如下優點:
- 減少載入網頁圖片時對伺服器的請求次數,降低伺服器壓力,同時提高了頁面的載入速度,節約伺服器的流量;
- 減少圖片載入所需要的時間,提高頁面的載入速度。
除了上述優點,雪碧圖還有一些無法避免的缺點:
- 雪碧圖的最大問題是記憶體使用,因為雪碧圖中會有大量的留白空間;
- 影響瀏覽器的縮放功能,如果使用雪碧圖的頁面縮放了,就需要做一些額外的工作來糾正圖片邊緣;
- 拼圖維護比較麻煩,無論是拼圖合成,還是修改圖片,都會很麻煩不便於操作;
- 使 CSS 的編寫變得困難,尤其是當圖片數量較多時,會大大增加 CSS 的程式碼量和複雜度。
二、破解例項
1.站點分析
該站點的連結為:http://www.glidedsky.com/level/web/crawler-sprite-image-1。
開啟網站,開啟開發者工具,選擇檢視網頁上的數字,發現這些數字其實都是 div,通過 CSS 來顯示圖片:
再檢視該元素的 CSS 樣式,發現除了寬高之外還有 background-position-x 屬性,該屬性就是用來控制顯示的數字。除此之外,我們可以看到 sprite 類裡面定義了一個背景圖,開啟連結後發現這個背景圖如下:
每次載入網頁時,都會下載類似上圖的包含0-9十個數字的背景圖,再通過 CSS 樣式中的 background-position-x 屬性來顯示所需要的數字。
2.破解思路
要應對這種使用雪碧圖來實現反爬的措施,有如下思路:
1)獲取所有 background-position-x 並求對應的數字
因為每個數字對應的 background-position-x 的數值是一樣的,所以思路一是獲取所有 background-position-x 的數值,再求集合,並根據數值從小到大排列,每個數值就對應一個數字。但這種思路是有問題的,例如上面的截圖中並沒有數字0,也就沒有數字0對應的位置,這就會導致我們獲取到的資料是不完整的,也就無法正確表示了。
2)下載圖片,根據 background-position-x 的值進行劃分
首先是將圖片下載下來,並計算出圖片的大小,然後根據從 CSS 樣式中獲取到的 background-position-x 的數值進行劃分,就能得到每個數字對應的位置區間。但這種思路也無法解決數字不全所帶來的問題,尤其是數字是扭曲的,大小也不一樣。
3)下載圖片,估算每個數字所佔的寬度
將圖片下載下來,得到圖片的寬度,因為每個數字的寬度其實是差不多的,所以我們可以簡單地將圖片的寬度除以10來估算每個數字的寬度,再用 background-position-x 的值和這個寬度進行整數除法,就能得到對應的數字了。使用這種方法,即使數字是不全的,也能夠計算出來。
三、破解步驟
1.下載圖片
前面已經說過在 sprite 類中指明瞭背景圖片,截圖如下:
在上面的 url() 中,data 表示取得資料的協定名稱,image/png 是資料型別名稱,base64 是資料的編碼方法,逗號後面就是這個 image/png 檔案 base64 編碼後的資料。使用這種方式就把影象檔案的內容直接寫在了 HTML 檔案中,這樣做的好處是,節省了一個 HTTP 請求。
要將這個圖片下載下來,首先要做的就是得到這個使用 base64 編碼後的資料,可以使用正則表示式進行匹配,然後進行解碼,再將圖片下載到本地,開啟並得到該圖片的寬度。下載 base64 編碼圖片的程式碼如下:
1 def save_img(img_data): 2 """ 3 save image in local directory 4 :param img_data: image base64 data 5 :return: width of image 6 """ 7 img = base64.urlsafe_b64decode(img_data) 8 filename = "{}.{}".format(uuid.uuid4(), "png") 9 filepath = os.path.join("./Images", filename) 10 with open(filepath, "wb") as f: 11 f.write(img) 12 image = Image.open(filepath) 13 return image.width
2.獲取位置-數字字典
我們可以知道 background-position-x 都定義在 CSS 程式碼中了,要獲取所有 background-position-x 的數值,可以使用正則表示式 re 模組中的 findall() 方法進行匹配,使用方法如下:
re.findall(r"background-position-x:-?(\d+)?px", html)
前面已經得到圖片寬度了,除以10的結果就可以當做每個數字所佔的寬度,再用 background-position-x 的數值和這個寬度進行整數除法,得到的結果就是這個 CSS 所對應的數字。為了方便後面將數字進行組合,還要轉換成 str 形式。具體程式碼如下:
1 def parse(num_list: list, gap: int): 2 """ 3 translate position to digit 4 :param num_list: number list 5 :param gap: average gap between numbers 6 :return: 7 """ 8 return {str(num): str(int(num // gap)) for num in num_list}
3.獲取數字並求和
在示例的網址中有十二個三位數,也就有三十六個數字,我們要做的就是獲取每個數字的 CSS 類名,再根據這個類名得到 background-position-x 的數值,再根據前面得到的字典就能得到每個數字,將三個數字組成一個三位數,最後使用 sum() 方法進行求和。
下面就是獲取每個數字並進行求和的程式碼,其中 pos_dict 就是位置和數字對應的字典:
1 def get_digits(html, pos_dict): 2 """ 3 get digit according to the class and sum up the numbers 4 :param html: html 5 :param pos_dict: position to digit 6 :return: 7 """ 8 et = etree.HTML(html) 9 pos_classes = et.xpath('//*[@id="app"]/main/div[1]/div/div/div/div/div/@class') 10 digits, d = [], "" 11 for pos in pos_classes: 12 if len(d) == 3: 13 digits.append(d) 14 d = "" 15 pos_x = re.findall(pos.split(" ")[0] + r" { background-position-x:-?(\d+?)px }", html) 16 d = d + pos_dict[pos_x[0]] 17 digits.append(d) 18 result = sum([int(i) for i in digits]) 19 print("The result is : {}".format(result))
完整程式碼已上傳到 GitHub!&n