Scrapy爬取大众点评酒店数据
参考网址:https://blog.csdn.net/weixin_42512684/article/details/86775357
环境:win10,python3.7
一、爬取酒店主页信息
进入大众点评首页默认的地区是上海地区,所以干脆直接进入上海地区酒店首页从这里开始爬数据
如图所示,爬取酒店信息列表。主要包括酒店名称,酒店id,酒店简要位置,酒店评论数目。
每个酒店都是在
def parse(self, response):
sel = scrapy.Selector(response)
hotel = sel.xpath('//li[@class="hotel-block"]')
n = 0
for h in hotel:
item = DazongdianpingItem()
item['hotel_id'] = sel.xpath('//li[@class="hotel-block"]/@data-poi').extract()[n]
item['hotel_name'] = h.xpath('.//h2[@class="hotel-name"]/a[@class="hotel-name-link"]/text()').extract()[
0].replace('\t', '').replace(
'\n', '')
item['hotel_place'] = h.xpath('.//p[@class="place"]/a/text()').extract()[0] + \
h.xpath('.//p[@class="place"]/span/text()').extract()[0]
item['comment_num'] = h.xpath('.//a[@class="comments"]/text()').extract()[0].replace('(', '').replace(
')', '')
其中,翻页功能,分析url地址可得:
http://www.dianping.com/shanghai/hotel/p2
http://www.dianping.com/shanghai/hotel/p3
指定p后面的数字即可进行分页。
接下来,我们跳转到每个酒店的评论页面,url形如:
http://www.dianping.com/shop/76998662/review_all/p1
http://www.dianping.com/shop/76998662/review_all/p2
其中76998662是每个酒店id,我们在上述爬取过程中已经获取。
二、爬取酒店详细评论页面
我们以上海外滩W酒店为例
在该页面主要爬取用户名称,酒店详细地址,评论内容,图片。
其中难点在于酒店详细地址和评论内容的提取。地址和评论内容内的class标签文字的替换就是这次**反爬的关键
点击标签,点进去会发现源码的右边会出现关于这一元素的基本格式:位置、大小、字体大小等,里面还会有个url。
根据标签属性中的url可以打开svg文件,显示如下:
这里应该就是隐藏的字符集,但是我们需要找到文字的替换规则,打开这个网页的源码:
这里要有两个点需要我们注意:一个是文字的大小font-size,另一个就是每一行都会有一个y标签会对应一个数值;这里我看了网上的一些教程,了解到与这两个点对应的就是图二中下标签内元素对应的background对应的两个位置元素(区别是位置元素里面的数值是负值);定位规则就是第二个元素对应的是y值决定该class标签替换的元素在哪一行,而第一个元素数值与文字大小之比对应的是这一行的第几个即为偏移量,从而形成映射关系;
下面举例说明,我们以,从图中可以看出它对应的应该是“房”字,它的css属性{background:-280.0px -2369.0px}。
根据svg文件源码分析:
接下来分析文字对应关系,根据background属性值,我们取其数值,2369在[2349,2392]区间,所以该文字对应这一行;然后通过第一个数值计算其偏移量,280/14+1=21(因为文字大小为14px所以需要除以14),验证即为所得。
获取class标签对应的位置元素url(以.css结尾的):
def get_css_link(self, url):
"""
请求评论首页,获取css样式文件和html文件
"""
try:
res = requests.get(url, headers=self.headers)
html = res.text
css_link = re.search(r'<link re.*?css.*?href="(.*?svgtextcss.*?)">', html)
css_link = 'http:' + css_link[1]
assert css_link
return html, css_link
except:
None
根据css文件获得和的位置属性:
构建class标签元素与位置元素数值对应的映射关系:
def get_css_svg(self, url):
'''
评论区内容span块里的隐藏内容,地址bb块里的隐藏内容,对应着不同的svg文件
:param url:
:return:
'''
res = requests.get(url, headers=self.css_headers)
html = res.text
background_image_link = re.findall(r'background-image:.*?\((.*?svg)\)', html)
assert background_image_link
# <bb>样式表
background_image_link1 = 'http:' + background_image_link[1]
# <span>样式表
background_image_link2 = 'http:' + background_image_link[2]
return html, background_image_link1, background_image_link2
def get_font_dict(self, html, background_image_link):
"""
构建class标签元素与位置元素数值对应的映射关系
获取css样式对应文字的字典
"""
html = re.sub(r'span.*?\}', '', html)
group_offset_list = re.findall(r'\.([a-zA-Z0-9]{5,6}).*?round:(.*?)px (.*?)px;', html)
font_dict_by_offset = self.get_font_dict_by_offset(background_image_link)
font_dict = {}
for class_name, x_offset, y_offset in group_offset_list:
x_offset = x_offset.replace('.0', '')
y_offset = y_offset.replace('.0', '')
构建隐藏文字与位置元素数值对应的映射关系:
def get_font_dict_by_offset(self, url):
"""
获取坐标偏移的文字字典, 会有最少两种形式的svg文件(目前只遇到两种)
"""
res = requests.get(url, headers=self.css_headers)
html = res.text
font_dict = {}
y_list = re.findall(r'd="M0 (\d+?) ', html)
if y_list:
font_list = re.findall(r'<textPath .*?>(.*?)<', html)
for i, string in enumerate(font_list):
y_offset = self.start_y - int(y_list[i])
sub_font_dict = {}
for j, font in enumerate(string):
x_offset = -j * self.font_size
sub_font_dict[x_offset] = font
font_dict[y_offset] = sub_font_dict
else:
font_list = re.findall(r'<text.*?y="(.*?)">(.*?)<', html)
for y, string in font_list:
y_offset = self.start_y - int(y)
sub_font_dict = {}
for j, font in enumerate(string):
x_offset = -j * self.font_size
sub_font_dict[x_offset] = font
font_dict[y_offset] = sub_font_dict
return font_dict
进行class属性元素与隐藏文字的替换:
def get_conment_page(self, html, font_dict):
"""
请求评论页,并将<span></span>样式和<bb></bb>样式替换成文字
进行class属性元素与隐藏文字的替换
"""
class_set = set()
for span in re.findall(r'<span class="([a-zA-Z0-9]{5,6})"></span>', html):
class_set.add(span)
for class_name in class_set:
try:
html = re.sub('<span class="%s"></span>' % class_name, font_dict[class_name], html)
except:
html = re.sub('<span class="%s"></span>' % class_name, '', html)
class_set2 = set()
for span in re.findall(r'<bb class="([a-zA-Z0-9]{5,6})"></bb>', html):
class_set2.add(span)
for class_name in class_set2:
try:
html = re.sub('<bb class="%s"></bb>' % class_name, font_dict[class_name], html)
except:
html = re.sub('<span class="%s"></span>' % class_name, '', html)
return html
其中需要注意的问题包括两点:
1.大众点评评论需要登陆才能够爬取,这里解决的方法比较简单就是先登录,获取cookies,添加到requests里面再进行爬取,但是一般爬取200页的时候会现滑块验证;所以可以准备一个相关cookies池让里面的cookie轮流访问;
2.虽然标签和标签对应不同的svg文件,但是我在测试过程中,发现单单使用对应的svg中的字符集就可以匹配出所有的相关词汇,如果发现酒店详细地址处无法提取完整内容,可以在程序中单独处理。
评论区内容的爬取代码:
def parse(self, response):
sel = scrapy.Selector(response)
hotel = sel.xpath('//li[@class="hotel-block"]')
n = 0
for h in hotel:
item = DazongdianpingItem()
item['hotel_id'] = sel.xpath('//li[@class="hotel-block"]/@data-poi').extract()[n]
item['hotel_name'] = h.xpath('.//h2[@class="hotel-name"]/a[@class="hotel-name-link"]/text()').extract()[
0].replace('\t', '').replace(
'\n', '')
item['hotel_place'] = h.xpath('.//p[@class="place"]/a/text()').extract()[0] + \
h.xpath('.//p[@class="place"]/span/text()').extract()[0]
item['comment_num'] = h.xpath('.//a[@class="comments"]/text()').extract()[0].replace('(', '').replace(
')', '')
# 每个酒店抓取一定页数的评论
for i in range(1, 2):
url = self.base_url + item['hotel_id'] + '/review_all/p' + str(i)
html, css_link = self.get_css_link(url)
print(css_link)
css_content, link1, link2 = self.get_css_svg(css_link)
print(link1)
print(link2)
# 地址隐藏区内容
# font_dict1 = self.get_font_dict1(css_content,link1)
# 评论隐藏区内容
font_dict2 = self.get_font_dict(css_content, link2)
# 实际测试过程中,发现<bb>和<span>对应的隐藏表有相同之处,因此选择使用更大的映照表
time.sleep(2)
html = self.get_conment_page(html, font_dict2)
doc = pq(html)
item['hotel_address'] = doc('.address-info').text().replace('\xa0', '') # 存在隐藏字段,需要转换
assert item
results = doc('.reviews-items > ul > li ').items()
for com in results:
it = DazongdianpingItem()
it = item
it['comment_user'] = com.find('.dper-info > a').text()
it['comment_desc'] = com.find('.Hide').text().replace('\t', '').replace(
'\n', '').replace('\xa0', '')
desc_list = com.find('.Hide').text().replace('\t', '').replace(
'\n', '')
pic_list = []
for p in com.find('.review-pictures > ul > li > a').items():
pic_list.append('http://www.dianping.com' + p.attr('href'))
it['comment_pics'] = pic_list
# print(it)
yield it
其中,我遇到的一个问题就是,yield关键字,必须在含有response中的方法才有效,即必须成对使用。
最后,将爬取的内容保存到Mongodb数据库中,爬取结果如下:
详细代码:https://github.com/Acorn2/dazhongdianping