貓眼票房 是檢視國內影院上線電影票房的一個站點,包括 大屏投影 和 常規版 兩個版面。本文嘗試對這兩個版面的資料進行爬取。
1 大屏投影
大屏投影是貓眼票房目前預設的首頁,其中票房資料近乎實時更新,該版面展示的資料中相較於常規版多了一項「場均人次」。
1.1 頁面分析
使用瀏覽器開啟 大屏投影 版後,通過 檢查 調出分析工具,選擇 Network 選項卡,頁面中會不斷更新 https://box.maoyan.com/promovie/api/box/se... 檔案 ~ 已實現更新票房資料。
也即,該 Ajax 相應的 json 資料即為我們所需的資料。同時,當我們跳轉到不同的日期,發現請求的 API 相應改變,如 2019/8/11 的票房資料請求 API 為 https://box.maoyan.com/promovie/api/box/se...。
所以,我們可以模仿瀏覽器的 Ajax 請求來實現資料的獲取。
1.2 程式碼實現
import time
import datetime
import requests
from requests.exceptions import RequestException
def get_one_page(date):
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'
}
url = 'https://box.maoyan.com/promovie/api/box/second.json?beginDate=' + date
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
except RequestException as e:
print("Requests {}, Error {}.".format(date, e.args))
def parse_one_page(json):
if json:
today = json.get('data').get('queryDate')
items = json.get('data').get('list')
for item in items:
yield {
'date': today,
'movieName': item['movieName'],
'releaseInfo': item['releaseInfo'],
'sumBoxInfo': item['sumBoxInfo'],
'splitSumBoxInfo': item['splitSumBoxInfo'],
'boxInfo': item['boxInfo'],
'boxRate': item['boxRate'],
'showInfo': item['showInfo'],
'showRate': item['showRate'],
'avgShowView': item['avgShowView'],
'avgSeatView': item['avgSeatView']
}
def main():
start_date = datetime.date.today()
for i in range(0, 31):
date = start_date - datetime.timedelta(days=i)
json = get_one_page(date.strftime(r'%Y%m%d'))
for item in parse_one_page(json):
print(item)
time.sleep(1)
if __name__ == '__main__':
main()
藉助於 requests 庫,通過 get_one_page(date)
函式獲得指定日期的票房資料;通過 parse_one_page(json)
實現對 json 格式資料的解析;在 main()
函式中,藉助於 datetime 函式,一次請求過去一個月以來資料。
2 常規版
普通版的資料 30 分鐘更新一次,直接請求 https://piaofang.maoyan.com/?ver=normal 頁面時,所需資料已經存在,但返回的數字加密過 ~ 而且每次請求後獲得的資料中數字加密方式不一致。
2.1 頁面分析
通過 檢查 常規版 頁面,選擇 Network 選項卡,檢視 https://piaofang.maoyan.com/?ver=normal 對應的 Response 資料:
我們發現,此頁面的數字進行過加密,對應的樣式為 class="cs"
,通過在該頁面中搜尋 cs
發現其對應的樣式為:
<style id="js-nuwa">
@font-face{font-family:"cs";src:url(data:application/font-woff;charset=utf-8;base64,d09G...yxfp) format("woff");}
.cs{font-family:cs}
</style>
也即,字型通過 base64 進行加密。
2.2 字型加密
我們將加密後的字串 (d09G...yxfp) 使用 base64 解碼後,藉助於 fontTools 庫儲存為字型檔案:
import os
import re
import base64
import requests
from fontTools.ttLib import TTFont
def font_face(url, name):
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36'
}
response = requests.get(url, headers=headers)
fonts = re.search(r'base64,(.*?)\)', response.text, re.S)
fonts = base64.b64decode(fonts)
with open(name+'.woff', 'wb') as fp:
fp.write(fonts)
TTFont(name+'.woff').saveXML(name+'.xml')
if __name__ == '__main__':
url1 = 'https://piaofang.maoyan.com/?ver=normal&date=2019-07-29'
url2 = 'https://piaofang.maoyan.com/?ver=normal&date=2019-07-28'
font_face(url1, 'font1')
font_face(url2, 'font2')
藉助於 百度字型編輯器 開啟儲存好的字型:
此時,我們即可找到加密後的字串與數字之間的對應關係。不過這裡有一點麻煩的地方在於不同時間載入加密方式不一樣,也即字串和數字對應的關係會發生變化。
不過,當我們將字型檔案使用 fontTools
庫儲存為 .xml
格式後發現:
各個數字對應的字串位於 <GlyphOrder>...</GlyphOrder>
中(id 與數值並沒有直接關係);如請求的 font1 中 8 對應於 uniE577,而 font2 中的 1 對應於 uniE5F9,但二者的字形 <glyf>...
中的內容完全一致:
因此,我們找到了所需的不變數,即數字與字型檔案中 <glyf>
的對應關係不會發生變化,而前端呈現的字串與數字之間的對應關係會發生變化。
因此,我們只要找到第一次的加密字串與數字之間的對應關係,利用 <glyf>
中的元素去匹配後續新檔案,即可實現加密字元還原。
2.3 程式碼實現
import re
import time
import datetime
import base64
import requests
from requests.exceptions import RequestException
from pyquery import PyQuery as pq
from fontTools.ttLib import TTFont
font = TTFont('font1.woff')
uni_list = font.getGlyphOrder()[2:]
first_match = {
'uniE893': '0',
'uniF690': '1',
'uniF55C': '2',
'uniF28F': '3',
'uniF4B1': '4',
'uniE623': '5',
'uniF294': '6',
'uniEEC4': '7',
'uniE577': '8',
'uniE77B': '9'
}
def get_one_page(date):
headers = {
'User-Agent': os.getenv('User_Agent')
}
url = 'https://piaofang.maoyan.com/?ver=normal&date=' + date
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.text
else:
return None
except RequestException as e:
print("Requests {}, Error {}.".format(date, e.args))
return None
def parse_font(html):
fonts = re.findall(r'base64,(.*?)\)', html, re.S)[0]
# fonts = re.search(r'base64,(.*?)\)', html, re.S)
fonts = base64.b64decode(fonts)
with open('tmp.woff', 'wb') as fp:
fp.write(fonts)
font1 = TTFont('tmp.woff')
# obj_list1 = font1.getGlyphNames()[1:-1]
uni_list1 = font1.getGlyphOrder()[2:]
tmp_match = {}
for uni1 in uni_list1:
obj1 = font1['glyf'][uni1] #獲取編碼 uni1 在 tmp.ttf 中對應的物件
for uni in uni_list:
obj = font['glyf'][uni]
if obj==obj1:
tmp_match[uni1] = first_match[uni]
return tmp_match
def rebuild_number(number, tmp_match):
'''還需要對數字進行改寫'''
result = ''
for num in number:
s = str(hex(ord(num)))
s = s.upper().replace('0X', 'uni')
if s in tmp_match.keys():
result += tmp_match[s]
else:
result += num
return result
def parse_one_page(html):
tmp_match = parse_font(html)
doc = pq(html)
today = doc('.today').text()[:10]
movies = doc('#ticket_tbody ul').items()
for movie in movies:
result = {}
result['date'] = today
result['movieName'] = movie.find('.c1 b').text()
result['releaseInfo'] = movie.find('.c1 em').text().split()[0]
result['sumBoxInfo'] = rebuild_number(movie.find('.c1 em i').text(), tmp_match)
result['boxInfo'] = rebuild_number(movie.find('.c2').text(), tmp_match)
result['boxRate'] = rebuild_number(movie.find('.c3').text(), tmp_match)
result['showRate'] = rebuild_number(movie.find('.c4').text(), tmp_match)
result['avgSeatView'] = rebuild_number(movie.find('.c5').text(), tmp_match)
yield result
def main():
start_date = datetime.date.today()
for i in range(0, 31):
date = start_date - datetime.timedelta(days=i)
html = get_one_page(date.isoformat())
for result in parse_one_page(html):
print(result)
time.sleep(1)
if __name__ == '__main__':
main()
使用 requests 庫,通過 get_one_page(date)
實現指定日期網頁請求;再利用 parse_one_page(html)
資料的提取,其中,首先呼叫 parse_font(html)
構建當前數字與加密字串的對應關係,再利用 rebuild_number(number, tmp_match)
實現加密字串的到數字的轉換過程。
3 參考資料
- 大齡碼農的Python之路, 丹楓無跡, Python爬蟲例項:爬取貓眼電影——破解字型反爬, 2019/8/13.
- 《Python3網路爬蟲開發實戰》, 崔慶才,2019/8/13.