一、目標數據介紹
爬取對象為大眾點評網北京地區“美食”標簽下參照“人氣”自動排序得到的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):
return html.xpath('//*[@id="shop-all-list"]/ul/li['+str(ID)+']/div[2]/div[1]/a/h4')[0].text
此時,我們已經能夠解析得到頁面內單一店鋪的相關數據。結合2.1及2.2,很容易構建出完整的數據結構實現自動爬取數據。打印data對象,如圖所示:
上圖顯示的"\u"開頭的字符串為已經過反反爬解析后得到的unicode編碼,在沒有經過反反爬之前,輸出得到的會是無意義的方框,例如:
接下來介紹反反爬思路。
2.3 反反爬
經過查找資料,發現大眾點評采用的是Web字體反爬策略。通過創建自定義字體,改變一定數量的常用字符的Unicode編碼,由於服務器端記錄了新字體與Unicode編碼之間的映射關系,網頁端能夠識別改變了Unicode編碼后的字符並正常顯示。然而,當爬蟲直接爬取HTML源碼並訪問子標簽值時,由於本地沒有對應的字體文件,就無法正常解析Unicode編碼,從而顯示為方框。
因此,只需要獲取對應的字體文件,並在本地建立常用字符和特定Unicode編碼間的映射關系,然后在2.2的解析過程中進行替換即可。
2.3.1 字體文件
由於前端頁面字體由CSS文件決定,故從這個角度出發,找到CSS文件,就有可能找到對應的字體文件。
以address屬性為例,瀏覽器后台找到對應代碼段,如圖所示:
下方Styles欄可以看到class=address的所有標簽所依賴的CSS文件。
打開該文件,可以發現,它定義了“reviewTag”、“address”、“shopNum”、“tagName”四類標簽對應的字體文件,如圖:
從而能夠通過解析該CSS文件獲取對應的字體文件。由於該文件命名沒有規律,大概率為隨機生成,故需要通過定位其在HTML源碼中的引用語句來獲取,如下:
很明顯,"svgtextcss"可以作為定位該段URL的唯一標簽。借助於正則表達式,就可以從HTML源碼中將其提取出來。
# 抓取woff文件,res_http為2.1requests模塊發出get請求得到的返回值,.text屬性為頁面HTML源碼的字符串
woff_file = getWoff(res_http.text)
def getWoff(page_html):
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軟件打開,如圖所示:
可以看到,總共603個常用字符,按照一定順序排列並編碼。因此,只要獲取字符與Unicode編碼的映射關系就可以將反爬字體替換為普通編碼方式的常用字體了。
2.3.2 字符映射關系解析
在獲取woff字體文件后,需要解析字符與Unicode編碼間的關系。調用fontTools模塊即可。
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的解析過程已經預設四類標簽順序固定,在實際應用中,需要構建更普適的數據結構以正確提取字體文件。