內容選自即將出版的《Python3 反爬蟲原理與繞過實戰》,本次公開書稿範圍爲第 6 章——文本混淆反爬蟲。本篇爲第 6 章中的第 3 小節 SVG 反爬蟲,第 4 小節《用前考慮清楚,傷敵一千自損八百的字體反爬蟲》已發,其他小節將逐步放送。css
SVG 是用於描述二維矢量圖形的一種圖形格式。它基於 XML 描述圖形,對圖形進行放大或縮小操做都不會影響圖形質量。矢量圖形的這個特色使得它被普遍應用在 Web 網站中。html
接下來咱們要了解的反爬蟲手段正是利用 SVG 實現的,這種反爬蟲手段用矢量圖形代替具體的文字,不會影響用戶正常閱讀,但爬蟲程序卻沒法像讀取文字那樣得到 SVG 圖形中的內容。因爲 SVG 中的圖形表明的也是一個個文字,因此在使用時必須在後端或前端將真實的文字與對應的 SVG 圖形進行映射和替換,這種反爬蟲手段被稱爲 SVG 映射反爬蟲。前端
示例 6:SVG 映射反爬蟲示例。算法
網址:www.porters.vip/confusion/f…編程
任務:爬取美食商家評價網站頁面中的商家聯繫電話、店鋪地址和評分數據,頁面內容如圖 6-15後端
所示。數組
圖 6-15 示例 6 頁面瀏覽器
在編寫 Python 代碼以前,咱們須要肯定目標數據的元素定位。在定位過程當中,發現一個與以往不一樣的現象:有些數字在 HTML 代碼中並不存在。例如口味的評分數據,其元素定位如圖 6-16 所示。bash
圖 6-16 評分數據中口味分數元素定位app
根據頁面顯示內容,HTML 代碼中應該是 8.7 纔對,但實際上咱們看到的倒是:
<span class="item">口味:<d class="vhkjj4"></d>.7</span>
複製代碼
HTML 代碼中有數字 7 和小數點,但沒有 8 這個數字,彷佛數字 8 的位置被 d 標籤佔據。而商家電話號碼處的顯示就更奇怪了,一個數字都沒有。商家電話對應的 HTML 代碼以下:
<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 代碼測試一下,代碼以下:
# 定義映射關係
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 標籤的屬性值,接着將這個屬性值的映射結果打印出來。代碼運行後獲得的結果爲:
6
複製代碼
運行結果說明映射這種方法是可行的。接着咱們試一試將商家的聯繫電話映射出來:
# 定義映射關係
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)
複製代碼
運行結果爲:
[4, 0, 0, None, 5, 1, 7, 7, 1]
複製代碼
咱們使用映射的方法獲得了商家聯繫電話,說明 SVG 映射反爬蟲已經被咱們繞過了。
這種映射手段不只僅出如今本書的示例中,在大型網站中也有應用。大衆點評是中國領先的本地生活信息及交易平臺,也是全球最先創建的獨立第三方消費點評網站。大衆點評不只爲用戶提供商戶信息、消費點評及消費優惠等信息服務,同時提供團購、餐廳預訂、外賣和電子會員卡等 O2O(Online To Offline)交易服務。
大衆點評網站也使用了映射型反爬蟲手段,打開瀏覽器並訪問 www.dianping.com/shop/147410… 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 數據?這都有可能,接下來咱們就去尋找答案。
映射關係不可能憑空出現,必定使用了某種技術特性。HTML 中與標籤 class 屬性相關的只有 JavaScript 和 CSS。根據這個線索,咱們須要繼續對示例 6 進行分析。案例中商家電話的 HTML 代碼爲:
<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 樣式以下:
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 標籤的座標定位都不一樣。它們的座標定位以下:
.vhkbvu {
background: -386px -97px;
}
.vhk08k {
background: -274px -141px;
}
.vhk84t {
background: -176px -141px;
}
複製代碼
座標是定位數字的關鍵,要想知道座標的計算方法,必須瞭解一些關於 SVG 的知識。
在本節開始的時候,咱們簡單地瞭解了 SVG 的概念,知道 SVG 是基於 XML 的。實際上它是用文本格式的描述性語言來描述圖像內容的,所以 SVG 是一種與圖像分辨率無關的矢量圖形格式。打開文本編輯器,並在新建的文件中寫入如下內容:
<?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 座標則用於肯定該文本在畫布中的位置,座標規則以下。
若是字符數量大於位置參數數量,那麼沒有位置參數的字符將以最後一個位置參數爲零座標點,並按原文順序排列。
看上去並非很好理解,咱們能夠經過修改代碼來理解座標軸的定義。首先是 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,以此類推,頁面中正常顯示的文字順序應該是:
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 的計算規則以下。
最後獲得 SVG 的座標爲:
x='7' y='19'
複製代碼
CSS 樣式的 x 軸和 y 軸與 SVG 是相反的,因此 CSS 樣式中對該字符的定位爲:
-7px -19px
複製代碼
這樣就可以定位到指定字符的中心點了。可是若是要在 HTML 頁面中完整顯示該字符,那麼還須要爲 HTML 中對應的標籤設置寬高樣式,如:
width: 14px;
height: 30px;
複製代碼
在瞭解了 SVG 與 CSS 樣式的關聯關係後,咱們就可以根據 CSS 樣式映射出 SVG 中對應的字符。
在實際場景中,咱們須要讓程序可以自動處理 CSS 樣式和 SVG 的映射關係,而不是人爲地完成這些
工做。以示例 6 中的 SVG 和 CSS 樣式爲例,假如咱們須要用 Python 代碼實現自動映射功能,首先我
們就須要拿到這兩個文件的 URL,如:
url_css = 'http://www.porters.vip/confusion/css/food.css'
url_svg = 'http://www.porters.vip/confusion/font/food.svg'
複製代碼
還有須要映射的 HTML 標籤的 class 屬性值,如:
css_class_name = 'vhkbvu'
複製代碼
接下來使用 Requests 庫向 URL 發出請求,拿到文本內容。對應代碼以下:
import requests
css_resp = requests.get(url_css).text
svg_resp = requests.get(url_svg).text
複製代碼
提取 CSS 樣式文件中標籤屬性對應的座標值,這裏使用正則進行匹配便可。對應代碼以下:
import re
pile = '.%s{background:-(\d+)px-(\d+)px;}' % css_class_name
pattern = re.compile(pile)
css = css_resp.replace('\n', '').replace(' ', '')
coord = pattern.findall(css)
if coord:
x, y = coord[0]
x, y = int(x), int(y)
複製代碼
此時獲得的座標值是正數,能夠直接用於 SVG 字符定位。定位前咱們要先拿到 SVG 中全部 text 標籤的 Element 對象:
from parsel import Selector
svg_data = Selector(svg_resp)
texts = svg_data.xpath('//text')
複製代碼
而後獲取全部 text 標籤中的 y 值,接着咱們將上一步獲得的 Element 對象進行循環取值便可:
axis_y = [i.attrib.get('y') for i in texts if y <= int(i.attrib.get('y'))][0]
複製代碼
獲得 y 值後就能夠開始字符定位了。要注意的是,SVG 中 text 標籤的 y 值與 CSS 樣式中獲得的 y 值並不須要徹底相等,由於樣式能夠隨意調整,好比 CSS 樣式中-90 和-92 對於 SVG 的定位來講並無什麼差異,因此咱們只須要知道具體是哪個 text 便可。
那麼如何肯定是哪個 text呢?
咱們能夠用排除法來肯定,假如當前 CSS 樣式中的 y 值是-97,那麼在 SVG 中 text 的 y 值就不可能小於 97,咱們只須要取到比 97 大且最相近的 text 標籤 y 值便可。好比當前 SVG 全部 text 標籤的 y 值爲:
[38, 83, 120, 164]
複製代碼
那麼大於 97 且最相近的是 120。將這個邏輯轉化爲代碼:
axis_y = [i.attrib.get('y') for i in texts if y <= int(i.attrib.get('y'))][0]
複製代碼
獲得 y 值後就能夠肯定具體是哪一個 text 標籤了。對應代碼以下:
svg_text = svg_data.xpath('//text[@y="%s"]/text()' % axis_y).extract_first()
複製代碼
接下來須要確認 SVG 中的文字大小,也就是須要找到 font-size 屬性的值。對應代碼以下:
font_size = re.search('font-size:(\d+)px', svg_resp).group(1)
複製代碼
獲得 font-size 的值後,咱們就能夠定位具體的字符了。x 軸有多少個字符呢?剛纔咱們拿到的
svg_text 就是指定的 text 標籤中的字符:
'671260781104096663000892328440489239185923'
複製代碼
咱們須要計算字符串長度嗎?並不用,咱們知道,每一個字符大小爲 14 px,只須要將 CSS 樣式中的 x 值除以字符大小,獲得的就是該字符在字符串中的位置。除法獲得的結果有多是整數也有多是非整數,當結果是整數是說明定位徹底準確,咱們利用切片特性就能夠拿到字符。若是結果是非整數,就說明定位不徹底準確,因爲字符不可能出現一半,因此咱們利用地板除(編程語言中常見的向下取整除法,返回商的整數部分。)就能夠拿到整數:
position = x // int(font_size) # 結果爲 27
複製代碼
也就是說 CSS 樣式 vhkbvu 映射的是 SVG 中第 4 行文本的第 27 個位置的值。映射結果如圖 6-31 所示。
圖 6-31 映射結果
而後再利用切片特性拿到字符。對應代碼以下:
number = svg_text[position]
print(number)
複製代碼
代碼運行結果爲 4。咱們還能夠嘗試其餘的 class 屬性值,最後獲得的結果與頁面顯示的字符都是相同的,說明這種映射算法是正確的。至此,咱們已經完成了對映射型反爬蟲的繞過。
與 6.1 節和 6.2 節相同,本節示例所用的反爬蟲手段,即便藉助渲染工具也沒法得到「見到」的內容。SVG 映射反爬蟲利用了瀏覽器與編程語言在渲染方面的差別,以及 SVG 與 CSS 定位這樣的前端知識。若是爬蟲工程師不熟悉渲染原理和前端知識,那麼這種反爬蟲手段就會帶來很大的困擾。
真是翹首以盼!《Python3 反爬蟲原理與繞過實戰》一書終於要跟你們見面了!爲了感謝你們對韋世東和本書的期待與支持,在新書發佈時會舉辦多場送書活動和限時折扣活動。
想要與做者韋世東交流或者參加新書發佈活動的朋友能夠掃描二維碼進羣與我互動哦!
本篇內容摘自出版圖書《Python3 反爬蟲原理與繞過實戰》,歡迎各位好友與同行轉載!
記得帶上相關的版權信息哦😊。