前言
今天在群裡有人派外包的其中一個是抓取大衆點評的店鋪資訊,價格一千五,三天内完成!很多剛學程式設計的小夥伴,應該覺得這個一千五也太容易拿了,相對于大衆點評這個網站,其實不是像爬小說,爬表情包那樣容易的,大衆點評這個網站它是有反爬的!其實也就兩個點!
1.正規表達式撰寫
2.破解字型反爬
這個網站難一點的就這兩個地方!其他都和爬其他網站的思路基本差不多,如果有新手小夥伴的,那今天就跟小編一起來完成這個1500塊錢的外包項目吧!
正文
需要安裝的庫:
import urllib.request
from bs4 import BeautifulSoup
import re
from fontTools.ttLib import TTFont
import xlwt
過程中使用的部分軟體:
- 正規表達式測試器
- fontcreator
- 合适的OCR軟體
記錄操作步驟及代碼如下所示:
1. 網頁解析
1.1 爬取資料解析
選擇餐飲店鋪數量較多的上海萬象城店,搜尋結果界面如下:
- 每頁顯示15條資料,共10頁内容
- 每條店鋪資訊包含内容如下:
店鋪名稱 | 是否提供團購/為分店/廣告 |
星級、評分、評價人數、人均價格 | 口味、環境、服務 |
菜品種類、位址分區、詳細位址 | |
推薦菜 | |
團購資訊、優惠資訊 |
1.2 網址解析
首頁URL位址:http://www.dianping.com/search/keyword/1/10_%E4%B8%87%E8%B1%A1%E5%9F%8E](http://www.dianping.com/search/keyword/1/10_萬象城)
第二頁URL位址:http://www.dianping.com/search/keyword/1/10_%E4%B8%87%E8%B1%A1%E5%9F%8E/p2](http://www.dianping.com/search/keyword/1/10_萬象城/p2)
第三頁URL位址:http://www.dianping.com/search/keyword/1/10_%E4%B8%87%E8%B1%A1%E5%9F%8E/p3
建立循環:
for i in range(1,11)
baseURL = 'http://www.dianping.com/search/keyword/1/10_%E4%B8%87%E8%B1%A1%E5%9F%8E/p'
URL = baseURL + str(i)
1.3 登陸處理
大衆點評的網頁翻頁需要登陸。這裡采用手機驗證碼的方式登陸,使用開發者工具提取cookie、User-Agent,打包為headers。
1.4 定義爬取函數askURL
def askURL(URL):
head = {"User-Agent": "", “cookie": ""}#保密原因,省略使用的User-Agent與Cookie
request = urllib.request.Request(URL, headers=head)
html = ""
html = urllib.request.urlopen(request).read().decode('utf-8') #使用UTF-8解碼
return (html)
2. 資料爬取與提取
2.1 資料爬取
循環調用askURL函數,爬取每頁資訊,儲存在字元串變量html中
def getData(baseURL):
for i in range(1,10):
URL = baseURL + str(i)
html = askURL(URL)#html是askURL的傳回結果,循環下的html記錄單頁的爬取結果,是以資料解析提取也需要在循環内進行
使用開發者工具讀取源碼,可以看到全部的店鋪資訊儲存在ID為shop-list-all-list的div标簽中,每個li标簽為一條店鋪記錄。其中pic分類記錄縮略圖、txt分類記錄店鋪資訊,svr-info記錄團購資訊
2.2 使用BeautifulSoup
方案1:提取多個标簽,手動合并
soup = BeautifulSoup(html, "html.parser")
soupfind = soup.find_all('div', { 'class' :{"pic" , "txt" , "svr-info"}})#提取多個标簽下資訊時的處理方式,會提取為3個清單,需要手動合并為一個
#僅提取單個标簽時的寫法
# soupfind = soup.find_all('div', class_ :"txt" )
#合并過程(僅供參考)
soup_find = []
i = 0
while i < len(soupfind):
l = ""
l = str(soupfind[i]) + str(soupfind[i+1]) + str(soupfind[i+2])
soup_find.append(l)
i += 3
但後續操作中發現,部分店鋪不含團購資訊,導緻”svr-info“class下面為空值,每三個合并出現錯誤
方案2:由于每個店鋪的全部資訊含在一個<li>标簽下
def getData(baseURL):
for i in range(1, 11):
URL = baseURL + str(i)
html = askURL(URL)
soup = BeautifulSoup(html, "html.parser")
soup_find = soup.find_all('li', class_ = "")
但這種方式會提取出一些非店鋪的分支,會造成正規表達式搜尋結構出現大量空值,帶來混亂,關注到title與店鋪資訊間必然存在一一對應關系,後續将采用這一方法剔除非店鋪分支。
2.3 正規表達式
全部資訊的正規表達式提取如下:
for item in soup_find:
item = str(item)
imgsrc = re.findall(re.compile(r'src="http(.*?)://(.*?).meituan.net/(.*?)"'), item) # 圖檔
title = re.findall(re.compile(r'<h4>(.*?)</h4>'), item) # 店名
if title == []: #<li>标簽會篩選出一些非店鋪的分支,由于title是店鋪資訊一定含有的資訊,使用title篩選,留下店鋪資訊
pass
else:
if imgsrc == []:
img = []
else:
img = ["http" + (imgsrc[0])[0] + "://" + (imgsrc[0])[1] + ".meituan.net/" + (imgsrc[0])[2]]
branch = re.findall(re.compile(r'分店'), item) # 分店
if branch == "分店":
ifbranch = ["1"]
else:
ifbranch = ["0"]
if re.findall(re.compile(r'<div class="star_score score_(.*?) star_score_sml">(.*?)</div>'), item) == []:
star = []
score = []
else:
star = (re.findall(re.compile(r'<div class="star_score score_(.*?) star_score_sml">(.*?)</div>'), item)[0])[
0] # 星級
score = (re.findall(re.compile(r'<div class="star_score score_(.*?) star_score_sml">(.*?)</div>'), item)[0])[
1] # 評分
numraters = re.findall(re.compile(r'<b>(.*?)</b>\s*條評價</a>'), item) # 評分人數
avgprice = re.findall(re.compile(r'人均\s*<b>¥(.*?)</b>'), item) # 人均價格
taste = re.findall(re.compile(r'<span>口味<b>(.*?)</span>'), item) # 口味
enviro = re.findall(re.compile(r'<span>環境<b>(.*?)</span>'), item) # 環境
serv = re.findall(re.compile(r'<span>服務<b>(.*?)</span>'), item) # 服務
variety = re.findall(
re.compile(r'<a data-click-name="shop_tag_cate_click".*?"><span class="tag">(.*?)</span></a>'),
item) # 菜品種類
zone = re.findall(
re.compile(r'<a data-click-name="shop_tag_region_click".*?"><span class="tag">(.*?)</span></a>'),
item) # 位址分區
address = re.findall(re.compile(r'<span class="addr">(.*?)</span>\s*</div>'), item) # 詳細位址
recommend = re.findall(re.compile(
r'<a class="recommend-click" data-click-name="shop_tag_dish_click".*?target="_blank">(.*?)</a>'),
item) # 推薦菜
tg_info = re.findall(re.compile(
r'<a data-click-name="shop_info_groupdeal_click" href=".*?" target="_blank" rel="external nofollow" target="_blank" title=".*?">\s*<span class="tit">團購:</span>(.*?)\s*</a>'),
item) # 團購資訊
coupon = re.findall(re.compile(r'<span class="tit">優惠:</span>(.*?)\s*</a>'), item) # 優惠資訊
data = []
data.append(img)
data.append(title)
data.append(ifbranch)
data.append(star)
data.append(score)
data.append(numraters)
data.append(avgprice)
data.append(taste)
data.append(enviro)
data.append(serv)
data.append(variety)
data.append(zone)
data.append(address)
data.append(recommend)
data.append(tg_info)
data.append(coupon)
datalist.append(data)
提取結果:
[['http://p0.meituan.net/biztone/193986969_1624004529519.jpeg%40340w_255h_1e_1c_1l%7Cwatermark%3D0'], ['桂滿隴-錦繡江南(萬象城店)'], [0], '45', '4.66', ['<svgmtsi class="shopNum">\uec0e</svgmtsi><svgmtsi class="shopNum">\ue10c</svgmtsi>1<svgmtsi class="shopNum">\uf819</svgmtsi>'], ['<svgmtsi class="shopNum">\uec0e</svgmtsi><svgmtsi class="shopNum">\ue498</svgmtsi>'], ['<svgmtsi class="shopNum">\ue82c</svgmtsi>.<svgmtsi class="shopNum">\uf819</svgmtsi><svgmtsi class="shopNum">\ue3b9</svgmtsi></b>'], ['<svgmtsi class="shopNum">\ue82c</svgmtsi>.<svgmtsi class="shopNum">\uecd2</svgmtsi><svgmtsi class="shopNum">\ue82c</svgmtsi></b>'], ['<svgmtsi class="shopNum">\ue82c</svgmtsi>.<svgmtsi class="shopNum">\ue3b9</svgmtsi><svgmtsi class="shopNum">\uf819</svgmtsi></b>'], ['浙<svgmtsi class="tagName">\ue913</svgmtsi>'], ['<svgmtsi class="tagName">\uf35e</svgmtsi><svgmtsi class="tagName">\ue12a</svgmtsi><svgmtsi class="tagName">\ueac9</svgmtsi>'], ['吳<svgmtsi class="address">\uf136</svgmtsi><svgmtsi class="address">\uf36b</svgmtsi>1599<svgmtsi class="address">\uf75f</svgmtsi><svgmtsi class="address">\uea4c</svgmtsi>象<svgmtsi class="address">\uf7d0</svgmtsi>4<svgmtsi class="address">\uf370</svgmtsi>402A<svgmtsi class="address">\uf089</svgmtsi><svgmtsi class="address">\ue668</svgmtsi>'], ['吮指雞爪', '石鍋沃豆腐', '公主素蟹粉'], ['桂滿隴·西湖船宴!僅售388元!價值454元的招牌必嘗四人餐1份,提供免費WiFi。'], []
3. 字型反爬
3.1 字型反爬方式
上圖爬出的資料中有較多
<svgmtsi class="shopNum">\uec0e</svgmtsi><svgmtsi class="shopNum">\ue10c</svgmtsi>1<svgmtsi class="shopNum">\uf819</svgmtsi>
類型的亂碼。這是因為大衆點評采用Web字型反爬的方式。
”即通過建立自定義字型,改變部分常用字元的Unicode編碼。由于伺服器端記錄了新字型與Unicode編碼之間的映射關系,網頁端能夠識别改變了Unicode編碼後的字元并正常顯示。然而,當爬蟲直接爬取HTML源碼并通路子标簽值時,由于本地沒有對應的字型檔案,就無法正常解析Unicode編碼,進而顯示為方框。“(來源:https://www.cnblogs.com/weosuper/p/13954662.html)
以評價為例,直接爬取的資訊會是亂碼的形式,是以需要進一步對爬取資料進行解碼。
3.2 擷取網頁字型庫
以shopNum字型為例,提取css檔案,位址
http://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/3a1fbc361fa4c6a862e661e34778709f.css
可以看到網頁共定義了4種字型:PingFangSC-Regular-shopNum;PingFangSC-Regular-tagName;PingFangSC-Regular-address;PingFangSC-Regular-reviewTag。(進一步分析可發現字型檔案名兩兩一緻,即網頁端實際上使用了兩種字型,這會為之後的解碼減輕一定工作量)
搜尋找到該URL在HTML源碼中的位置
進一步縮小範圍,發現svgtextcss可以唯一對應該字段。據此,使用正規表達式提取位址,通路css檔案,在解碼後的檔案中再次使用正規表達式解析得到woff字型檔案存儲。此段代碼如下:
def getWoff(baseURL):
url = baseURL + "1"
html = askURL(url)
# print(html)
css_str = re.findall(re.compile(r'href="(.*?)svgtextcss(.*?).css" target="_blank" rel="external nofollow" >'), html)
css_url = "http:" + css_str[0][0] + "svgtextcss" +css_str[0][1] + ".css"
html_css = str(urllib.request.urlopen(urllib.request.Request(css_url)).read())
#沒有登陸、反爬操作,可以不加header直接讀取源碼
#urllib.request傳回的是byte類型
woff_url = re.findall(re.compile(r'//s3plus.meituan.net/v1/mss_\w{32}/font/\w{8}.woff'),html_css)
print(woff_url)
tags = ['shopNum','tagName','address','reviewTag']
woff_files = []
for nNum, url in enumerate(woff_url):
res_woff = urllib.request.urlopen(urllib.request.Request("http:" + url)) #傳回類别:HTTPResponce
print(res_woff)
with open('./woff/' + tags[nNum] + '.woff',"wb") as f:#需要在py檔案路徑下建立woff檔案夾
f.write(res_woff.read()) #讀取檔案
woff_files.append('./woff/' + tags[nNum] + '.woff')
return(dict(zip(tags, woff_files)))
之後将在建立的woff檔案夾中生成4個字型檔案:
使用fontcreator打開woff包,可看到字型反爬中加密的字型:
3.3 建立新的映射
建立兩個新的映射:shopNum和tagName。其中address與shopNum映射方式一緻,tagName與reviewTag映射方式一緻。
shopNum的映射方式建立如下所示:
shopNum = TTFont('./woff/shopNum.woff')
woff_unicode = shopNum['cmap'].tables[0].ttFont.getGlyphOrder()
woff_glpy_shopNum = []
for i in woff_unicode:
woff_glpy_shopNum.append(i.replace("uni", "u"))
woff_str_601 = '1234567890店中美家館小車大市公酒行國品發電金心業商司超生裝園場食有新限天面工服海華水房飾城樂汽香部利子老藝花專東肉菜學福飯人百餐茶務通味所山區門藥銀農龍停尚安廣鑫一容動南具源興鮮記時機烤文康信果陽理鍋寶達地兒衣特産西批坊州牛佳化五米修愛北養賣建材三會雞室紅站德王光名麗油院堂燒江社合星貨型村自科快便日民營和活童明器煙育賓精屋經居莊石順林爾縣手廳銷用好客火雅盛體旅之鞋辣作粉包樓校魚平彩上吧保永萬物教吃設醫正造豐健點湯網慶技斯洗料配彙木緣加麻聯衛川泰色世方寓風幼羊燙來高廠蘭阿貝皮全女拉成雲維貿道術運都口博河瑞宏京際路祥青鎮廚培力惠連馬鴻鋼訓影甲助窗布富牌頭四多妝吉苑沙恒隆春幹餅氏裡二管誠制售嘉長軒雜副清計黃訊太鴨号街交與叉附近層旁對巷棟環省橋湖段鄉廈府鋪内側元購前幢濱處向座下臬鳳港開關景泉塘放昌線灣政步甯解白田町溪十八古雙勝本單同九迎第台玉錦底後七斜期武嶺松角紀朝峰六振珠局崗洲橫邊濟井辦漢代臨弄團外塔楊鐵浦字年島陵原梅進榮友虹央桂沿事津凱蓮丁秀柳集紫旗張谷的是不了很還個也這我就在以可到錯沒去過感次要比覺看得說常真們但最喜哈麼别位能較境非為歡然他挺着價那意種想出員兩推做排實分間甜度起滿給熱完格薦喝等其再幾隻現朋候樣直而買于般豆量選奶打每評少算又因情找些份置适什蛋師氣你姐棒試總定啊足級整帶蝦如态且嘗主話強當更闆知己無酸讓入啦式笑贊片醬差像提隊走嫩才剛午接重串回晚微周值費性桌拍跟塊調糕'
#這裡601個字元的提取方式為:使用OCR軟體識别fontcreator中顯示的字形,按順序轉換為字元串。此處工作較為繁瑣,但目前沒有找到比較合适的其他辦法
woff_character = ['.notdef', 'x'] + list(woff_str_601)
woff_dict_shopNum = dict(zip(woff_glpy_shopNum, woff_character))
将datalist中的代碼轉譯為字元。
def woff1(num):
num1 = (repr(num).replace("'", "").replace("[", "").replace("]", "").replace("\\","")).split("</svgmtsi>")
#repr(str):相當于r',否則轉義字元生效,"'","\"無法被替換
t = 0
while t < len(num1):
if num1[t][-5:] in woff_dict_shopNum.keys():
#由于并非所有的字元都被加密,會出現”未加密字元+加密字元“的結構。觀察發現加密字元長度為5位,這裡提取後5位,并将組合結構拆開。
num1.insert(t,num1[t][:-5])
num1[t+1] = woff_dict_shopNum[num1[t+1][-5:]]
elif num1[t][-6:-1] in woff_dict_shopNum.keys():
num1.insert(t,num1[t][-1:])
num1[t+1] = woff_dict_shopNum[num1[t+1][-6:-1]]
elif num1[t][-7:-2] in woff_dict_shopNum.keys():
num1.insert(t,num1[t][-2:])
num1[t+1] = woff_dict_shopNum[num1[t+1][-7:-2]]
#防止後接數字/字元的情況
t = t + 1
return(num1)
def getshopNum(datalist):
for i in datalist:
for j in range (5,10):
num = i[j][0].replace('<svgmtsi class="shopNum">',"").replace("</b>","")
num1 = woff1(num)
i.pop(j)
i.insert(j,num1)
return(datalist)
4. 存儲到excel表格中
簡單套用代碼即可,不再贅述。
def saveData(datalist,savepath):
book = xlwt.Workbook(encoding="utf-8", style_compression=0)
sheet = book.add_sheet("dzdp", cell_overwrite_ok=True)
col = ("圖檔", "店名","分店(1:為分店)", "星級","評分", "評分人數", "人均價格","口味","環境","服務", "菜品種類", "位址分區","詳細位址","推薦菜","團購資訊","優惠資訊")
for i in range(0, 16):
sheet.write(0, i, col[i])
for i in range(0, 150):
print("第%d條" % i)
data = datalist[i]
for j in range(0, 16):
if j == 13:
sheet.write(i + 1, j, ";".join(data[j])) #推薦菜用:分隔
else:
sheet.write(i + 1, j, "".join(data[j]))
book.save('dzdp.xls')
print("save.")
def PrintResult(datalist):
result = []
for i in (0 , 16):
res = []
for j in datalist[i]:
res.append(datalist[i][j])
result.append(res)
print(result)
打開excel,可以看到成功提取出150條資料,且不含亂碼,爬取成功。
5. 總結
5.1 要點
- 正規表達式測試時,注意每頁的資訊條數與搜尋獲得的條數,可以檢驗正規表達式品質。
- 字型反爬時,容易出錯的點在于分隔時将已有的字元與unicode誤分到同一個元素中,造成與字庫的比對失敗。本文中采用的解決方案是從尾端分隔,并提取相應位數。
- 字型反爬時,另一個花費較長時間解決的點是如何替換Unicode編碼中的”\“字元,解決方法是對字元串進行轉義操作。
5.2 初學者常犯的錯誤
- 檢查是否有typo、中英文符号是否正确、括号是否左右對應、函數定義與循環是否正确以冒号結尾,可以解決80%的問題。
- 清單與字典看起來易于掌握,但實際使用中使用不夠熟練則比較容易出錯,值得反複學習。
- 另一個常犯的錯誤是變量類型的轉換,尤其是”str“和”list“兩種類型的變量。
5.3 進一步改進的空間......
- 試圖以循環的方式比對字庫,但對變量循環命名掌握不夠熟練,嘗試幾次均失敗,不得已将兩個字庫分别定義為兩個函數。
- 是否能夠不通過大量的手動操作,而是有更好的辦法擷取字庫中的漢字?
- 如何使用soup.findall同時提取多個标簽,但用一個清單囊括一家店鋪對應的所有的資訊?
到這裡就差不多結束這個項目了!
如果還是有小夥伴通過文章感覺沒學會的!那就可以找小編拿視訊教程或者爬取整個大衆點評的一個源代碼分享給大家!免費的源碼,其實大衆點評更新挺快的,可能今天用這個方法可以實作,下次就換了!
需要完整視訊教程或者源碼的可以點選藍色字型:點選這裡 領取 或者加Q羣:754370353 自取即可,對于這快有疑問的,有不會的小夥伴可以直接進去提問哈!小編看到會第一時間回複大家!
如果覺得此篇文章對你有幫助記得給小編點贊評論哦!能轉發一下就更好拉!