今天為大家重寫一個美團美食板塊小爬蟲,說不定哪天做旅游攻略的時候也可以用下呢。廢話不多說,讓我們愉快地開始吧~
開發工具
Python版本:3.6.4
相關模塊:
requests模塊;
argparse模塊;
pyquery模塊;
jieba模塊;
pyecharts模塊;
wordcloud模塊;
以及一些Python自帶的模塊。
環境搭建
安裝Python並添加到環境變量,pip安裝需要的相關模塊即可。
原理簡介
前期准備:
因為我想讓這個小爬蟲可以爬取美團上任意城市美食板塊的數據,但是每個城市的URL是不一樣的,其格式為:
https://{城市拼音縮寫}.meituan.com/
不同的城市需要不同的URL來構造請求從而爬取我們所需要的數據,於是現在的問題就變成了:如何獲取所有城市對應的城市拼音縮寫呢?
其實很簡單,點擊網頁上的切換城市按鈕:

然后查看網頁源代碼:

於是我們很easy地就可以爬取所有城市對應的城市拼音縮寫了,代碼實現如下:
'''城市名-拼音碼爬取'''
def downCitynamesfile(citynamesfilepath):
url = 'https://www.meituan.com/changecity/'
doc = PyQuery(requests.get(url).text)
cities_dict = dict()
[cities_dict.update({city.text(): city.attr('href').replace('.', '/').split('/')[2]}) for city in doc('.cities a').items()]
with open(citynamesfilepath, 'w', encoding='utf-8') as f:
f.write(json.dumps(cities_dict, indent=2, ensure_ascii=False))
爬蟲主程序:
現在隨便切換到一個城市,以杭州為例。簡單抓個包,可以發現美食商家的數據可以通過請求下圖這個URL獲得:

其構造方式為上圖紅框框出的baseURL加上下圖所示的一堆參數:

其中變量為:
cityName:城市名
page:頁碼
uuid:uuid
_token:_token
其他均為不變量,直接copy過來就行了。前面兩個變量很明顯是什么,就不多說了。變量uuid在網頁源代碼里就能找到:

至於_token,稍微麻煩一點。考慮到_token結尾出現了=,所以猜測是base64編碼,但是解碼后發現是一堆16進制ASCII碼,所以考慮原數據是先進行二進制壓縮然后base64編碼的。反向操作一波,發現果然是這樣的:

全局搜索找生成相關參數的源代碼:

一頓分析之后就可以開始寫_token生成的代碼了,具體如下:
'''獲取SIGN'''
def getSIGN(cityname, page, uuid, city_code):
url = 'https://{}.meituan.com/meishi/'.format(city_code)
sign = 'areaId=0&cateId=0&cityName={}&dinnerCountAttrId=&optimusCode=1&originUrl={}&page={}&partner=126&platform=1&riskLevel=1&sort=&userId=&uuid={}'
sign = sign.format(cityname, url, page, uuid)
return sign
'''獲取_token參數'''
def getToken(brfilepath, city_code, uuid, page, cityname):
ts = int(time.time() * 1000)
with open(brfilepath, 'r') as f:
brs_dict = json.load(f)
key = random.choice(list(brs_dict.keys()))
info = brs_dict[key]
_token = {
'rId': 100900,
'ver': '1.0.6',
'ts': ts,
'cts': ts + random.randint(100, 120),
'brVD': info.get('barVD'),
'brR': [info.get('brR_one'), info.get('brR_two'), 24, 24],
'bI': ['https://{}.meituan.com/meishi/'.format(city_code),''],
'mT': [],
'kT': [],
'aT': [],
'tT': [],
'aM': '',
'sign': getSIGN(cityname, page, uuid, city_code)
}
return base64.b64encode(zlib.compress(str(_token).encode())).decode()
OK,知道了baseURL,獲得了所有參數,我們就可以愉快地寫主程序了:
'''主函數'''
def MTSpider(cityname, maxpages=50):
data_pages = {}
citynamesfilepath, uafilepath, uuidfilepath, brfilepath, savedatapath = initialProgram(cityname)
base_url = 'https://{}.meituan.com/meishi/api/poi/getPoiList?'.format(cityname2CODE(cityname, citynamesfilepath))
try:
for page in range(1, maxpages+1):
print('[INFO]: Getting the data of page<%s>...' % page)
data_page = None
while data_page is None:
params = getGETPARAMS(cityname, page, citynamesfilepath, uuidfilepath, brfilepath)
url = base_url + urlencode(params)
headers = {
'Accept': 'application/json',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'User-Agent': getRandomUA(uafilepath),
'Connection': 'keep-alive',
'Host': 'bj.meituan.com',
'Referer': 'https://{}.meituan.com/'.format(cityname2CODE(cityname, citynamesfilepath))
}
res = requests.get(url, headers=headers)
data_page = parsePage(json.loads(res.text))
if data_page is None:
time.sleep(random.random()+random.randint(3, 6))
initialProgram(cityname)
data_pages.update(data_page)
if page != maxpages:
time.sleep(random.random()+random.randint(3, 6))
except:
print('[Warning]: Something wrong...')
with open(savedatapath, 'wb') as f:
pickle.dump(data_pages, f)
其中解析返回的json數據的函數如下:
'''解析一頁數據'''
def parsePage(data_page):
data_parse = dict()
infos = data_page.get('data')
if infos is None:
return None
else:
infos = infos.get('poiInfos')
for info in infos:
# 店名: 地址, 評論數量, 平均得分, 平均價格
data_parse[info.get('title')] = [info.get('address'), info.get('allCommentNum'), info.get('avgScore'), info.get('avgPrice')]
return data_parse
一些細節和tricks就不細說了。
All Done****!完整源代碼詳見主頁個人介紹獲取相關文件。
數據可視化
按慣例隨手可視化一波,以抓取的杭州美食數據為例吧(這里只爬取了前50頁),省的重新爬了。
先來搞個詞雲玩玩吧,用爬到的所有商家名/商家地址來搞個詞雲:


然后我們假設美食性價比的定義為(這個假設很可能是不合理,這里只是為了方便自己做下簡單的數據分析隨便假設了一下。):
性價比 = 評論數量 x 平均得分 / 平均價
於是我們可以得到"杭州性價比最高的十家店"為(只是個小例子,不供參考,如有雷同,不勝榮幸。):
