大眾點評餐飲資料爬取(2020.11)

WeoSuper發表於2020-11-10

一、目標資料介紹

​ 爬取物件為大眾點評網北京地區“美食”標籤下參照“人氣”自動排序得到的750條餐館資料,示例如下:

1.1 屬性值介紹

​ 需要爬取的屬性值,如下表所示:

屬性 命名 資料型別
店名 title str
星級評分 star float
評價數 review_num int
人均消費 cost int
特徵 feature str
地址 address str

1.2 資料排列規律分析

​ 通過瀏覽大眾點評內容頁,可以發現,每頁包含最多15條記錄,共50頁。因此,可能需要通過迴圈函式多次爬取。

​ 進一步,注意到第一頁的URL地址為http://www.dianping.com/beijing/ch10/o2。很容易想到ch10和o2代表資料類別,檢視下一頁,可以看到,第二頁的URL為http://www.dianping.com/beijing/ch10/o2p2。因此可以推斷,350頁的URL應為*/ch10/o2p3*/ch10/o2p50。

1.3 資料提取路徑

​ 通過後臺讀取頁面原始碼,可以看到,單一頁面的店鋪記錄資訊都儲存在ID為shop-list-all-list的div標籤中,如下圖所示,每個li標籤即為一條店鋪記錄。

​ 點開li標籤後,可以看到需要的屬性資料都儲存在class='txt'的div標籤內。

​ 以店名title為例,可以看到店名就儲存在h4子標籤中。

​ 相似的,可以遞推尋得star、review_num、cost、feature、address等屬性的儲存路徑。

1.4 大眾點評反爬策略

​ 在此次資料爬取過程中發現,大眾點評針對於網路爬蟲對資料進行了加密,如下所示:

​ 網頁顯示的資料正常,人民幣符號'¥'加上數字表示人均消費。

​ 然而,在後臺顯示的原始碼中,部分數字顯示為亂碼,無法正常讀取。

​ 這是大眾點評採用的字型反爬措施導致的,如果直接按照常規方法讀取子標籤資訊,爬下來的資料也會顯示為亂碼。解決方案將在後文提出。

二、爬取流程

2.1 requests訪問目標網頁並獲取HTML原始碼

import requests
from lxml import etree

page_url = 'http://www.dianping.com/beijing/ch10/o2'
# 新增headers資訊,User-Agent模擬正常瀏覽器訪問,cookie為當需要登入授權時使用。
page_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", "cookie": "your cookie"}

# 建立http連結
res_http = requests.get(page_url, headers=page_headers)
html = etree.HTML(res_http.text)
# res_http.text即為網頁的html原始碼,資料型別為str。但無法解析,需要通過lxml模組的etree.HTML函式解析為xml格式,建立樹狀結構資料格式,並返回值賦給html物件

2.2 解析HTML獲得子標籤資訊

# 在得到經過解析的html資訊後,開始迴圈解析獲得單個飯店的資訊
# 為了避免存在某一頁飯店記錄數不為15,需要檢測當前頁面飯店記錄數量
# 經過分析,可以採用<div class="tit">字串作為檢測標籤

import re # 呼叫正規表示式模組用於解析

data = [] # 以字典列表形式儲存資料,列表內每一條記錄為一個字典資料結構,代表一家飯店
# 獲取當前頁飯店數量
record_tags = re.findall(re.compile(r'<div class=\"tit\">'), res_http.text)
nRecord = len(record_tags)
for n in range(nRecord):
    ID = n + 1 # 飯店記錄序號
    values = [] # 鍵值列表,每個迴圈均重置
    values.append(getTitle(ID, html)) # 提取店名
    values.append(getStar(ID, html)) # 提取星級評分
    values.append(getReviewNum(ID, html, woff_dict_shopNum)) # 提取評論數
    values.append(getCost(ID, html, woff_dict_shopNum)) # 提取平均每人消費
    values.append(getFeature(ID, html, woff_dict_tagName)) # 提取飯店標籤
    values.append(getAddress(ID, html, woff_dict_address)) # 提取地址
    values.append(getRecommend(ID,html)) # 提取推薦菜品
    # 建立字典並存入資料列表
    data.append(dict(zip(keys, values)))

​ getTitle等函式為自定義函式,用於執行解析的具體操作。以getTitle為例:

def getTitle(ID, html):
	para_ID: 	代表單一頁面中店面從上至下的排列序號(1~15)
    para_html:	經過樹狀結構化的html原始碼即步驟2.1中得到的html物件
    func:		根據子標籤的xpath路徑提取其值
~~~
return html.xpath('//*[@id="shop-all-list"]/ul/li['+str(ID)+']/div[2]/div[1]/a/h4')[0].text

​		此時,我們已經能夠解析得到頁面內單一店鋪的相關資料。結合2.1及2.2,很容易構建出完整的資料結構實現自動爬取資料。列印data物件,如圖所示:

![image-20201110152541680](C:\Users\32188\AppData\Roaming\Typora\typora-user-images\image-20201110152541680.png)

​		上圖顯示的"\u"開頭的字串為已經過反反爬解析後得到的unicode編碼,在沒有經過反反爬之前,輸出得到的會是無意義的方框,例如:

![image-20201110152816409](C:\Users\32188\AppData\Roaming\Typora\typora-user-images\image-20201110152816409.png)

接下來介紹反反爬思路。

#### 2.3 反反爬

​		經過查詢資料,發現大眾點評採用的是Web字型反爬策略。通過建立自定義字型,改變一定數量的常用字元的Unicode編碼,由於伺服器端記錄了新字型與Unicode編碼之間的對映關係,網頁端能夠識別改變了Unicode編碼後的字元並正常顯示。然而,當爬蟲直接爬取HTML原始碼並訪問子標籤值時,由於本地沒有對應的字型檔案,就無法正常解析Unicode編碼,從而顯示為方框。

​		因此,只需要獲取對應的字型檔案,並在本地建立常用字元和特定Unicode編碼間的對映關係,然後在2.2的解析過程中進行替換即可。

##### 2.3.1 字型檔案

​		由於前端頁面字型由CSS檔案決定,故從這個角度出發,找到CSS檔案,就有可能找到對應的字型檔案。

​		以address屬性為例,瀏覽器後臺找到對應程式碼段,如圖所示:

![image-20201110154623418](C:\Users\32188\AppData\Roaming\Typora\typora-user-images\image-20201110154623418.png)

​		下方Styles欄可以看到class=address的所有標籤所依賴的CSS檔案。

![image-20201110154844552](C:\Users\32188\AppData\Roaming\Typora\typora-user-images\image-20201110154844552.png)

​		開啟該檔案,可以發現,它定義了“reviewTag”、“address”、“shopNum”、“tagName”四類標籤對應的字型檔案,如圖:

![image-20201110160006817](C:\Users\32188\AppData\Roaming\Typora\typora-user-images\image-20201110160006817.png)

​		從而能夠通過解析該CSS檔案獲取對應的字型檔案。由於該檔案命名沒有規律,大概率為隨機生成,故需要通過定位其在HTML原始碼中的引用語句來獲取,如下:

![image-20201110155106550](C:\Users\32188\AppData\Roaming\Typora\typora-user-images\image-20201110155106550.png)

​		很明顯,"svgtextcss"可以作為定位該段URL的唯一標籤。藉助於正規表示式,就可以從HTML原始碼中將其提取出來。

​~~~python
# 抓取woff檔案,res_http為2.1requests模組發出get請求得到的返回值,.text屬性為頁面HTML原始碼的字串
woff_file = getWoff(res_http.text)

def getWoff(page_html):
	para_page_html:	HTML頁面原始碼字串
    func:			解析CSS檔案並下載woff字型檔案至本地
~~~
woff_files = []
# 提取css檔案url
css_str = re.findall(re.compile(r'//.*/svgtextcss/.*\.css'), page_html)[0]
css_url = 'https:' + css_str
# http訪問css檔案解析得到woff檔案
res_css = requests.get(css_url).text
woff_urls = re.findall(re.compile(r'//s3plus.meituan.net/v1/mss_\w{32}/font/\w{8}\.woff'), res_css)
tags = ['tagName', 'reviewTag', 'shopNum', 'address']
for nNum, url in enumerate(woff_urls):
    res_woff = requests.get('http:' + url)
    with open('./resources/woff/'+tags[nNum]+'.woff', 'wb') as f:
        f.write(res_woff.content)
    woff_files.append('./resources/woff/'+tags[nNum]+'.woff')
return dict(zip(tags, woff_files))

​		在得到woff檔案後,可以通過FontCreator軟體開啟,如圖所示:

![image-20201110161624070](C:\Users\32188\AppData\Roaming\Typora\typora-user-images\image-20201110161624070.png)

可以看到,總共603個常用字元,按照一定順序排列並編碼。因此,只要獲取字元與Unicode編碼的對映關係就可以將反爬字型替換為普通編碼方式的常用字型了。

##### 2.3.2 字元對映關係解析

​		在獲取woff字型檔案後,需要解析字元與Unicode編碼間的關係。呼叫fontTools模組即可。

​~~~python
from fontTools.ttLib import TTFont

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

​ 最終就能解析得到特定woff檔案對應的對映關係字典woff_dict,在步驟2.2的解析過程中參與解析便可將反爬字型置換為常用字型。

2.3.3 附加

​ 由於包含字型檔案資訊的CSS檔案為隨機生成,其內容順序不是固定的,步驟2.3.1的解析過程已經預設四類標籤順序固定,在實際應用中,需要構建更普適的資料結構以正確提取字型檔案。

相關文章