[Python爬蟲] 「暴力」破解貓眼電影票房數據的反爬蟲機制
12月28日,人民日報發文批評豆瓣、貓眼上對《長城》、《擺渡人》、《鐵道飛虎》等電影的差評傷害了中國電影產業。
第二天(12月29日),人民日報再次發文,說中國電影要有容得下一星的肚量。
我對國產電影已經無話可說,所以咱們還是來聊一聊有關數據分析的話題。
01. 常見反爬蟲機制
01.01 通過Headers反爬蟲
Headers就像寄快遞填的那個單子,這些信息與正文無關,卻關系著通信能否成功。以下圖為例,當我訪問自己的知乎主頁時,消息頭就包括:
消息頭 | 簡介 |
---|---|
請求網址 | 我們與之互動通信的網絡地址 |
請求方法 | GET指從這個網址獲取內容,而輸入用戶名密碼登錄網站則是POST方法 |
遠程地址 | 115.159.241.95是知乎的服務器IP,443是SSL加密通信的端口號 |
狀態碼 | 200表示OK,另一個很有名的狀態碼是404 Not Found |
當然消息頭遠不止這些內容,還包括你的操作系統型號、瀏覽器型號、語言、cookie等。多年以前還有人專門做個網頁,搞得好像算命似的來“猜”你的電腦有什么信息,其實都是瀏覽器悄悄地出賣了你。
如果直接調用Python的urllib等擴展包,也可以發起網絡請求,但是默認的Headers信息也會誠實地顯示出來。目標網站受到帶有Headers信息的請求,就可以知道請求的發起方是人是鬼還是爬蟲。
這是最基礎的一種反爬蟲機制,破解也很簡單,只要在發起請求時偽造Headers,裝的像個人就行了。
01.02 基于用戶行為反爬蟲
有時候網絡狀況不好,點擊一個按鈕沒有反應,我們就會連續點擊很多次,然后網站彈出個對話框說“您在短時間內執行了太多相同操作”,并暫時封禁了這一行為。
人類的手工操作上限是很低的,高橋名人每秒也不過17次,專業電競選手APM也只有幾百,而爬蟲程序則可以高出幾個數量級,這會給服務器帶來很大負擔。
除了操作頻率以外,真正的人類和程序還有很多行為模式上的差別,很多網站都會采取機器學習算法來鑒別請求的發起方是否為正常人。
破解手段也是很多樣的,比如建立“IP池”,把大量的請求分散到不同的IP地址來源上,這樣看起來好像是很多用戶在短時間內自然操作的。
01.03 其他手段
爬蟲與反爬蟲之間的消長已有幾十年歷史,發展出的技術理念和手段紛繁多樣,如通過AJAX、JS腳本等方式動態產生網頁元素。應對這些反爬蟲技術,我的習慣是使用自動化測試框架Selenium,驅動瀏覽器內核,完全模擬用戶行為,也就是“不是爬蟲的爬蟲”,所謂手中無蟲、心中有蟲也。
在實際應用中,不論爬蟲還是反爬蟲,都是多種方式結合起來的。
02. 貓眼的反爬蟲機制
打開瀏覽器控制臺可以發現,票房數據其實是加密過的生僻unicode編碼,而且每次訪問獲得的unicode是隨機生成的。也就是說,明文攻擊只對單次訪問有效。
而前端的閱讀是正常的,這是因為貓眼使用了來自美團的特殊字體,把密文編碼對應的字符,通過樣式表渲染成為數字,這其實就是一個解密的過程。
如果手動把數據元素的
class="cs"
屬性去掉,那么在系統默認字體中,前端出來的也是生僻字符。
破解這種機制,大致上有兩個方向:
- 密碼學破譯: 采集足量的明文-密文對照樣本,挖掘映射關系。
- 模式識別: 繞過加密系統,從圖像中“讀”出數字。
密文的unicode編碼位數不多,破解的難度不大。但是問題在于,就像上面提到過的,反爬蟲技術也是綜合了多種方式,要高頻、大量地采集數據,很有可能觸發其他防御手段。
所以這里我采用了第二個方向。
03. 網頁自動截圖
(這一部分涉及較多本地文件處理,因各人操作系統差別較大,暫時不將代碼在線互動化)
03.01 擴展包
這一步用到的擴展包主要有兩個:
- selenium: 自動化測試框架
- PIL: Python Image Library,即Python圖片處理庫
from selenium import webdriver from PIL import Image
03.02 獲取數字圖像
driver = webdriver.Firefox() # 創建webdriver對象 url = "http://piaofang.maoyan.com" # 定義目標url driver.get(url) # 打開目標頁面 # 獲取當前電影名稱列表 movie_names = [driver.find_element_by_xpath(".//*[@id='ticket_tbody']/ul[{}]/li[1]/b".format(i)).text for i in range(1,24)] # 獲取實時票房列表 current_piaofang = [driver.find_element_by_xpath(".//*[@id='ticket_tbody']/ul[{}]/li[2]/b/i".format(i)).text for i in range(1,24)]
接下來是如何自動截圖并存儲,注意FireFox內核的截圖API只能截取當前可視頁面,而貓眼票房超過一屏,就必須加一個向下滾動操作。
從整個頁面的圖像中,再根據實時票房數據的位置和尺寸,單獨把數字截取出來。為了區分這兩個步驟,前一步叫“截圖”,后一步叫“摳圖”。
# 定義截圖函數 def snap_shot(url, image_path, scroll_top=90): # 打開頁面,窗口最大化 driver.get(url) driver.maximize_window() # 調用JS腳本滾動頁面 scroll_js = "var q=document.documentElement.scrollTop={}".format(scroll_top) driver.execute_script(scroll_js) # 截圖存儲 driver.save_screenshot(image_path) # 定義摳圖函數 def crop_image(image_path, pattern_xpath, crop_path, scroll_top=90): # 獲取頁面元素及其位置、尺寸 element = driver.find_element_by_xpath(pattern_xpath) location = element.location size = element.size # 計算摳取區域的絕對坐標 left = location['x'] top = location['y'] - scroll_top right = location['x'] + size['width'] bottom = location['y'] + size['height'] - scroll_top # 打開圖片,摳取相應區域并存儲 im = Image.open(image_path) im = im.crop((left, top, right, bottom)) im.save(crop_path) # 獲取當前時間戳 now = datetime.datetime.now() now_sign = str(now.day)+str(now.hour)+str(now.minute)+str(now.second) # 啟動截圖函數,獲取當前頁面 snap_shot_path_1 = "snap_shot/maoyan_{0}_{1}.png".format('1', now_sign) snap_shot_path_2 = "snap_shot/maoyan_{0}_{1}.png".format('2', now_sign) snap_shot(url, snap_shot_path_1, scroll_top=90) snap_shot(url, snap_shot_path_2, scroll_top=720) # 啟動摳圖函數 for i in range(1,12): pattern = ".//*[@id='ticket_tbody']/ul[{}]/li[2]/b/i".format(i) crop_path = "snap_shot/crop/current_piaofang_{}.png".format(movie_names[i-1]) crop_image(snap_shot_path_1, pattern, crop_path, scroll_top=90) for i in range(13,24): pattern = ".//*[@id='ticket_tbody']/ul[{}]/li[2]/b/i".format(i) crop_path = "snap_shot/crop/current_piaofang_{}.png".format(movie_names[i-1]) crop_image(snap_shot_path_2, pattern, crop_path, scroll_top=720)
03.03 建立訓練集
如此我們已經獲得了實時票房數據的圖像,但是這些數據少則三位(有效數字),多則六位,趕上大片的話七八位都是有可能的。對于程序來說要認識這么多數字,需要很長的訓練過程和極大的訓練集,那是不理智、不合適的。
其實我們地球人認識數字也不是這樣認的,而是先認識0~9這十個阿拉伯數字,再結合數位的知識,組成一個多位數字。
在這一案例中,加密字體是按位加密的,但是整個數字的結構沒有變,有效數字和小數點位置都是可以通過爬蟲直接獲取的信息。這就相當于,程序已經學會了關于“數位”的知識,那么只需要再讓程序學會0~9十個數字就行了。
要建立這樣的訓練集,還需要進一步處理上面得到的圖片,就是單獨把每一位數字切出來。
# 獲取實時票房數據的有效數字長度列表 curpf_lenths = [len(current_piaofang[i-1]) for i in range(1,24)] # 定義切圖函數 def single_digit(index=1): lenth = curpf_lenths[index-1] name = movie_names[index-1] im = Image.open("snap_shot/crop/current_piaofang_{}.png".format(name)) # 轉換為灰度圖像 im = im.convert('L') # 切分整數部分 for j in range(lenth-3): locals()['digit_'+ str(j)] = im.crop((0+j*6, 0, 6+j*6, 12)) # 切分小數部分 for j in range(lenth-3, lenth-1): locals()['digit_'+ str(j)] = im.crop((j*6+4.8, 0, 6+j*6+4.8, 12)) # 對每部電影,按位存儲圖片 for k in range(0, lenth-2): locals()['digit_'+ str(k)].save("snap_shot/train/digit_{0}_{1}_{2}.png".format(k, name, now_sign)) # 啟動切圖函數 for i in range(1,24): single_digit(i)
將得到的數字圖像分類整理(這一步真的只能人工完成了),因為貓眼的刷新頻率不高,所以沒有積累太多的訓練樣本,每個數字只有2、30個。但因為是印刷體,遠比手寫體容易識別,所以準確率還勉強可以接受。
04. 模式識別
04.01 擴展包
- os: 操作系統API
- PIL: 圖像處理
- numpy: 基礎數值計算
- pandas: 結構化數據處理
- scikit-learn: 機器學習
因為本案例識別難度不高,所以沒有使用專門的圖像處理模塊如keras, mxnet等。
04.02 圖像矢量化
這里的“圖像矢量化”跟“矢量圖”不是一個概念,后者是平面設計中常見的圖片格式。“矢量化”是一個特征提取的過程,對于scikit-learn,每一個訓練樣本(一張圖)都是一個高維矢量。
之前獲得的訓練集是由6×12的灰度圖片構成的,灰度的值域是0~255,每個像素點的灰度值都構成一個特征維度,這樣每個樣本就有18432維的特征,這就太高了。如果不轉換為灰度圖像,而是保留色彩,那維度又會高出幾個數量級。
所以對圖片需要進行特征壓縮,比如數字識別可以說是白紙黑字,那么不需要知道具體的灰度值,只要確定一個像素點是背景還是字體就可以了,也就是二值化,非0即1。
# 二值化函數 def binary_image(im): threshold = 200 # 閾值設為200 table = [] for i in range(256): if i < threshold: table.append(0) else: table.append(1) out = im.point(table,'1') return out # 矢量化函數 def buildvector(im): v = [] for i in im.getdata(): v.append(i) return v
經過二值化與矢量化,每張圖片就變成了1行、72列的矢量,每一列都是0或1,代表紙或字。
04.03 機器學習
最后的過程我沒有弄得太復雜,直接調用了Scikit-learn里的支持向量分類器,基本上保留了默認設置,交叉驗證得分在
0.85
左右。
實際體驗是比85%準確率要好的,因為票房數據中0,1,2的出現頻率更高,訓練樣本多、識別率就更高。
End.
轉載請注明來自36大數據(36dsj.com): 36大數據 ? [Python爬蟲] 「暴力」破解貓眼電影票房數據的反爬蟲機制