一、雪碧圖
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!