破解點評網字體反爬,深入挖掘系統背後的原理

  • 2019 年 10 月 3 日
  • 筆記

上次數獨(旁友數獨會伐啦?python秒解數獨了解下伐啦?)後,老王好像從哪裡得到了風聲,跟我說少往他們家帶撲克牌……意思里你的家庭矛盾都是因為一副撲克牌咯?

行,那我這段時間先歇一歇,來日方長……

那閑著也是閑著,不能去隔壁了,也不能讓小胖這雙手停下來不是……

那就上點評網找找妹子樂趣,然後就發現點評的反爬做的是真厲害。各個方面的反爬都有涉及,今天我們主要來看一下字體反爬這個玩意兒。

演示環境

  • 作業系統:windows10
  • python版本:python 3.7
  • 程式碼編輯器:pycharm 2018.2
  • 使用模組:requests,json,re,fontTools

什麼是字體反爬?

首選我們先來看一下點評網的評論資訊。

從這裡可以看到,網頁上顯示的文字和源碼中顯示的文字有些出入,並不是一一對應,那繼續查看sources中的程式碼。

可以看到,評論中的某些文字點評網做了特殊處理,這就是所謂的字體反爬。

抓取數據

前面的步驟,我們已經知道點評網對評論內容做了處理,至於是如何處理,這裡我們先不管,還是先把數據拿到再說。要是數據都沒有拿到,還怎麼對數據進行處理呢?

首先使用Google的network,對所有請求進行抓包。然後隨便搜索一個評論中的某些東西,找到返回的評論數據請求。這裡我使用評論人的名字進行搜索,找到其中的請求。有沒有覺得這個請求就是返回的評論數據呢。那來驗證下。因為這裡返回的是一個json數據,可以藉助在線json數據查看工具,方便我們對內容進行查看。

將數據複製下來,然後在瀏覽器中輸入網址json.cn

接著就能看到解析出來的json數據。

經過分析,我們知道所有的評論數據都在['reviewAllDOList'],這個集合里裝了當前頁面前10人的評論數據。這樣就可以通過列表遍歷的方式拿到相應的數據。
點擊這個url的headers,找到請求的url,準備獲取數據。

注意:

  • 這個獲取到的url只能使用一會兒,過一會就會變化。如果一直使用這個url請求,後面就會得不到數據。所以後續當請求不到數據的時候,就需要刷新網頁,獲取一個新的url。因為url中有個_token參數是每次變化的。
  • 然後下面框中的內容也必須在請求頭中添加上去,否則也還是得不到數據。
  • 這裡的重點是在字體反爬,所以其他的一些反爬在這裡就不進行贅述了。

至此就找到請求的評論介面數據,直接請求這個url,就能得到我們想要的數據。

import requests  import json  import re      def get_page_info():      # 首先分析網頁,找到返回評論數據的url,這個url就會直接返回評論數據了,但是urlt中的token是會變化的,只能用一會兒,我也不知道一會兒是好久,得不到數據了就換url吧      url = 'http://www.dianping.com/ajax/json/shopDynamic/allReview?shopId=131013635&cityId=1604&shopType=10&tcv=7bbq1hdmsj&_token=eJxVTstugkAU%2FZe7nsBcBlBIulBrGxC0MmATTReACoSCFIg4Nv33Thu66Oq8k%2FMJrXMEGymlOhK4nlqwARWqmECg72RimAZOkFkTauoE0v%2BehRqBpN09gn1glkEmhv72YwRSH9BgJpma0hmpJqmmE%2B2348gK5H3f2Ko6DINyLOK6KepMSS%2BV2uWXRkWGFJnJDHkF5KQK5URiOWI8Yv%2BnfflddrsiqyU7ubeQd3r3cQ78Loyof58HQlgrzjXhpejxiHn3Zb%2BO%2BHUjFtOZCMrkOc%2Fi6lYlWbZbrLKeJ1u6RqfxUuaHhWitZb0Oy4RHrnveV0X6%2FhQ0VbPZvu5FOZ%2B91Oi4wwN8fQMlVWIi&uuid=c59d33fd-e043-a0f5-f6e1-79ae90d14254.1565007755&platform=1&partner=150&optimusCode=10&originUrl=http%3A%2F%2Fwww.dianping.com%2Fshop%2F131013635'          # 定義模擬請求頭      headers = {          'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',          'Cookie': 'cy=8; cye=chengdu; _lxsdk_cuid=16c61bb35536e-0e2ab00cb9c2a8-c343162-144000-16c61bb35547b; _lxsdk=16c61bb35536e-0e2ab00cb9c2a8-c343162-144000-16c61bb35547b; _hc.v=c59d33fd-e043-a0f5-f6e1-79ae90d14254.1565007755; s_ViewType=10; __utmz=1.1565010551.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _lx_utm=utm_source%3DBaidu%26utm_medium%3Dorganic; __utma=1.1978331348.1565010551.1565010551.1565161172.2; __utmc=1; _lxsdk_s=16c6b1cf413-8ae-d6-7b8%7C%7C31',          'Referer':'http://www.dianping.com/shop/131013635',          'Connection': 'keep-alive',      }          # 使用requests庫請求url,得到數據json數據      result_json_str = requests.get(url,headers=headers).text      # 應為返回的數據是裡面包含富文本數據,所以首先使用正則表達式刪除標籤      result_json_str = re.sub('<.*?>','',result_json_str)        # json數據其實就是一個字元串,所以我們需要先將json轉化為python能操作的字典      result = json.loads(result_json_str)      # 分析得到的數據,得到我們需要的所有評論在result['reviewAllDOList']裡面      all_review = result['reviewAllDOList']          # 遍歷得到的所有評論      for review in all_review:          # 得到用戶名          username = review['user']['userNickName']          # 得到評論內容          content = review['reviewDataVO']['reviewBody']          # 這裡我們就是簡單的顯示出內容就是了,沒有進行儲存          print('*'*30,'n',username,content,'n','*'*30)

運行程式碼,查看數據,得到的數據果然就是經過處理的。

破解字體反爬

上面雖然拿到了數據,但是這些都是經過處理之後的數據,拿著完全不能用,所以還是得想辦法將他給破解下。

首先我們分析網頁得知,這些處理之後的數據class都為review,然後他的字體都是'PingFangSC-Regular-review'

猜想這就是點評網自己定義的字體。居然自定義了字體,那麼網頁中肯定需要載入字體文件,所以果斷打開network對字體文件進行抓包。

搜索關鍵字'PingFangSC-Regular-review',就能找到相應的資訊。

我們可以看到,點評網有許多個自定義的字體,這裡只需要找自己想要的字體文件即可,即找字體文件的url。只是這些字體文件一般都是.woff或者.ttf結尾的,我們可以將下面的滾動條往右邊拖動,就能找到一個.woff的url了。

發現這個url前面是以//開始的,那嘗試直接在網址前面加https就行了,那麼完整的url就是
https://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/c667da25.woff

然後在瀏覽器中輸入這個網址,就可以下載一個後綴是.woff的字體文件。

為了方便查看字體文件的內容,我們還需要下載fontCreator這個軟體。使用這個軟體打開我們剛才下載的文件,就能夠看到相應的值。fontCreator官網地址為
https://www.high-logic.com/font-editor/fontcreator

使用fontCreator打開這個woff文件,如下圖所示。

我們得把這裡面所有的文字按順序都寫出來,並用一個列表保存,空的數據使用”表示。
是的,你沒有看錯,將這些文字全部寫下。

這些就是字體的形狀,我們已經認識了這些字,但我們還得讓程式也認識這些字,當然你也可以使用機器學習來識別這些文字,注意:一定要按順序來,不能遺漏,不然的話對應關係會出錯,替換出來的結果也就會出錯。

我們把文字敲出來之後,然後需要得到字體形狀對應的名字,對應程式碼字元串。這裡需要使用fontTools這個第三方庫來處理字體文件。

from fontTools.ttLib import TTFont      def get_font_map():      # 這個字體文件需要先析網頁,找到這個url,然後下載下來到本地,然後使用TTFont()載入字體文件      #       字體文件的名字      font = TTFont('76d0609c.woff')      # 得到cmap 字體對應程式碼->字體名字      font_cmap = font.getBestCmap()      # 得到所有的字體名字      font_names = font.getGlyphOrder()      # 這個文字是先使用fontCreator軟體打開字體文件,然後查看到字體,從而得到的數據      texts = [          '','','1','2','3','4','5','6','7','8',          '9','0','店','中','美','家','館','小','車','大',          '市','公','酒','行','國','品','發','電','金','心',          '業','商','司','超','生','裝','園','場','食','有',          '新','限','天','面','工','服','海','華','水','房',          '飾','城','樂','汽','香','部','利','子','老','藝',          '花','專','東','肉','菜','學','福','飯','人','百',          '餐','茶','務','通','味','所','山','區','門','葯',          '銀','農','龍','停','尚','安','廣','鑫','一','容',          '動','南','具','源','興','鮮','記','時','機','烤',          '文','康','信','果','陽','理','鍋','寶','達','地',          '兒','衣','特','產','西','批','坊','州','牛','佳',          '化','五','米','修','愛','北','養','賣','建','材',          '三','會','雞','室','紅','站','德','王','光','名',          '麗','油','院','堂','燒','江','社','合','星','貨',          '型','村','自','科','快','便','日','民','營','和',          '活','童','明','器','煙','育','賓','精','屋','經',          '居','庄','石','順','林','爾','縣','手','廳','銷',          '用','好','客','火','雅','盛','體','旅','之','鞋',          '辣','作','粉','包','樓','校','魚','平','彩','上',          '吧','保','永','萬','物','教','吃','設','醫','正',          '造','豐','健','點','湯','網','慶','技','斯','洗',          '料','配','匯','木','緣','加','麻','聯','衛','川',          '泰','色','世','方','寓','風','幼','羊','燙','來',          '高','廠','蘭','阿','貝','皮','全','女','拉','成',          '雲','維','貿','道','術','運','都','口','博','河',          '瑞','宏','京','際','路','祥','青','鎮','廚','培',          '力','惠','連','馬','鴻','鋼','訓','影','甲','助',          '窗','布','富','牌','頭','四','多','妝','吉','苑',          '沙','恆','隆','春','干','餅','氏','里','二','管',          '誠','制','售','嘉','長','軒','雜','副','清','計',          '黃','訊','太','鴨','號','街','交','與','叉','附',          '近','層','旁','對','巷','棟','環','省','橋','湖',          '段','鄉','廈','府','鋪','內','側','元','購','前',          '幢','濱','處','向','座','下','県','鳳','港','開',          '關','景','泉','塘','放','昌','線','灣','政','步',          '寧','解','白','田','町','溪','十','八','古','雙',          '勝','本','單','同','九','迎','第','台','玉','錦',          '底','後','七','斜','期','武','嶺','松','角','紀',          '朝','峰','六','振','珠','局','崗','洲','橫','邊',          '濟','井','辦','漢','代','臨','弄','團','外','塔',          '楊','鐵','浦','字','年','島','陵','原','梅','進',          '榮','友','虹','央','桂','沿','事','津','凱','蓮',          '丁','秀','柳','集','紫','旗','張','谷','的','是',          '不','了','很','還','個','也','這','我','就','在',          '以','可','到','錯','沒','去','過','感','次','要',          '比','覺','看','得','說','常','真','們','但','最',          '喜','哈','么','別','位','能','較','境','非','為',          '歡','然','他','挺','著','價','那','意','種','想',          '出','員','兩','推','做','排','實','分','間','甜',          '度','起','滿','給','熱','完','格','薦','喝','等',          '其','再','幾','只','現','朋','候','樣','直','而',          '買','於','般','豆','量','選','奶','打','每','評',          '少','算','又','因','情','找','些','份','置','適',          '什','蛋','師','氣','你','姐','棒','試','總','定',          '啊','足','級','整','帶','蝦','如','態','且','嘗',          '主','話','強','當','更','板','知','己','無','酸',          '讓','入','啦','式','笑','贊','片','醬','差','像',          '提','隊','走','嫩','才','剛','午','接','重','串',          '回','晚','微','周','值','費','性','桌','拍','跟',          '塊','調','糕'      ]          font_name_map = {}          # 將 字體名字 和 我們查看到的值 組成一個字典      for index,value in enumerate(texts):          font_name_map[font_names[index]] = value          return font_cmap,font_name_map

這裡我還是很貼心的給大家畫了一個圖來解釋其中的對應關係。

這樣就得到了字體的對應關係,但是code是一個整數,而網頁上顯示的是&#xef05;&#xe40e;類似這樣的數據,這需要幾步轉化下:
1、將code變成16進位
2、將最前面的0替換為&#
3、在最後面添加一個;

所以我們也把code按照這個規則進行轉換,然後使用re模組。只要找到這樣的一個code,就直接替換為文字。這樣就能夠拿到準確的數據了。

所以,我們最後的get_page()函數的程式碼如下所示

def get_page(font_names_map=None,font_cmap=None):      # 首先分析網頁,找到返回評論數據的url,這個url就會直接返回評論數據了,但是urlt中的token是會變化的,只能用一會兒,我也不知道一會兒是好久,得不到數據了就換url吧      url = 'http://www.dianping.com/ajax/json/shopDynamic/allReview?shopId=131013635&cityId=1604&shopType=10&tcv=txgmn7z01d&_token=eJxVj81ugkAUhd9ltp3A%2FCskXag1DQq2MmBSTReAOhIEEYg6Nn33Do1ddHXO%2Fe45yb1foPG2wMUIIYYhuOwa4AJsIUsACLrWbLjgRGBHcIdzCLJ%2FTPABhSBtVi%2FA3TCC4ICzzx6EZt5gTgUcCkMelhhLGCS%2FGc9EwKHrate2r9ertc2Tqs4rZWWn0m4Pp9rGFCNMBeXmFGAqZdRXCKOQDFgPih4YTR7a%2Fc2BecKU2lxVxu1mt0i2rD3vw6CNYhTcx6HWzlxKov0M%2BzKm%2Fn3aLWJ5edOT4UiHRfp6UEl5K1OlVpO56mS6RAvs1X5GgyjXjTOtFlGRyng226%2BPR1nwp2S9uhUfda7Go3d99jz0DL5%2FANI8Y5M%3D&uuid=c59d33fd-e043-a0f5-f6e1-79ae90d14254.1565007755&platform=1&partner=150&optimusCode=10&originUrl=http%3A%2F%2Fwww.dianping.com%2Fshop%2F131013635'          # 定義模擬請求頭,注意,得不到數據的時候,也要將Cookie的值進行替換      headers = {          'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36',          'Cookie': 'cy=8; cye=chengdu; _lxsdk_cuid=16c61bb35536e-0e2ab00cb9c2a8-c343162-144000-16c61bb35547b; _lxsdk=16c61bb35536e-0e2ab00cb9c2a8-c343162-144000-16c61bb35547b; _hc.v=c59d33fd-e043-a0f5-f6e1-79ae90d14254.1565007755; s_ViewType=10; __utmz=1.1565010551.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _lx_utm=utm_source%3DBaidu%26utm_medium%3Dorganic; __utma=1.1978331348.1565010551.1565010551.1565161172.2; _lxsdk_s=16c70ded480-ab0-fe2-71%7C%7C2',          'Referer':'http://www.dianping.com/shop/131013635',          'Connection': 'keep-alive',      }          # 使用requests庫請求url,得到數據json數據      result_json_str = requests.get(url,headers=headers).text      # 應為返回的數據是富文本數據,所以首先我們先去掉標籤      result_json_str = re.sub('<.*?>','',result_json_str)          # 遍歷 字體程式碼->字體名字 這個字典(code 是一個數字)      for code, name in font_cmap.items():          try:              # 嘗試從 字體名字 -> 對應值 這個字典中得到值,防止程式出現KeyError的錯誤              text = font_names_map[name]          except:              pass          else:              # 分析網頁資訊得知,將code變成16進位,並且把最前面的0換成&#,在加上一個';'. 就是網頁加密了的字元竄了              # 這裡就是將59322這樣的值變成類似`&#xef05;&#xe40e;`的值              code_str = str(hex(code)).replace('0', '&#', 1) + ';'              print(code, code_str, name, text)              # 將得到的加密之後的字元串進行替換為相應的數據              # result_str = re.sub('需要替換的字元竄','替換為怎樣的字元串','從這個字元串裡面查找')              result_json_str = re.sub(code_str, text, result_json_str)          # 處理之後的數據使用json模組變成字典      result = json.loads(result_json_str)      # 分析得到的數據,得到我們需要的所有評論在result['reviewAllDOList']裡面      # 因為這裡有可能我們別識別出來是一個爬蟲了,就會返回其他的數據,比如說你沒有登陸啊這樣的提示。所以這個時候我們就需要改變我們的額url了。然後重新運行我們的爬蟲了      try:          all_review = result['reviewAllDOList']      except:          print(result_json_str)          raise ValueError('爬取數據失敗')          # 遍歷得到的所有評論      for review in all_review:          # 得到用戶名          username = review['user']['userNickName']          # 得到評論內容          content = review['reviewDataVO']['reviewBody']          # 因為我們的重點是字體反爬,所以這裡我們就是簡單的顯示出內容就是了          print('*'*30,'n',username,":",content,'n','*'*30)

呼。。。我們終於破解了點評網的字體加密。

最後還有一點需要注意,因為這個程式我當天寫好之後,能成功的替換相應的字元串,但是當我第二天運行程式的時候,缺不能替換了。

經過分析發現,原來是點評網每天(或許不是每天,每幾個小時)應該都會變換字體文件,然後code->name,name->形狀也就對應不上了,但是形狀->值一定是對應上的,這個不會變化。

那麼我們每次運行之前,就直接找到字體文件對應的url,然後先將這個文件下載保存到本地,再運行我們的爬蟲即可。

注意:這個字體文件的url是會變化的,也就是點評網的伺服器上每個字體應該存放了好幾個不同的字體文件。所以我們每次運行都需要先去找到對應的字體文件的url。

from urllib.request import urlretrieve      def get_font_file():      url = 'https://s3plus.meituan.net/v1/mss_73a511b8f91f43d0bdae92584ea6330b/font/c667da25.woff'      urlretrieve(url,'font.woff')

這裡說一下urlretrieve函數的用法吧。

urlretrieve:將網路上的文件下載下來,保存到本地。第一個參數為url,第二個參數為保存到本地文件的文件名。

使用這個函數我們可以很方便的下載網路上一些文件,圖片等。
最後我們來看一波運行結果吧。

不得不服點評網,反爬蟲做的真是厲害。。。

關注公眾號「Python專欄」,更多有趣好玩的Python等著你喲~

全部程式碼已上傳至Github:https://github.com/MiracleYoung/You-are-Pythonista/tree/master/PythonExercise/App/dianping_spider