我用Python爬取了全國(guó)4500個(gè)熱門(mén)景點(diǎn),告訴你國(guó)慶哪兒最堵?
金秋九月,丹桂飄香,在這秋高氣爽,陽(yáng)光燦爛的收獲季節(jié)里,我們送走了一個(gè)個(gè)暑假余額耗盡哭著走向校園的孩子們,又即將迎來(lái)一年一度偉大祖國(guó)母親的生日趴體(無(wú)心上班,迫不及待想為祖國(guó)母親慶生)。
那么問(wèn)題來(lái)了,去哪兒玩呢?百度輸了個(gè)“國(guó)慶”,出來(lái)的第一條居然是“去哪里旅游人少”……emmmmmmm,因缺思廳。
于是我萌生了通過(guò)旅游網(wǎng)站的景點(diǎn)銷量來(lái)判斷近期各景點(diǎn)流量情況的想法(這個(gè)想法很危險(xiǎn)啊)。
所以這次的目標(biāo)呢,是爬去哪兒網(wǎng)景點(diǎn)頁(yè)面,并得到景點(diǎn)的信息,大家可以先思考下大概需要幾步。
本文建議有一定 Python 基礎(chǔ)和前端(html,js)基礎(chǔ)的朋友閱讀,零基礎(chǔ)可以去看我之前的文。(咳咳,不能總更小白文,這樣顯得我不(mei)夠(you)專(xue)業(yè)(xi))。
百度的地圖 API 和 echarts
因?yàn)榍皫状闻老x(chóng)都是爬一些文本信息,做一下詞云之類的,我覺(jué)得:沒(méi)!意!思!了!這次正好爬的是數(shù)據(jù),我決定用數(shù)據(jù)的好基友——圖表來(lái)輸出我爬取的數(shù)據(jù),也就是說(shuō)我要用爬取的景點(diǎn)銷量以及景點(diǎn)的具體位置來(lái)生成一些可視化數(shù)據(jù)。
安利一下百度的地圖 API 和 echarts,前者是專門(mén)提供地圖 API 的工具,聽(tīng)說(shuō)好多 APP 都在用它,后者是數(shù)據(jù)處理居家旅行的好伙伴,用了之后,它好,我也好(隱約覺(jué)得哪里不對(duì))。
API 是什么,API 是應(yīng)用程序的編程接口,就好像插頭與插座一樣,我們的程序需要電(這是什么程序?),插座中提供了電,我們只需要在程序中寫(xiě)一個(gè)與插座匹配的插頭接口,就可以使用電來(lái)做我們想做的事情,而不需要知道電是如何產(chǎn)生的。
引入數(shù)據(jù)后的百度熱力圖
通過(guò) API 對(duì)接的開(kāi)發(fā)者與服務(wù)商
確定輸出文件
有人可能說(shuō),我已經(jīng)懂了 API 是啥意思了,可是咋個(gè)用呢。關(guān)于這一點(diǎn),我很負(fù)責(zé)任的告訴你:我也不會(huì)。
但是!百度地圖提供了很多 API 使用示例,有 html 基礎(chǔ),大致可以看懂,有 js 基礎(chǔ)就可以嘗試改函數(shù)了(不會(huì) js 的,我默默地復(fù)制源代碼),仔細(xì)觀察源代碼,可以知道熱力圖生成的主要數(shù)據(jù)都存放在 points 這個(gè)變量中。
這種[{x:x,x:x},{x:x,x:x}]格式的數(shù)據(jù),是一種 json 格式的數(shù)據(jù),由于具有自我描述性,所以比較通俗易懂,大概可以知道這里的三個(gè)值,前兩個(gè)是經(jīng)緯度,最后一個(gè)應(yīng)該是權(quán)重(我猜的)。
也就是說(shuō),如果我希望將景點(diǎn)的熱門(mén)程度生成為熱力圖,我需要得到景點(diǎn)的經(jīng)緯度,以及它的權(quán)重,景點(diǎn)的銷量可以作為權(quán)重,并且這個(gè)數(shù)據(jù)應(yīng)該是 json 格式的呈現(xiàn)方式。
echarts 也是一樣滴(*^__^*)。
爬取數(shù)據(jù)
這次的爬蟲(chóng)部分是比較簡(jiǎn)單的。分析網(wǎng)址(去哪兒景點(diǎn))→爬取分頁(yè)中信息(景點(diǎn)經(jīng)緯度、銷量)→轉(zhuǎn)為 json 文件。
分析去哪兒景點(diǎn)頁(yè)的網(wǎng)址,可得出結(jié)構(gòu):http://piao.qunar.com/ticket/list.htm?keyword=搜索地點(diǎn)®ion=&from=mpl_search_suggest&page=頁(yè)數(shù)
這次沒(méi)有用正則來(lái)匹配內(nèi)容,而使用了 xpath 匹配,非常好用。
- def getList():
- place = raw_input('請(qǐng)輸入想搜索的區(qū)域、類型(如北京、熱門(mén)景點(diǎn)等):')
- url = 'http://piao.qunar.com/ticket/list.htm?keyword='+ str(place) +'®ion=&from=mpl_search_suggest&page={}'
- i = 1
- sightlist = []
- while i:
- page = getPage(url.format(i))
- selector = etree.HTML(page)
- print '正在爬取第' + str(i) + '頁(yè)景點(diǎn)信息'
- i+=1
- informations = selector.xpath('//div[@class="result_list"]/div')
- for inf in informations: #獲取必要信息
- sight_name = inf.xpath('./div/div/h3/a/text()')[0]
- sight_level = inf.xpath('.//span[@class="level"]/text()')
- if len(sight_level):
- sight_level = sight_level[0].replace('景區(qū)','')
- else:
- sight_level = 0
- sight_area = inf.xpath('.//span[@class="area"]/a/text()')[0]
- sight_hot = inf.xpath('.//span[@class="product_star_level"]//span/text()')[0].replace('熱度 ','')
- sight_add = inf.xpath('.//p[@class="address color999"]/span/text()')[0]
- sight_add = re.sub('地址:|(.*?)|\(.*?\)|,.*?$|\/.*?$','',str(sight_add))
- sight_slogen = inf.xpath('.//div[@class="intro color999"]/text()')[0]
- sight_price = inf.xpath('.//span[@class="sight_item_price"]/em/text()')
- if len(sight_price):
- sight_price = sight_price[0]
- else:
- i = 0
- break
- sight_soldnum = inf.xpath('.//span[@class="hot_num"]/text()')[0]
- sight_url = inf.xpath('.//h3/a[@class="name"]/@href')[0]
- sightlist.append([sight_name,sight_level,sight_area,float(sight_price),int(sight_soldnum),float(sight_hot),sight_add.replace('地址:',''),sight_slogen,sight_url])
- time.sleep(3)
- return sightlist,place
- 這里把每個(gè)景點(diǎn)的所有信息都爬下來(lái)了(其實(shí)是為了練習(xí)使用 xpath……)。
- 使用了 while 循環(huán),for 循環(huán)的 break 的方式是發(fā)現(xiàn)無(wú)銷量時(shí)給 i 值賦零,這樣 while 循環(huán)也會(huì)同時(shí)結(jié)束。
- 地址的匹配使用 re.sub() 函數(shù)去除了 n 多復(fù)雜信息,這點(diǎn)后面解釋。
輸出本地文本
為了防止代碼運(yùn)行錯(cuò)誤,維護(hù)代碼運(yùn)行的和平,將輸出的信息列表存入到 excel 文件中了,方便日后查閱,很簡(jiǎn)單的代碼,需要了解 pandas 的用法。
- def listToExcel(list,name):
- df = pd.DataFrame(list,columns=['景點(diǎn)名稱','級(jí)別','所在區(qū)域','起步價(jià)','銷售量','熱度','地址','標(biāo)語(yǔ)','詳情網(wǎng)址'])
- df.to_excel(name + '景點(diǎn)信息.xlsx')
百度經(jīng)緯度 API
非常悲傷的,(ಥ﹏ಥ)我沒(méi)找到去哪兒景點(diǎn)的經(jīng)緯度,以為這次學(xué)(zhuang)習(xí)(bi)計(jì)劃要就此流產(chǎn)了。(如果有人知道景點(diǎn)經(jīng)緯度在哪里請(qǐng)告訴我)
但是,enhahhahahaha,我怎么會(huì)放棄呢,我又找到了百度經(jīng)緯度 API。
網(wǎng)址:http://api.map.baidu.com/geocoder/v2/?address=地址&output=json&ak=百度密鑰,修改網(wǎng)址里的“地址”和“百度密鑰”,在瀏覽器打開(kāi),就可以看到經(jīng)緯度的 json 信息。
- #上海市東方明珠的經(jīng)緯度信息
- {"status":0,"result":{"location":{"lng":121.5064701060957,"lat":31.245341811634675},"precise":1,"confidence":70,"level":"UNKNOWN"}}
百度密鑰申請(qǐng)方法:http://jingyan.baidu.com/article/363872eccda8286e4aa16f4e.html
這樣我就可以根據(jù)爬到的景點(diǎn)地址,查到對(duì)應(yīng)的經(jīng)緯度辣!Python 獲取經(jīng)緯度 json 數(shù)據(jù)的代碼如下:
- def getBaiduGeo(sightlist,name):
- ak = '密鑰'
- headers = {
- 'User-Agent' :'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'
- }
- address = 地址
- url = 'http://api.map.baidu.com/geocoder/v2/?address=' + address + '&output=json&ak=' + ak
- json_data = requests.get(url = url).json()
- json_geo = json_data['result']['location']
觀察獲取的 json 文件,location 中的數(shù)據(jù)和百度 API 所需要的 json 格式基本是一樣,還需要將景點(diǎn)銷量加入到 json 文件中,這里可以了解一下 json 的淺拷貝和深拷貝知識(shí),最后將整理好的 json 文件輸出到本地文件中。
- def getBaiduGeo(sightlist,name):
- ak = '密鑰'
- headers = {
- 'User-Agent' :'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'
- }
- list = sightlist
- bjsonlist = []
- ejsonlist1 = []
- ejsonlist2 = []
- num = 1
- for l in list:
- try:
- try:
- try:
- address = l[6]
- url = 'http://api.map.baidu.com/geocoder/v2/?address=' + address + '&output=json&ak=' + ak
- json_data = requests.get(url = url).json()
- json_geo = json_data['result']['location']
- except KeyError,e:
- address = l[0]
- url = 'http://api.map.baidu.com/geocoder/v2/?address=' + address + '&output=json&ak=' + ak
- json_data = requests.get(url = url).json()
- json_geo = json_data['result']['location']
- except KeyError,e:
- address = l[2]
- url = 'http://api.map.baidu.com/geocoder/v2/?address=' + address + '&output=json&ak=' + ak
- json_data = requests.get(url = url).json()
- json_geo = json_data['result']['location']
- except KeyError,e:
- continue
- json_geo['count'] = l[4]/100
- bjsonlist.append(json_geo)
- ejson1 = {l[0] : [json_geo['lng'],json_geo['lat']]}
- ejsonlist1 = dict(ejsonlist1,**ejson1)
- ejson2 = {'name' : l[0],'value' : l[4]/100}
- ejsonlist2.append(ejson2)
- print '正在生成第' + str(num) + '個(gè)景點(diǎn)的經(jīng)緯度'
- num +=1
- bjsonlist =json.dumps(bjsonlist)
- ejsonlist1 = json.dumps(ejsonlist1,ensure_ascii=False)
- ejsonlist2 = json.dumps(ejsonlist2,ensure_ascii=False)
- with open('./points.json',"w") as f:
- f.write(bjsonlist)
- with open('./geoCoordMap.json',"w") as f:
- f.write(ejsonlist1)
- with open('./data.json',"w") as f:
- f.write(ejsonlist2)
在設(shè)置獲取經(jīng)緯度的地址時(shí),為了匹配到更準(zhǔn)確的經(jīng)緯度,我選擇了匹配景點(diǎn)地址,然而,景點(diǎn)地址里有各種神奇的地址,帶括號(hào)解釋在 XX 對(duì)面的,說(shuō)一堆你應(yīng)該左拐右拐各種拐就能到的,還有英文的……
于是就有了第三章中復(fù)雜的去除信息(我終于圓回來(lái)了!)。
然而,就算去掉了復(fù)雜信息,還有一些匹配不到的景點(diǎn)地址,于是我使用了嵌套 try,如果景點(diǎn)地址匹配不到;就匹配景點(diǎn)名稱,如果景點(diǎn)名稱匹配不到;就匹配景點(diǎn)所在區(qū)域,如果依然匹配不到,那我……那我就……那我就跳過(guò)ㄒ_ㄒ……
身為一個(gè)景點(diǎn),你怎么能,這么難找呢!不要你了!
這里生成的三個(gè) json 文件,一個(gè)是給百度地圖 API 引入用的,另兩個(gè)是給 echarts 引入用的。
網(wǎng)頁(yè)讀取 json 文件
將第二章中所述的百度地圖 API 示例中的源代碼復(fù)制到解釋器中,添加密鑰,保存為 html 文件,打開(kāi)就可以看到和官網(wǎng)上一樣的顯示效果。
echarts 需要在實(shí)例頁(yè)面,點(diǎn)擊頁(yè)面右上角的 EN 切換到英文版,然后點(diǎn)擊 download demo 下載完整源代碼。
根據(jù) html 導(dǎo)入 json 文件修改網(wǎng)頁(yè)源碼,導(dǎo)入 json 文件。
- #百度地圖api示例代碼中各位置修改部分
- <head>
- <script src="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
- </head>
- <script type="text/javascript">
- $.getJSON("points.json", function(data){
- var points = data;
- script中原有函數(shù);
- });
- </script>
這里使用了 jQuery 之后,即使網(wǎng)頁(yè)調(diào)試成功了,在本地打開(kāi)也無(wú)法顯示網(wǎng)頁(yè)了,在 chrome 中右鍵檢查,發(fā)現(xiàn)報(bào)錯(cuò)提示是需要在服務(wù)器上顯示,可是,服務(wù)器是什么呢?
百度了一下,可以在本地創(chuàng)建一個(gè)服務(wù)器,在終端進(jìn)入到 html 文件所在文件夾,輸入 python -m SimpleHTTPServer,再在瀏覽器中打開(kāi) http://127.0.0.1:8000/,記得要將 html 文件名設(shè)置成 index.html 哦!
后記
因?yàn)樽?cè)但沒(méi)有認(rèn)證開(kāi)發(fā)者賬號(hào),所以每天只能獲取 6K 個(gè)經(jīng)緯度 API(這是一個(gè)很好的偷懶理由),所以我選擇了熱門(mén)景點(diǎn)中前 400 頁(yè)(每頁(yè) 15 個(gè))的景點(diǎn)。
結(jié)果可想而知,(ಥ﹏ಥ)為了調(diào)試因?yàn)閿?shù)據(jù)增多出現(xiàn)的額外 Bug,最終的獲取的景點(diǎn)數(shù)據(jù)大概在 4500 條左右(爬取時(shí)間為 2017 年 9 月 10 日,爬取關(guān)鍵詞:熱門(mén)景點(diǎn),僅代表當(dāng)時(shí)銷量)。
熱門(mén)景點(diǎn)熱力圖
熱門(mén)景點(diǎn)示意圖
這些地圖上很火爆的區(qū)域,我想在國(guó)慶大概是這樣的
這樣的
還有這樣的
將地圖上熱門(mén)景點(diǎn)的銷量 Top20 提取出來(lái),大多數(shù)都是耳熟能詳?shù)牡攸c(diǎn),帝都的故宮排在了第一位,而大四川則占據(jù)了 Top5 中的三位,排在 Top20 中四川省景點(diǎn)就占了 6 位。
如果不是因?yàn)榈卣?,我想還會(huì)有更多的火爆的景點(diǎn)進(jìn)入排行榜的~這樣看來(lái)如果你這次國(guó)慶打算去四川的話,可以腦補(bǔ)到的場(chǎng)景就是:人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人人……
熱門(mén)景點(diǎn)銷量Top20
于是我又做了一個(gè)各城市包含熱門(mén)景點(diǎn)數(shù)目的排行,沒(méi)想到在 4 千多個(gè)熱門(mén)景點(diǎn)中,數(shù)目最多的竟是我大浙江,是第二個(gè)城市的 1.5 倍,而北京作為首都也……可以說(shuō)是景點(diǎn)數(shù)/總面積的第一位了。
主要城市熱門(mén)景點(diǎn)數(shù)
這些城市有辣么多熱門(mén)景點(diǎn),都是些什么級(jí)別的景點(diǎn)呢?由下圖看來(lái),各城市的各級(jí)別景點(diǎn)基本與城市總熱門(mén)景點(diǎn)呈正相關(guān),而且主要由 4A 景區(qū)貢獻(xiàn)而來(lái)。
主要城市熱門(mén)景點(diǎn)級(jí)別
既然去哪些地方人多,去哪里景多都已經(jīng)知道了,那再看看去哪些地方燒得錢最多吧。
下圖是由各城市景點(diǎn)銷售起步價(jià)的最大值-最小值扇形組成的圓,其中湖北以單景點(diǎn)銷售起步價(jià) 600 占據(jù)首位。
但也可以看到,湖北的景點(diǎn)銷售均價(jià)并不高(在紅色扇形中的藏藍(lán)色線條)。而如果國(guó)慶去香港玩,請(qǐng)做好錢包減肥的心理和生理準(zhǔn)備(•̀ω•́)✧。
各省旅游景點(diǎn)銷售起步價(jià)
好啦分析完啦,ヾ(*ΦωΦ)ツ大家可要好好玩呀。
PS:寫(xiě)了個(gè)網(wǎng)頁(yè),展示百度地圖的熱力圖效果和 echarts 的景點(diǎn)排行榜,方便大家查看。
熱力度效果:http://easyinfo.online
gayhub源碼:https://github.com/otakurice/notravellist/tree/master
寫(xiě)完這篇文的時(shí)候發(fā)現(xiàn) echarts 有針對(duì) Python 的模塊可以引入,所以打算去學(xué)一下 Django、Flask 之類的 Web 框架,最近會(huì)更一些純理論的意識(shí)流文,大家一起進(jìn)步吧~
參考資料:
1.地圖API:http://developer.baidu.com/map/reference/index.php
2.echarts:http://echarts.baidu.com/
3.API使用示例:http://developer.baidu.com/map/jsdemo.htm#c1_15
4.json:http://www.runoob.com/json/json-tutorial.html
5.xpath:http://www.runoob.com/xpath/xpath-tutorial.html
6.pandas:http://python.jobbole.com/84416/
7.百度經(jīng)緯度api:http://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding
8.淺拷貝和深拷貝:http://python.jobbole.com/82294/
9.html導(dǎo)入json文件:http://www.jb51.net/article/36678.htm
最后,懷著敬畏又惋惜的心情紀(jì)念一下 WePhone 創(chuàng)始人蘇享茂,在發(fā)生自殺事件之前我不認(rèn)識(shí)他,我也不希望以這種方式認(rèn)識(shí)他,希望程序員的世界永遠(yuǎn)單純、沒(méi)有欺詐。