一起來相約貓眼
- 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)

對比官網