当前位置:   article > 正文

Python网络爬虫实战:爬取携程网酒店评价信息_python爬取携程酒店评论

python爬取携程酒店评论

这个爬虫是在一个小老弟的委托之下写的。

他需要爬取携程网上的酒店的评价数据,来做一些分词和统计方面的分析,然后来找我帮忙。

爬这个网站的时候也遇到了一些有意思的小麻烦,正好整理一下拿出来跟大家分享一下。

这次爬取过程稍微曲折,各种碰壁,最终成功的过程,还是有点意思。

所以本文我会按照自己当时爬取的思路来讲述,希望能给大家一些思路上的启发。

分析部分略长,如果赶时间可以直接拉到最下面,自取代码。

如果是想学习爬虫的话,最好还是跟着文章的思路走一遍吧。

一、明确需求

这位小老弟给我的需求是:

要求很简单不是嘛,数据量也不是很大(我看了一下,也才 910 条评价,后来爬取完成之后发现其实只有 750 条左右),根本不够看的。于是,我自作主张,返了个场,在他需求的基础上添加了几条:

  • 酒店不只爬一家了,要爬就爬取【北京市】的所有【四星级以上】的酒店。
  • 评价数据也不止爬【家庭亲子】类型了,要爬就爬所有的评价数据。

二、分析目标网站

这里我发现新手在这里一般都有一个共有的误区,就是他们觉得爬虫都是 “通用” 的,一个网站的爬虫拿过来,网址改一下,再随便撺吧撺吧就可以爬另一个网站了。

实际上,每一个网站的爬取都是需要单独进行分析的,你需要找到目标数据是在网页上的什么位置,是通过静态还是动态的方式加载进去的,网站是否有难搞的反爬虫措施,等等,从而来制定自己爬虫的爬取策略。

一般情况下,除非两个网站是极其相似的,或者根本就是用同一个网页模板开发的,这样的话可以套用同一个爬虫来爬,否则,需要针对每个网站的特点去写对应的爬虫。

1. 酒店列表爬取

好了,话不多讲,我们先来分析一下目标网站。

首先打开携程网站,目的地选择【北京】,星级选择【四星级】和【五星级】,点击搜索。

此时网址是:北京酒店,北京酒店预订查询,北京宾馆住宿【携程酒店】

可以看到网址中只包含了 【北京】 这个信息,什么四星级五星级的筛选条件,并没有体现在 URL 中。但是从结果来看,它又是确实完成了筛选,所以筛选条件的这些参数肯定是包含在请求的某一个位置的。

继续向下看,翻到页尾,发现网站是用这种方式来实现【翻页】功能的。点击【下一页】,跳转到了第 2 页。

回头看一下 URL,居然没有一丝变化,还是:北京酒店,北京酒店预订查询,北京宾馆住宿【携程酒店】

到现在基本可以确定一件事儿了,网页中的酒店信息是通过【动态】方式加载进来的。

好,我们去抓包看一下,按 F12 召唤出【开发者工具】,切换到【Network】选项卡,然后刷新一下页面。

天哪,瞧我发现了什么!!!

在浏览器加载页面时,我抓到了一个叫【AjaxHotelList.aspx】的网络请求,而它的返回结果,恰恰就是我们页面中展示的酒店列表的信息。

果然,携程网的酒店数据,是通过 Ajax 请求动态地加载的。如果没猜错的话,刚才没找到的【四星级、五星级】筛选条件参数,以及页码的参数,应该就藏在这个 Ajax 请求的参数中吧。

如图,切换到 Headers 选项,拉到最底下(Form Data 里的参数有点多,代表了各种各样的筛选条件,不过我们不关心那些),看到了 star 和 page 两个参数。

果然如我所料!

不过不能高兴太早,为了防止网站有什么比较坑爹的反爬机制,最好先写段代码验证一下,看能否按照预期爬到数据。

这里我网络请求用的是 requests 库,数据解析用的是 json 库。

照着浏览器中开发者工具里的 Ajax 请求,把里面的 url,headers,以及 form data 搬过来填这里,发起请求,打印返回结果。

(无关的参数实在太多了,这里简化了一下,只保留了关键的三个参数,cityId,star,和 page)

  1. import requests
  2. import json
  3. def fetchHotel(city, star, page):
  4. url = "https://hotels.ctrip.com/Domestic/Tool/AjaxHotelList.aspx"
  5. headers = {
  6. 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  7. 'Origin': 'https://hotels.ctrip.com',
  8. 'Referer': 'https://hotels.ctrip.com/hotel/beijing1',
  9. 'accept': '*/*',
  10. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
  11. }
  12. formData = {
  13. 'cityId': city,
  14. 'star': star,
  15. 'page': page,
  16. }
  17. # 发起网络请求
  18. r = requests.post(url, data=formData,headers=headers)
  19. r.raise_for_status()
  20. r.encoding = r.apparent_encoding
  21. # 打印 r.text 来看看是否获取到了酒店数据
  22. print(r.text)
  23. fetchHotel('1','4,5',1)

 运行一下,确实出来结果了(虽然输出一堆 “乱七八糟” 的东西,但是从中文字里还是能够看出来,数据取到了)

 OK,这条路走通了,不过既然都写到这儿了,顺手把 json 给解析一下,把数据提取了吧。(免得有的小伙伴不相信)

这里我们提取 酒店名称,酒店ID 打印出来看看。

  1. import requests
  2. import json
  3. def fetchHotel(city, star, page):
  4. url = "https://hotels.ctrip.com/Domestic/Tool/AjaxHotelList.aspx"
  5. headers = {
  6. 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  7. 'Origin': 'https://hotels.ctrip.com',
  8. 'Referer': 'https://hotels.ctrip.com/hotel/beijing1',
  9. 'accept': '*/*',
  10. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
  11. }
  12. formData = {
  13. 'cityId': city,
  14. 'star': star,
  15. 'page': page,
  16. }
  17. # 发起网络请求
  18. r = requests.post(url, data=formData,headers=headers)
  19. r.raise_for_status()
  20. r.encoding = r.apparent_encoding
  21. # 打印 r.text 来看看是否获取到了酒店数据
  22. #print(r.text)
  23. # 解析 json 文件,提取酒店数据
  24. json_data = json.loads(r.text)['hotelPositionJSON']
  25. hotelList = []
  26. for item in json_data:
  27. hotelId = item['id']
  28. hotelList.append(hotelId)
  29. print(item['name'], hotelId)
  30. return hotelList
  31. fetchHotel('1','4,5',1)

运行代码,你看!没错吧,是我们要的酒店列表。

到这里,酒店列表爬取工作,基本就跑通了。


 2. 酒店评论爬取

接下来该研究研究酒店评论该怎么爬取吧。

随便打开一个酒店,进入详情页之后,找到了【酒店点评】部分。

在这里,我们可以找到需要的评论的数据,用户昵称,评分,出游类型,入住时间,评价时间,房型,评价内容等等。

继续往下翻,评论页数同样的方式翻页,而且翻页时候 URL 不变,不用说,又是 Ajax 动态加载咯。

直接 F12 召唤 开发者工具,流程很熟悉了,就讲快一点啦。于是我就抓到了评价数据的包了。

(其实很好找的啦,AjaxHotelCommentList,懂点英语的都能猜到是这个了)

不过它返回的内容格式不是 json 了,而是 html,而且没有排版,格式有点乱。

这个不要紧,去随便找一个在线代码格式化网站(在线代码格式化),排个版就好了。

解析 json 文件可以用 json 库,解析 HTML 文件用什么呢?我一般用 BeautifulSoup 库,贼拉好用。

这里先不急解析,先写代码验证一下,看看有没有什么坑爹的反爬机制。

 同样的方法,讲 Ajax 请求中的 Url,headers 还有 formdata 里的参数都扣过来,跑一下。

  1. import requests
  2. def fetchCmts(hotel, page):
  3. url = "https://hotels.ctrip.com/Domestic/tool/AjaxHotelCommentList.aspx?MasterHotelID=469055&hotel=469055&NewOpenCount=0&AutoExpiredCount=0&RecordCount=5420&OpenDate=2013-12-01&card=-1&property=-1&userType=-1&productcode=&keyword=&roomName=&orderBy=2&viewVersion=c&contyped=0&eleven=12488c2f039b057861112f7bc2f1322271c415a3618cba855bcc85b09795189e&callback=CASOmTvWnCuMJeETo&_=1572277191008"
  4. headers = {
  5. 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  6. 'referer':'https://hotels.ctrip.com/hotel/469055.html?isFull=F&masterhotelid=469055&hcityid=2',
  7. 'accept': '*/*',
  8. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
  9. }
  10. formData = {
  11. 'hotel': str(hotel),
  12. 'currentPage': str(page),
  13. 'eleven': '12488c2f039b057861112f7bc2f1322271c415a3618cba855bcc85b09795189e',
  14. 'callback': 'CASOmTvWnCuMJeETo',
  15. '_': '1572277191008',
  16. }
  17. r = requests.post(url, headers=headers)
  18. r.raise_for_status()
  19. r.encoding = "utf-8"
  20. print(r.text)
  21. fetchCmts('469055', 1)

运行代码,果然,可以获取到数据。

不过,这个可不能高兴的太早,为什么呢?

你看 Form Data 的参数中,有三个参数 eleven,callback 和 _ ,这三个的值有点奇怪,一长串看不懂的数字和字母,而且每次的值都不一样。

'_' 的值,1572277191008,这个有点熟悉,好像是时间戳,找在线工具解析一下,没错,果然是。

内心咯噔一下,坏了!

根据经验来讲,参数中带时间戳的,请求一般都是有时效性的。什么意思呢?就是这类请求的参数都是根据一定的规则动态生成的,而且一般几分钟之内就会失效。(再次运行上面的代码,果然啥也获取不到了,失效了)。

 也就是说,如果我想通过 Ajax 请求去获取数据的话,我必须搞清楚这三个参数的生成规则。

而这些参数又是经过 JS 加密的,搞这个又涉及到了 JS 逆向的东西。。。

我其实去网上查过携程网酒店爬虫,想看看别人是怎么绕过这个反爬机制的。

结果搜出来的好几个结果,都是用 Selenium webdriver 爬的。那个是什么原理呢。

就是我们正常的思路,是用爬虫直接去访问网站获取数据,爬虫伪装不好的话很容易被发现;

而它们这个,相当于是爬虫操作一个真的浏览器去访问网站,对方网站看到的是真正的浏览器在访问,它怎么也想不到操作浏览器的不是人,而是一只爬虫。所以这个方法几乎可以绕过所有的反爬机制。

不过!!!!

一个爬虫玩家的尊严,不允许我使用这种低效率又无脑的方式(误,手动狗头保命)。

于是我决定硬刚 Ajax 请求!!

 不过 JS 逆向哪有这么容易的,一时半会儿也搞不定(记得我第一次做 JS 逆向时,整整调试了一个礼拜的 JS 代码才搞出来),而那边小老弟要的又比较急......

正在我一筹莫展之际,看到了一张帖子,有个老哥的回答让我茅塞顿开。

对呀,网页端的不行,那就模拟手机端的来试试。 

网址由 https://hotels.ctrip.com/hotel 变成了 酒店预订,酒店价格查询,宾馆住宿预订,手机订酒店-【携程酒店手机版】 。

手机版的评论不是点页码翻页的,是划到页面底部时候自动加载下一页内容的,然后我们在 开发者工具中 成功抓到了评论数据的请求包。

再看一下它的参数列表,嗯,还好,没有奇奇怪怪的动态加密的参数了。

 这次怎么样呢?写段代码验证一下吧。

  1. import requests
  2. def fetchCmts(hotel, page):
  3. url = "https://m.ctrip.com/restapi/soa2/16765/gethotelcomment?&_fxpcqlniredt=09031074110034723384"
  4. headers = {
  5. 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  6. 'Origin': 'https://m.ctrip.com',
  7. 'accept': '*/*',
  8. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
  9. }
  10. formData = {
  11. 'groupTypeBitMap': '2',
  12. 'hotelId': str(hotel),
  13. 'pageIndex': str(page),
  14. 'pageSize': '10',
  15. 'travelType': '-1', # -1 表示全部,家庭亲子为 30
  16. }
  17. r = requests.post(url, data=formData, headers=headers) # formData,
  18. r.raise_for_status()
  19. r.encoding = "utf-8"
  20. return r.text
  21. fetchCmts('6410223', 1)

运行程序,可以获取到结果,修改酒店编号,修改页码,再运行都没问题。

返回的结果是 json格式的,回头用 json 库解析一下,把关键数据提取出来就可以了。 

事情进行到这儿,对目标网站的分析研究也就基本结束了。

酒店列表,评论数据的爬取,流程也基本跑通了,接下来只需要把代码整理一下,爬就完事儿了。

二、爬虫代码编写

前面将网站的爬取思路已经捋清楚了,而且做了些小测试也基本跑通了,接下来就是撸码环节了。

1. 获取酒店列表

我们其实也发现了,爬取评论数据时,只需要酒店ID和页码两个参数就够了,所以爬酒店列表时,我们只需要提取 酒店ID 即可。

  1. import requests
  2. import json
  3. def fetchHotel(city, star, page):
  4. url = "https://hotels.ctrip.com/Domestic/Tool/AjaxHotelList.aspx"
  5. headers = {
  6. 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  7. 'Origin': 'https://hotels.ctrip.com',
  8. 'Referer': 'https://hotels.ctrip.com/hotel/beijing1',
  9. 'accept': '*/*',
  10. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
  11. }
  12. formData = {
  13. 'cityId': city,
  14. 'star': star,
  15. 'page': page,
  16. }
  17. # 发起网络请求
  18. r = requests.post(url, data=formData,headers=headers)
  19. r.raise_for_status()
  20. r.encoding = r.apparent_encoding
  21. # 解析 json 文件,提取酒店数据
  22. json_data = json.loads(r.text)['hotelPositionJSON']
  23. hotelList = []
  24. for item in json_data:
  25. hotelId = item['id']
  26. hotelList.append(hotelId)
  27. return hotelList
'
运行

2. 爬取评论数据

  1. import requests
  2. import json
  3. def fetchCmts(hotel, page):
  4. url = "https://m.ctrip.com/restapi/soa2/16765/gethotelcomment?&_fxpcqlniredt=09031074110034723384"
  5. headers = {
  6. 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  7. 'Referer': 'https://m.ctrip.com/webapp/hotel/hoteldetail/dianping/'+ hotel + '.html?&fr=detail&atime=20191027&days=1',
  8. 'Origin': 'https://m.ctrip.com',
  9. 'accept': '*/*',
  10. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
  11. }
  12. formData = {
  13. 'groupTypeBitMap': '3',
  14. 'auth': "",
  15. 'cid': "09031074110034723384",
  16. 'ctok': "",
  17. 'cver': "1.0",
  18. 'extension': '[]',
  19. 'lang': "01",
  20. 'sid': "8888",
  21. 'syscode': "09",
  22. 'hotelId': str(hotel),
  23. 'needStatisticInfo': '0',
  24. 'order': '0',
  25. 'pageIndex': str(page),
  26. 'pageSize': '10',
  27. 'tagId': '0',
  28. 'travelType': '-1',
  29. }
  30. r = requests.post(url, data=formData, headers=headers) # formData,
  31. r.raise_for_status()
  32. r.encoding = r.apparent_encoding
  33. json_data = json.loads(r.text)
  34. cmtsList = []
  35. hotelName = json_data['hotelName']
  36. for item in json_data['othersCommentList']:
  37. cmt = []
  38. userName = item['userNickName']
  39. travelType = item['travelType']
  40. baseRoomName = item['baseRoomName']
  41. checkInDate = item['checkInDate']
  42. postDate = item['postDate']
  43. ratingPoint = item['ratingPoint']
  44. content = item['content']
  45. cmt.append(userName)
  46. cmt.append(hotelName)
  47. cmt.append(travelType)
  48. cmt.append(baseRoomName)
  49. cmt.append(checkInDate)
  50. cmt.append(postDate)
  51. cmt.append(ratingPoint)
  52. cmt.append(content)
  53. cmtsList.append(cmt)
  54. return cmtsList
'
运行

3. 数据保存函数

将数据保存到 csv 文件中。

  1. import pandas as pd
  2. import os
  3. def saveCmts(path, filename, data):
  4. # 如果路径不存在,就创建路径
  5. if not os.path.exists(path):
  6. os.makedirs(path)
  7. # 保存文件
  8. dataframe = pd.DataFrame(data)
  9. dataframe.to_csv(path + filename, encoding='utf_8_sig', mode='a', index=False, sep=',', header=False )
'
运行

4. 爬虫调度器

由于小老弟提的要求是: 上海静安香格里拉大酒店,家庭亲子类型的,评论数据。所以,

在 fetchCmts 中,将 travelType 的值设置为 30,

'travelType': '30',   # 30 表示 家庭亲子 类型

由于看到评论区内容只有九百多条, 每页显示 10 条,所以我们将页码范围设置为 1 - 100 。

  1. import time
  2. if __name__ == '__main__':
  3. hotel = '469055' # 上海静安香格里拉大酒店
  4. startPage = 1
  5. endPage = 100
  6. path = 'Data/'
  7. filename = 'cmtTest.csv'
  8. for p in range(startPage, endPage+1):
  9. cmts = fetchCmts(hotel, p)
  10. saveCmts(path, filename, cmts)
  11. time.sleep(0.5)

 为了保险期间,还加了一个 sleep 函数,每爬一次歇半秒,免得因为爬取太频繁被发现。

 几分钟之后,爬取完成,共爬取到 735 条数据。至此,小老弟的忙总算是帮完了。

不过,最开始也说了,我嫌爬的不过瘾,又给自己加了几条需求。

爬取北京市的,所有四星级以上酒店的,所有类型的评价数据。

  1. import time
  2. if __name__ == '__main__':
  3. city = '1'
  4. star = '4,5'
  5. startPage = 1
  6. hotelEndPage = 30
  7. cmtsEndPage = 100
  8. for page in range(startPage, hotelEndPage + 1):
  9. hotelList = fetchHotel(city, star, page)
  10. for hotel in hotelList:
  11. for p in range(startPage, cmtsEndPage + 1):
  12. cmts = fetchCmts(hotel, p)
  13. saveCmts("Data/", "cmtTest.csv", cmts)
  14. time.sleep(1)
  15. print("爬取完成")

 这里偷了个懒,具体有多少酒店,每个酒店有多少评论我们不去管它了,就爬 30 页的酒店,每个酒店爬 100 页的评论,

大概就是爬 300 个酒店,每个酒店 1000 条左右的评论,差不多可以了,如果想爬更多的话,可以自行去修改页码范围。


后记

这个爬虫给了我一个新的启示,就是,遇到问题,我们要有死磕的觉悟,但是也要有灵活变通的思维。

就像这个爬虫,爬取评论信息时,PC 版网页的请求加了密不好整,那就换个途径,从手机端来获取数据。

时间也省了,事儿也办了,岂不快哉。


2019年12月18日 更新

有读者反馈说,在抓取酒店列表信息的部分,使用文章中的代码无法正常获取数据。

运行代码的结果是这样的,也不报错,就是返回的搜索结果是 0 条。

经调试发现,可能是对方服务器做了调整,需要验证 cookies 信息,只需要在 headers 中添加 cookies 参数即可。

2020年6月20日 更新

距离这个爬虫写好已经比较久了,期间对方网站也做过一些反爬机制的调整导致爬虫失效。

很多读者反馈说,前面更新时说的向 headers 中添加 cookies 的方法也失效了。经过测试,确实是,现在网站需要验证 “登陆账号后” 的 cookie 了,注意是登陆账号后的,未登录的cookie爬出来还是0条。

后续网站是否会有调整我不知道,截至本次更新时,添加登陆账号后的 cookie 后,文中的爬虫仍是有效的。(可能有些读者刚刚接触爬虫,不知道 cookie 加在哪儿,下面贴一段测试代码,大家参考)

  1. import requests
  2. def fetchCmts(hotel, page):
  3. url = "https://m.ctrip.com/restapi/soa2/16765/gethotelcomment?&_fxpcqlniredt=09031074110034723384"
  4. headers = {
  5. 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
  6. 'Origin': 'https://m.ctrip.com',
  7. 'accept': '*/*',
  8. 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36',
  9. 'cookie':'这里放登陆账号后登陆账号后登陆账号后的cookie',
  10. }
  11. formData = {
  12. 'groupTypeBitMap': '2',
  13. 'hotelId': str(hotel),
  14. 'pageIndex': str(page),
  15. 'pageSize': '10',
  16. 'travelType': '-1', # -1 表示全部,家庭亲子为 30
  17. }
  18. r = requests.post(url, data=formData, headers=headers) # formData,
  19. r.raise_for_status()
  20. r.encoding = "utf-8"
  21. print(r.text)
  22. return r.text
  23. fetchCmts('10246623', 2)

对方网站反爬机制升级,也是侧面反映了网站收到各种爬虫爬取的困扰很大。

希望大家在爬取数据时,注意控制爬取节奏,时间允许的范围内,尽量放慢爬取速度。


 如果文章中有哪里没有讲明白,或者讲解有误的地方,欢迎在评论区批评指正,或者扫描下面的二维码,加我微信,大家一起学习交流,共同进步。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号