python爬虫(上)--请求——关于旅游网站的酒店评论爬取(传参方法)
前言
最近考试一直都没有时间写这篇总结,现在考试暂告一段落,现在抽空出来写一篇总结,总结一下python爬虫的学习进度。
承接上一篇基于scrapy框架爬虫学习小结,上一篇主要是第二次作业后,“老师说会给我们时间继续完善这个作业,直到可以真的爬到微信朋友圈内容….”,其实之后前面半句是有,但是后面半句真的爬到朋友圈却没有了,老师改变了需求,我们变成了去爬一些旅游网站了。
我们被分派到的任务是:研究分析携程,艺龙,去哪儿,途牛和驴友的爬去规则分析以及爬取他们的酒店评论,我分配到的是携程和艺龙的酒店评论的爬取。
刚开始是我想用之前用过的scrapy来做这个爬取,但是不知道是不是我用的不太熟练,了解不深入,导致一些问题我自己没有办法处理,而且上网搜索的一些答案我都看不懂,之后我才直到,scrapy如果自己没有一定实力,排错很困难。
所以还是回到底层python爬虫实现,凡是要一步步来,因为如果我发现网上许多python爬虫都是用urllib,urllib2,bs4,Request等库来实现的,所以先抛开scrapy框架来,回归一些更加基础的爬虫相关库的学习。 (在进行下面之前请执行pip list查看自己是否已经安装了beautiful soup,lxml,Request和scrapy等第三方库,urllib,urllib2,re和json都是内置库不用考虑,如果没有上面四个第三方库,请用pip install之)
分析要爬取的网站
①携程
http://hotels.ctrip.com/hotel/
上面这个是携程酒店页,广度优先的话就是这一层先完成爬去每个酒店评论页的URL,携程酒店页长这样:
然后再在每个url里面找到评论数据来源再抓下来,这里我们选择上图第一个酒店桔子酒店(北京天宁寺店)为例子,点进去长这样:
看到图中酒店评论(1423)没有,点开看看:
貌似我们的要的评论就近在咫尺,但是当我们打开网页源码我们发现它的评论只有第一页的5条,后面的没有了,那么剩下的评论(第2,第3页…)又是怎么获得的呢?
AJAX:这里就得说说AJAX了,本身什么意思我就不详细说了,我们只要知道AJAX技术是一种异步加载数据的方式,在原来静态网页的天下,每次点击都要重新从服务器再次请求整个网页下来,而其实中间我们需要更新的数据只有一小部分(如果原来请求整个网页是100%的话,那么这一小部分只有5%),这样就造成了大量的带宽浪费,AJAX就是当需要更新部分数据时候,只从服务器加载这一小部分数据,而不用把整个页面又重新下载一遍,AJAX属于前后端数据交互技术,那么我们又要怎么观察AJAX的交互呢?又得如何获得这些动态加载的酒店评论数据呢?我自己用的是chrome所以推荐chrome浏览器自带的开发者工具【Ctrl】+【Shift】+【I】,就可以调出来了,长这样:
图中我们可以看到现在是评论的第二页,右边啥都没有,先点到Network,再选择筛选器,原来是【All】,现在换成【XHR】(大部分重新加载的AJAX在浏览器中都属于XHR类,当然有部分还可能在JS 类中),这样更加方便找出通过AJAX加载的更新数据,接下就刷新一下页面,让它重新加载第二页评论,结果如图:
P.s.我这里试了一下,直接刷新会回到最开始的这个页面。最好的办法就是,先点进酒店评论(1420),此时为第一页,打开chrome的开发者工具,选择XHR筛选器,然后点击下一页,就会得到一模一样的上图
我们可以看到,在右侧的这个URL的内容里面有很多有价值的信息,有Request URL,往下翻,可以看到Reques Headers(Cookie,Referer,User-Agent等伪装头部必备元素~)内容,这里上张图更清晰:
一般来说,通过Request URL,我们就可以抓到更新加载的评论数据了,但是这个URL打开是这样的,一片空白,不对啊,以往经验来说应该是有显示出json格式的数据的!!后面我以为是请求头request headers或者是referer(有些网站会检查这个请求来源URL,这个referer记录的就是请求来源URL)的伪装问题,但是写上去了问题依旧,但是通过Preview倒是能够看到加载的内容:
我这里纠结了好几天,也尝试很多办法,可能携程有特殊的反爬虫手段?现在目测只能后面再试试通过Selenium+PhantomJS(无头浏览器)模仿用户正常请求来试试了,但是Selenium也要做功课的呀,外加这篇讲传参的就不涉及了。考虑到最近要准备期末考试了,所以暂时把携程酒店评论抓取的工作挂起,以后有空再折腾- -b
P.s.今天在开发者头条看到携程酒店研发部研发经理崔广宇的一个分享,感觉之后可以参考再分析
②艺龙
艺龙总的来说给我的感觉没有携程那么复杂,可能携程太出名了吧,又广告,各种耳闻。
http://hotel.elong.com
上面这个是艺龙酒店页面,我们以广州的酒店为例子,谁叫我们是广州仔呢,广州的话就是这个:
http://hotel.elong.com/guangzhou/
和携程一样又是一列的酒店,长这样:
我们也可以广度优先先爬去每个酒店的URL,不过这个先放着,我们先看看点进去每个酒店的评论页面,先分析一番,这里我们以上图中的广州珠江新城希尔顿欢朋酒店为例,下面就是点进去,点击点评,呈现出来长这样:
惯例对比网页源码和网页内容(这里指评论),我们发现和携程一样,艺龙酒店评论页面的源代码中只有5条评论,其他估计也是通过AJAX异步加载的,通过chrome的开发者工具分析交互过程,果然如此:
上图中是我通过分析评论加载过程得出的,中间省略了一些步骤。
P.s.强烈建议你花点时间琢磨一下如何利用开发者工具分析交互过程,这有助于你快速找出AJAX加载地址URL
详细的信息:
把General下的Request URL复制粘贴到浏览器地址栏,打开,会有这样的返回:
和携程不一样,我们直接打开AJAX请求的URL能够如期地得到我们想要的json格式数据,接下来我们请求这个URL就可以了,不用像静态网页那样通过re(正则表达式)或者xpath定位到某个标签了。
做到这里,貌似我们已经找到我们想要的数据了,别着急,我们再来分析一下这个AJAX请求的URL的规律:
http://hotel.elong.com/ajax/detail/gethotelreviews/?hotelId=91245064&recommendedType=0&pageIndex=1&mainTagId=0&subTagId=0&=1467394392854
1.我注意到上面的URL中有hotelId字段,这里是91245064,和原来这个【http://hotel.elong.com/guangzhou/91245064/】中的数字一样,我就想是不是每个酒店都有属于自己的一个编号,那么为了验证我的想法,我就打开了【http://hotel.elong.com/guangzhou/32001199/】——广州东山宾馆的页面,同样出现了32001199这一串数字:
我们可以看到,这次AJAX请求的URL是:
http://hotel.elong.com/ajax/detail/gethotelreviews?hotelId=32001199&recommendedType=0&pageIndex=1&mainTagId=0&subTagId=0&=1467395437371
hotelId是32001199和原本【http://hotel.elong.com/guangzhou/32001199/】 中的一样,所以我基本确定,这串数字就是每个酒店都有的编号。
知道这个又有什么意义?首先知道了酒店有自己的酒店编号,然后又知道了原来通过AJAX请求的URL中的hotelId是等于这个这个酒店编号的,那么我们可不可以只是通过修改这个hotelId来获取不同酒店的酒店评论呢?答案是肯定的,我就不贴试验图了,你们可以自己做试验。
2.再回来看1里面的hotelId=91245064的那个AJAX地址:
http://hotel.elong.com/ajax/detail/gethotelreviews?hotelId=91245064&recommendedType=0&pageIndex=1&mainTagId=0&subTagId=0&=1467396059996
我们发现pageIndex=1,学过英文的我们能猜到pageIndex应该就是指页码,对比一下这个URL打开的json中的评论和原页面中第一页的评论内容,发现一致,修改pageIndex=2,回车,发现重新加载,内容改变,再对比重新加载的json数据内容和原页面中第二页的评论内容,发现也是一致的,基本可以肯定pageIndex的值就是确定请求第几页的值的字段
3.通过上面两点对AJAX请求的URL地址的分析,我们知道了我们只要改变hotelId和pageIndex的值就可以请求完艺龙内所有酒店的评论数据,不过如果要用程序实现,我们还需要直到所有的hotelId和每一个酒店内评论最大页面数量
程序实现
1.第一步要做的是获取全部酒店hotelId (凡事都得小范围试验先,所以还是先拿广州开刀,试着抓第一页全部的酒店编号)
#! usr/bin/python
#coding:utf-8
import urllib
import urllib2
import json
from bs4 import BeautifulSoup as bs
import re
'''
crawl page from hotel.elong.com
'''
#this part can be replaced by requests
place="guangzhou/"
url="http://hotel.elong.com/"+place
#headers camouflage
useragent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36'
headers={
'User-Agent':useragent
}
req=urllib2.Request(url,"",headers)
res=urllib2.urlopen(req)
data=res.read()
res.close()
'''
parse and select what i need
'''
#BeautifulSoup(data) will transform [data] in unicode automatically
soup=bs(data)
hotellinks=[]#define a list for hotellink
hotelnumbers=[]#define a list for hotelbumber
for tagdiv in soup.find_all('div',class_='h_info_base'):
for tagp in tagdiv.find_all('p',class_='h_info_b1'):
for taga in tagp.find_all('a'):
hotellink=taga.get('href')
#throw every hotellink to hotellinks list
hotellinks.append(hotellink)
for hotellink_to_hotelnumber in hotellinks:
for hotelnumber in re.findall(r'/\d*/',hotellink_to_hotelnumber):
for hotelnumber_again in re.findall(r'\d*',hotelnumber):
hotelnumbers.append(hotelnumber_again)
print hotellinks,"\n"
print hotelnumbers,"\n"
以下为输出结果为第一页的全部酒店编号,以及URL路径:
P.s.酒店列表翻页也用了AJAX技术实现动态加载,但是和上面谈论的不一样的是,之前谈论的都是GET方法,但是这里POST方法,有点不一样,还没有深入研究,不过我知道的一点是,如果你直接找到它AJAX请求URL,然后直接打开是获取不了数据的,原因现在推测是请求头的“Content-Type:application/x-www-form-urlencoded;”这里出现的,更加准确的原因得继续研究实验POST方法才能得出。 【也是尚未完成,没有完成后面全部页面酒店编号(hotelnumber_list)和路径的抓取,待续】
2.第二步获取每个酒店的评论页面最大值(Pgnum_max) 【未完成,待续】
3.最后获取酒店评论信息
由于前面两部已经获取了酒店编号列表和每个酒店评论页最大值,那么这一步就是基于hotelnumber_list和Pgnum_max做的一个函数(粗略示范一下):
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
import time
import os
from os.path import exists
import json
import codecs
import gzip
import StringIO
import urllib
import urllib2
import re
def getComment(hotelnumber,Pgnum_max):
for page in xrange(Pgnum_max):
Comments=[]
page=str(page)
print "第"+page+"页"
url="http://hotel.elong.com/ajax/detail/gethotelreviews/?hotelId="+hotelnumber+"&recommendedType=0&pageIndex="+page+"&mainTagId=0&subTagId=0&_=1468730838292"
headers={
'User-Agent':'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.94 Safari/537.36',
'Accept':'application/json, text/javascript, */*; q=0.01',
'Accept-Language':'zh-CN,zh;q=0.8',
'Accept-Encoding':'gzip, deflate, sdch',
'Connection':'keep-alive',
'X-Requested-With':'XMLHttpRequest',
#'Referer':'http://hotel.elong.com/guangzhou/32001005/',
'Host':'hotel.elong.com'
}
#请求AJAX
req=urllib2.Request(url,None,headers)
res=urllib2.urlopen(req)
data=res.read()
res.close()
#因为data有压缩所以要从内存中读出来解压
data=StringIO.StringIO(data)
gz=gzip.GzipFile(fileobj=data)
ungz=gz.read()
#正则提取
pattern_Comment=re.compile(r'"content":"\W*"')
#仍不能完全匹配到所有用户,这里只是匹配到中文名的用户,可能要用到非贪婪模式!
pattern_username=re.compile(r'"nickName":"\W*"')
pattern_CommentTime=re.compile(r'"createTimeString":"....-..-..\s..:..:.."')
for username in re.findall(pattern_username,ungz):
# print type(username)
# print username,'\n'
Comments.append(username)
for CommentTime in re.findall(pattern_CommentTime,ungz):
# print type(CommentTime)
# print CommentTime,'\n'
Comments.append(CommentTime)
for Comment in re.findall(pattern_Comment,ungz):
# print Comment,'\n'
# print type(Comment)
Comments.append(Comment)
#保存成HTML
comment=",".join(Comments)#list转str
cdir = u'./Comments/'
if not exists(cdir):
os.makedirs(cdir)
f=codecs.open(cdir+u'page'+page+u'.txt','wb+','utf-8')
f.write(comment)
'''
程序入口
'''
#前面也是没有做完,所以酒店编号列表先构造一个
hotelnumber_list=['32001005']
#前面没有抓到最大评论页面数,先假定最大有200页
Pgnum_max=2
for hotelnumber in hotelnumber_list:
getComment(hotelnumber,Pgnum_max)
下面是用存储的page0.txt(商品编号为32001005的第一页信息)作为示范,吐槽一下自己,因为偷懒所以直接在windows下开putty用vim打开的page0.txt截的图,现在贴上去后觉得丑死了….
免责声明
本篇文章只是为了分享技术和共同学习为目的,并不出于商业目的和用途,也不希望用于商业用途,特此声明。