一起來相約貓眼

  • 2019 年 10 月 5 日
  • 筆記

一起來相約貓眼

0.說在前面1.原理2.相約貓眼3.作者的話

0.說在前面

之前有人給我提了個需求,讓我去看看貓眼專業版,字體反爬問題,我覺得有趣,因為之前沒學過字體反爬。然後,就嘗試去搞了一下,結果當時因為xx原因,放棄了。也是實力不夠啊!後來,也就是昨天,又想起來了,這個遺留問題,就來嘗試學習學習,本文將以貓眼專業版網站為例,深入研究字體反爬問題。

我們一起來學習吧,嗨啊嗨!

1.原理

網站:貓眼專業版 https://piaofang.maoyan.com/?ver=normal

我想獲取票房數據,結果看下圖,沒有數據。這就涉及到了字體反爬!

我們暴力一波,直接用xpath解析,然後爬取出來的並不是想要的。。

然後我們來分析一下網頁源碼,看到style標籤下面,有如下內容:

我們看到,這個網站使用的是自定義字體,並且編碼採用base64,我們來刷新一下頁面,再看看一下當前的這個自定義字體位置,會發現,base64後面的字體編碼內容是隨機的,並不是固定的,最大的難點也在於這裡!

我們將這裡的d09開頭到AAA結尾這一整塊的字體編碼複製出來,並通過python程式碼進行base64解碼,並保存為maoyan.woff格式的字體。

這裡介紹一個查看woff字體內部對應編碼的網站:

http://fontstore.baidu.com/static/editor/index.html

下圖是我隨機將woff文件打開後的樣子!

如上圖,我們知道數字與編碼對應關係為:

"uniF1D0": "4","uniE13A": "3","uniE64F": "0","uniECF2": "1","uniF382": "2",  "uniE1FD": "8", "uniF5E4": "6","uniF1B0": "9","uniE71E": "7","uniE979": "5"  

是不是,我們直接拿到了這個字體編碼,然後根據字體編碼匹配對應的數字,然後在爬出的數據中替換掉那些反爬字體就可以了呢?

答案是肯定的,但是這裡要重點說明一下,每次獲取網站的數字與編碼不會一樣

仔細琢磨上面這句話,會發現矛盾了!

編碼是不固定,不能用編碼一一對應關係來處理字體反爬!

那麼怎麼做?這裡才是重重之中

引入第三方庫fontTools,我們可以利用fontTools可以獲取每一個字元對象

這個對象你可以簡單的理解為保存著這個字元的形狀資訊。而且編碼可以作為這個對象的id,具有一一對應的關係。

對象每次不會變化,我們可以根據對象中的編碼屬性獲取編碼所對應的數字

那麼到這裡,我們的整體思路就搞定了,總結一波!

首先我們隨機從網站上獲取原始的字體數據,然後對其base64進行解碼,轉為woff文件,通過上面的網站,手動匹配當前這個字體的編碼與數字關係。對剛才建立的關係,通過footTools為編碼與數字建立關係,由於對象是不變的,我們此時就不必考慮網站的編碼與數字動態變化問題,只需要將編碼塞進之前的footTools對象中,即可獲取對應的數字!

這裡再做解釋,第一次我們取網站上的一個字體並解碼為xx.woff,並得到映射關係,相應的編碼相應的字體對象,而編碼又與字體對應,反過來,當我們隨機取得網上另外一個yy.woff字體時,我們知道了該字體的對象,那麼我們可以通過對象與編碼關係,得到編碼,然後通過編碼與字體關係,最終的得到我們想要的數字!

關係圖如下:

xx.woff 字體->編碼->對象  yy.woff 字體->對象->編碼->字體  

下面我們來實戰吧!

2.相約貓眼

導包

import re  import base64  import requests  from lxml import etree  from fontTools.ttLib import TTFont  from prettytable import PrettyTable  

封裝—定義貓眼爬蟲類

class maoyanSpider():      def __init__(self,url):          headers = {              'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36',          }          self.url = url          self.headers = headers  

獲取網站源碼

def get_html(self):      raw_html = requests.get(self.url, headers=self.headers).text      return raw_html  

存儲字體

將字體存儲為xml文件以及woff文件,這裡填入自己在網站的上那一串字元!

def save_xml(self):      font = "填入網站上的base64後的一串字元"      fontdata = base64.b64decode(font)      with open("./maoyan.woff","wb") as f:          f.write(fontdata)      maoyan_fonts = TTFont('./maoyan.woff')      maoyan_fonts.saveXML("text.xml")  

對應關係

以我的woff為例,定義映射關係!

建立字體與編碼關係:

xx.woff 字體->編碼->對象  
def get_rel(self):      maoyan_fonts = TTFont('./maoyan.woff')      font_dict = {}      base_num = {          "uniF1D0": "4","uniE13A": "3","uniE64F": "0","uniECF2": "1","uniF382": "2",          "uniE1FD": "8", "uniF5E4": "6","uniF1B0": "9","uniE71E": "7","uniE979": "5"}      # 這裡不明白的直接列印,獲取到的是dict類型,編碼與對象之間的關係      # 得到對應字體的編碼與對象關係      _data = maoyan_fonts.getGlyphSet()._glyphs.glyphs      for k, v in base_num.items():          # 為對象建立字體編碼關係!          font_dict[_data[k].data] = v      return font_dict  

獲取網站實時編碼字體對象

def get_woff(self,html):      selector = etree.HTML(html)      font_text = selector.xpath('//style[@id="js-nuwa"]/text()')[0]      base64_behind = re.split(';base64,', font_text)[1]      font_content = re.split(')', base64_behind)[0].strip()      if font_content:          bs_font = base64.b64decode(font_content)          with open("new.woff",'wb') as f:              f.write(bs_font)      font_ttf = TTFont("new.woff")      data = font_ttf.getGlyphSet()._glyphs.glyphs      return data  

將反爬的字體進行填充

有些數據是萬,億或者%結尾,那麼得做判斷!

查看網頁源碼,反爬蟲字體為如下所示,以分號隔開,我們就是通過分號分割字元串,並建立循環,在循環中我們根據是否數據以.開頭來判斷是從3取還是4取,目的是取出後4位,將其與uni進行拼接即為我們上面woff字體文件中的編碼!

然後通過字體->對象->編碼->字體,最終獲取真實字體,並返回真實字體!

.萬  
def replace_Str(self,str_r, data_woff):      font_dict = self.get_rel()      if str_r[-1] == "萬" or str_r[-1] == "%"  or str_r[-1] == "億" :          str_end = str_r[-1]          string = str_r.replace("萬", '').replace("%", "").replace("億", "")          num_list = string.split(";")          str_All = ""          for each_str in num_list:              if not each_str.startswith("."):                  each_str = each_str[3:].upper()                  if each_str:                      each_str = font_dict[data_woff["uni%s" % each_str].data]                      str_All+=each_str              else:                  str_All+='.'                  each_str = each_str[4:].upper()                  each_str = font_dict[data_woff["uni%s" % each_str].data]                    str_All+=each_str            str_All+=str_end          return str_All      else:          str_list = str_r.split(";")          str_All = ""          for each_str in str_list:              if each_str and not each_str.startswith("."):                  each_str = each_str[3:].upper()                  each_str = font_dict[data_woff["uni%s" % each_str].data]                  str_All+=each_str              elif each_str:                  str_All += '.'                  each_str = each_str[4:].upper()                  each_str = font_dict[data_woff["uni%s" % each_str].data]                  str_All += each_str          return str_All  

數據抓取及美化列印

注意事項:不要用xpath去取數據,只能用正則!!!

    def get_content(self):          html = self.get_html()          data_woff = self.get_woff(html)          selector = etree.HTML(html)          dayStr = selector.xpath('//span[@id="dayStr"]/text()')[0].replace(' ','').replace('n','')          dapanStr = selector.xpath('//div[@class="logo"]/span[2]/text()')[0]          total_Ticket = re.findall("<span id='ticket_count'><i class="cs">(S+)</i></span>", html)[0]          total_Ticket = self.replace_Str(total_Ticket, data_woff)          title_content =dayStr + dapanStr + total_Ticket            dayTips = selector.xpath('//div[@id="dayTips"]/text()')[0]            movie_name = selector.xpath('//li[@class="c1"]/b/text()')          print(movie_name)          time_ticket_list = []          ticket_bili_list = []          rank_bili_list = []          site_bili_list = []          for i in range(len(movie_name)):              time_ticket = re.findall(r'<b><i class="cs">(S+)</i></b>', html)[i]              ticket_bili = re.findall(r'<li class="c3 "><i class="cs">(S+)</i></li>', html)[i]              rank_bili = re.findall(r'<li class="c4 ">[^.]+<i class="cs">(S+)</i>[^.]+</li>', html)[i]              site_bili = re.findall(r'<span style="margin-right:-.1rem">[^.]+<i class="cs">(S+)</i>[^.]+</span>', html)[i]              time_ticket_list.append(self.replace_Str(time_ticket, data_woff))              ticket_bili_list.append(self.replace_Str(ticket_bili, data_woff))              rank_bili_list.append(self.replace_Str(rank_bili, data_woff))              site_bili_list.append(self.replace_Str(site_bili, data_woff))            print(time_ticket_list)          print(ticket_bili_list)          print(rank_bili_list)          print(site_bili_list)          pt = PrettyTable()          pt.add_column("片名",movie_name)          pt.add_column("實時票房",time_ticket_list)          pt.add_column("票房佔比",ticket_bili_list)          pt.add_column("排片佔比",rank_bili_list)          pt.add_column("上座率",site_bili_list)            print(title_content)          print(dayTips)          print(pt)  

對比官網