SVG 映射反爬蟲 SVG 是用於描述二維矢量圖形的一種圖形格式。它基於 XML 描述圖形,對圖形進行放大或縮小操作都不會影響圖形質量。矢量圖形的這個特點使得它被廣泛應用在 Web 網站中。 接下來我們要瞭解的反爬蟲手段正是利用 SVG 實現的,這種反爬蟲手段用矢量圖形代替具體的文字,不會影響用戶正 ...
SVG 映射反爬蟲
SVG 是用於描述二維矢量圖形的一種圖形格式。它基於 XML 描述圖形,對圖形進行放大或縮小操作都不會影響圖形質量。矢量圖形的這個特點使得它被廣泛應用在 Web 網站中。
接下來我們要瞭解的反爬蟲手段正是利用 SVG 實現的,這種反爬蟲手段用矢量圖形代替具體的文字,不會影響用戶正常閱讀,但爬蟲程式卻無法像讀取文字那樣獲得 SVG 圖形中的內容。由於 SVG 中的圖形代表的也是一個個文字,所以在使用時必須在後端或前端將真實的文字與對應的 SVG 圖形進行映射和替換,這種反爬蟲手段被稱為 SVG 映射反爬蟲。
6.3.1 SVG 映射反爬蟲繞過實戰
示例 6:SVG 映射反爬蟲示例。
網址:http://www.porters.vip/confusion/food.html。
任務:爬取美食商家評價網站頁面中的商家聯繫電話、店鋪地址和評分數據,頁面內容如圖 6-15
所示。
圖 6-15 示例 6 頁面
在編寫 Python 代碼之前,我們需要確定目標數據的元素定位。在定位過程中,發現一個與以往不同的現象:有些數字在 HTML 代碼中並不存在。例如口味的評分數據,其元素定位如圖 6-16 所示。
圖 6-16 評分數據中口味分數元素定位
根據頁面顯示內容,HTML 代碼中應該是 8.7 才對,但實際上我們看到的卻是:
1 | <span class="item">口味:<d class="vhkjj4"></d>.7</span> |
HTML 代碼中有數字 7 和小數點,但沒有 8 這個數字,似乎數字 8 的位置被 d 標簽占據。而商家電話號碼處的顯示就更奇怪了,一個數字都沒有。商家電話對應的 HTML 代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 | <div class="col more"> 電話: <d class="vhkbvu"></d> <d class="vhk08k"></d> <d class="vhk08k"></d> <d class="">-</d> <d class="vhk84t"></d> <d class="vhk6zl"></d> <d class="vhkqsc"></d> <d class="vhkqsc"></d> <d class="vhk6zl"></d> </div> |
包含很多的 d 標簽,難道它使用 d 標簽進行占位,然後用元素進行覆蓋嗎?我們可以將 d 標簽的數量和數字的數量進行對比,發現它們的數量是相同的,也就是說一對 d 標簽代表一個數字。
每一對 d 標簽都有 class 屬性,有些 class 屬性值是相同的,有些則不同。我們再將 class 屬性值與數字進行對比,看一看能否找到規律,如圖 6-17 所示。
圖 6-17 class 屬性值和數字的對比
從圖 6-17 中可以看出,class 屬性值和數字是一一對應的,如屬性值 vhk08k 與數字 0 對應。根據這個線索,我們可以猜測每個數字都與一個屬性值對應,對應關係如圖 6-18 所示。
圖 6-18 數字與屬性值對應關係
瀏覽器在渲染頁面的時候就會按照這個對應關係進行映射,所以頁面中顯示的是數字,而我們在 HTML 代碼中看到的則是這些 class 屬性值。瀏覽器在渲染時將 HTML 中的 d 標簽與數字按照此關係進行映射,並將映射結果呈現在頁面中。映射邏輯如圖 6-19 所示。
圖 6-19 映射邏輯
我們的爬蟲代碼可以按照同樣的邏輯實現映射功能,在解析 HTML 代碼時將 d 標簽的 class 屬性值取出來,然後進行映射即可得到頁面中顯示的數字。如何在爬蟲代碼中實現映射關係呢?實際上網頁中使用的是“屬性名數字”這種結構,Python 中內置的字典正好可以滿足我們的需求。我們可以用 Python 代碼測試一下,代碼如下:
1 2 3 4 5 6 7 8 9 | # 定義映射關係 mappings = {'vhk08k': 0, 'vhk6zl': 1, 'vhk9or': 2, 'vhkfln': 3, 'vhkbvu': 4, 'vhk84t': 5, 'vhkvxd': 6, 'vhkqsc': 7, 'vhkjj4': 8, 'vhk0f1': 9} # HTML 中得到的屬性值 html_d_class = 'vhkvxd' # 將映射後的結果列印輸出 print(mappings.get(html_d_class)) |
這段代碼的邏輯是:首先定義屬性值與數字的映射關係,然後假設一個 HTML 中 d 標簽的屬性值,接著將這個屬性值的映射結果列印出來。代碼運行後得到的結果為:
1 | 6 |
運行結果說明映射這種方法是可行的。接著我們試一試將商家的聯繫電話映射出來:
1 2 3 4 5 6 7 8 9 10 11 12 13 | # 定義映射關係 mappings = {'vhk08k': 0, 'vhk6zl': 1, 'vhk9or': 2, 'vhkfln': 3, 'vhkbvu': 4, 'vhk84t': 5, 'vhkvxd': 6, 'vhkqsc': 7, 'vhkjj4': 8, 'vhk0f1': 9} # 商家聯繫電話 class 屬性 html_d_class = ['vhkbvu', 'vhk08k', 'vhk08k', '', 'vhk84t', 'vhk6zl', 'vhkqsc', 'vhkqsc', 'vhk6zl'] phone = [mappings.get(i) for i in html_d_class] # 將映射後的結果列印輸出 print(phone) |
運行結果為:
1 | [4, 0, 0, None, 5, 1, 7, 7, 1] |
我們使用映射的方法得到了商家聯繫電話,說明 SVG 映射反爬蟲已經被我們繞過了。
6.3.2 大眾點評反爬蟲案例
這種映射手段不僅僅出現在本書的示例中,在大型網站中也有應用。大眾點評是中國領先的本地生活信息及交易平臺,也是全球最早建立的獨立第三方消費點評網站。大眾點評不僅為用戶提供商戶信息、消費點評及消費優惠等信息服務,同時提供團購、餐廳預訂、外賣和電子會員卡等 O2O(Online To Offline)交易服務。大眾點評網站也使用了映射型反爬蟲手段,打開瀏覽器並訪問 https://www.dianping.com/shop/14741057,頁面如圖 6-20 所示。
圖 6-20 大眾點評商家信息頁
大眾點評的商家信息頁主要用於展示消費者對商家的各項評分、商家電話、店鋪地址和推薦菜品等。我們可以看一看商家電話或評分的 HTML 代碼,如圖 6-21 所示。
圖 6-21 商家電話 HTML 代碼
大眾點評中的商家號碼並不是全部使用 d 標簽代替,其中有部分使用了數字。但是仔細觀察一下就可以發現商家號碼的數量等於 d 標簽數量加上數字的數量,說明 d 標簽的 class 屬性值與數字也有可能是一一對應的映射關係。感興趣的同學可以使用示例 6 中的方法,嘗試映射大眾點評案例中的數字。
如果這種手段的繞過方法這麼簡單的話,那麼它早就被淘汰了,為什麼連大眾點評這樣的大型網站都會使用呢?我們繼續往下看,大眾點評的商家營業時間部分的 HTML 代碼如圖 6-22 所示。
圖 6-22 大眾點評商家營業時間
除了剛纔的數字映射之外,大眾點評還對中文進行了映射。此時如果按照示例 6 中人為地將 class 值和對應的文字進行映射的話,就非常麻煩了。試想一下,如果網頁中所有的文字都使用這種映射反爬蟲的手段,那麼爬蟲工程師要如何應對呢?對所有用到的文字進行映射嗎?
這不可能做到,其中要完成映射的包括 10 個數字、26 個英文字母和幾千個常用漢字。而且目標網站一旦更改文字的對應關係,那麼爬蟲工程師就需要重新映射所有文字。面對這樣的問題,我們必須找到文字映射規律,並且能夠使用 Python 語言實現映射演算法。如此一來,無論目標網站文字映射的對應關係如何變化,我們都能夠使用這套映射演算法得到正確的結果。
這種映射關係在網頁中是如何實現的呢?是使用 JavaScript 在頁面中定義數組嗎?還是非同步請求API 拿到 JSON 數據?這都有可能,接下來我們就去尋找答案。
6.3.3 SVG 反爬蟲原理
映射關係不可能憑空出現,一定使用了某種技術特性。HTML 中與標簽 class 屬性相關的只有 JavaScript 和 CSS。根據這個線索,我們需要繼續對示例 6 進行分析。案例中商家電話的 HTML 代碼為:
1 2 3 4 5 6 7 8 9 10 11 | <div class="col more">電話: <d class="vhkbvu"></d> <d class="vhk08k"></d> <d class="vhk08k"></d> <d class="">-</d> <d class="vhk84t"></d> <d class="vhk6zl"></d> <d class="vhkqsc"></d> <d class="vhkqsc"></d> <d class="vhk6zl"></d> </div> |
我們可以隨意選擇一對 d 標簽,然後觀察它對應的 CSS 樣式有沒有可以深入分析的線索,如果沒有線索再看 JavaScript。 d 標簽的 CSS 樣式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | d[class^="vhk"] { width: 14px; height: 30px; margin-top: -9px; background-image: url(../font/food.svg); background-repeat: no-repeat; display: inline-block; vertical-align: middle; margin-left: -6px; } .vhkqsc { background: -288.0px -141.0px; } |
d 標簽樣式看上去沒有什麼特別之處,只是設置了 background 屬性的坐標值。但是上方 d 標簽的公共樣式中設置了背景圖片,我們可以複製背景圖片的地址,在瀏覽器的新標簽頁中打開,d 標簽背景圖如圖 6-23 所示。
圖 6-23 標簽背景圖
d 標簽的背景圖中全部都是數字,這些無序的數字共有 4 行。但這好像不是一張大圖片,我們查看該圖片頁面的源代碼,內容如圖 6-24 所示。
圖 6-24 圖片頁面源代碼
源代碼中前兩行表明這是一個 SVG 文件,該文件中使用 text 標簽定義文本, style 標簽用於設置文本樣式, text 標簽定義的文本正是圖片頁面顯示的數字。難道這些無序的數字就是我們在頁面中看到的電話號碼和評分數字?
除了 class 屬性值為 vhkbvu 的 d 標簽,其他標簽也使用了這個的 CSS 樣式,但每對 d 標簽的坐標定位都不同。它們的坐標定位如下:
1 2 3 4 5 6 7 8 9 | .vhkbvu { background: -386px -97px; } .vhk08k { background: -274px -141px; } .vhk84t { background: -176px -141px; } |
坐標是定位數字的關鍵,要想知道坐標的計算方法,必須瞭解一些關於 SVG 的知識。
在本節開始的時候,我們簡單地瞭解了 SVG 的概念,知道 SVG 是基於 XML 的。實際上它是用文本格式的描述性語言來描述圖像內容的,因此 SVG 是一種與圖像解析度無關的矢量圖形格式。打開文本編輯器,併在新建的文件中寫入以下內容:
1 2 3 4 5 6 7 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/ DTD/svg11.dtd"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/ 1999/xlink" width="250px" height="250.0px"> <text x='10' y='30'>hello,world</text> </svg> |
將該文件保存為 test.svg,然後使用瀏覽器打開 test.svg 文件,顯示內容如圖 6-25 所示。
圖 6-25 test.svg 顯示內容
代碼前 3 行聲明文件類型,第 4 行~第 5 行定義了 SVG 內容塊和畫布寬高,第 6 行使用 text 標簽定義了一段文本並指定了文本的坐標。這段文本就是我們在瀏覽器中看到的內容,而代碼中的 x 坐標和 y 坐標則用於確定該文本在畫布中的位置,坐標規則如下。
- 以頁面的左上角為零坐標點,即坐標值為 (0, 0)。
- 坐標以像素為單位。
- x 軸的正方向為從左到右,y 軸的正方向是從上到下。
- n 個字元可以有 n 個位置參數。
如果字元數量大於位置參數數量,那麼沒有位置參數的字元將以最後一個位置參數為零坐標點,並按原文順序排列。
看上去並不是很好理解,我們可以通過修改代碼來理解坐標軸的定義。首先是 x 軸, text 標簽中的 x 代表列表字元在頁面中的 x 軸位置,test.svg 中的 x 值為 10,現在我們將其設為 0 ,保存後刷新網頁,頁面內容如圖 6-26 所示。
圖 6-26 x 為 0 時的 test.svg 顯示內容
x 的值為 0 時,文本緊貼瀏覽器左側。而 x 的值為 10 時,文本距離瀏覽器左側有一定的距離,這說明 x 的值能夠決定文字所在的位置。現在我們將代碼中 x 對應的值改為“10 50 30 40 20 60”(註意這裡特意將第 2個數字 20與第 5個數字互換了位置),這樣做是為了設定前 6個字元的坐標位置。
此時,第 1 個字元的位置參數為 10,第 2 個字元的位置參數為 50,第 3 個字元的位置參數為 30,以此類推,頁面中正常顯示的文字順序應該是:
1 | holle,world |
但是由於我們調換了第 2 個字元和第 5 個字元的位置參數,即字母 e 和字母 o 的位置互換,如圖 6-27
所示。
圖 6-27 設定多個 x 值的 svg
圖 6-27 中文字順序與我們猜測的順序是一樣的,這說明 SVG 中每個字元都可以有自己的 x 軸坐標值。y 與 x 同理,每個字元都可以有自己的 y 軸坐標值。雖然我們只設定了 6 個位置參數, svg 中的字元卻有 11 個,但沒有設定位置參數的字元依然能夠按照原文順序排序。在瞭解 SVG 基本知識之後,我們回頭看一下案例中所使用的 SVG 文件中坐標參數的設定,圖 6-23 中的字元與圖 6-24 圖片頁源代碼中的字元一一對應,且每個字元都設定了 x 軸的位置參數,而 y 軸則只有 1 個值。
在瞭解位置參數之後,我們還需要弄清楚字元定位的問題。瀏覽器根據 CSS 樣式中設定的坐標和元素寬高來確定 SVG 中對應數字。x 軸的正方向為從左到右,y 軸的正方向是從上到下,如圖 6-28 所示。
圖 6-28 SVG x 軸和 y 軸與位置參數的關係
而 CSS 樣式中的 x 軸與 y 軸是相反的,也就是說 CSS 樣式中 x 軸是負數向右的,y 軸是負數向下的,如圖 6-29 所示。
圖 6-29 CSS x 軸和 y 軸與位置參數的關係
所以當我們需要在 CSS 中定位 SVG 中的字元位置時,需要用負數表示。我們可以通過一個例子來理解它們的關係,現在需要在 CSS 中定點陣圖 6-30 中第 1 行的第 1 個字元的中心點。
圖 6-30 SVG
假設字元大小為 14 px,那麼 SVG 的計算規則如下。
- 字元在x軸中心點的計算規則為:字元大小除以2,再加字元的x軸起點位置參數,即14÷2+0 等於 7。
- 字元在 y 軸中心點的計算規則為:y 軸高度減字元 y 軸起點減字元大小,其值除以 2 後加上字元 y 軸起點位置參數,最後再加上字元大小數值的一半,即(38−0−14)÷2+0+7 等於 19。
最後得到 SVG 的坐標為:
1 | x='7' y='19' |
CSS 樣式的 x 軸和 y 軸與 SVG 是相反的,所以 CSS 樣式中對該字元的定位為:
1 | -7px -19px |
這樣就能夠定位到指定字元的中心點了。但是如果要在 HTML 頁面中完整顯示該字元,那麼還需要為 HTML 中對應的標簽設置寬高樣式,如:
1 2 | width: 14px; height: 30px; |
在瞭解了 SVG 與 CSS 樣式的關聯關係後,我們就能夠根據 CSS 樣式映射出 SVG 中對應的字元。
在實際場景中,我們需要讓程式能夠自動處理 CSS 樣式和 SVG 的映射關係,而不是人為地完成這些
工作。以示例 6 中的 SVG 和 CSS 樣式為例,假如我們需要用 Python 代碼實現自動映射功能,首先我
們就需要拿到這兩個文件的 URL,如:
1 2 | url_css = 'http://www.porters.vip/confusion/css/food.css' url_svg = 'http://www.porters.vip/confusion/font/food.svg' |
還有需要映射的 HTML 標簽的 class 屬性值,如:
1 | css_class_name = 'vhkbvu' |
接下來使用 Requests 庫向 URL 發出請求,拿到文本內容。對應代碼如下: