Python爬虫内容都是于2019上半年写的,关于某些网站的爬取技巧可能已经过时了,仅供参考。
前言
平时旅游出行关于住宿的安排都是由我来做,包括地址的选择以及酒店房间的舒适性,女朋友每次对我选择的酒店都很满意。一开始我是在手机上对比搜索来选择合适的酒店, 虽然做的不错,就是比较耗时。自从学了爬虫之后,我就想着用爬虫来解决,把所有酒店信息爬取下来,从中选择满意的方案。
废话少说,我们直接进入正题,进行代码开发。
环境: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酒店为例
在该页面主要爬取用户名称,酒店详细地址,评论内容,图片。
其中难点在于酒店详细地址和评论内容的提取。地址
点击 标签,点进去会发现源码的右边会出现关于这一元素的基本格式:位置、大小、字体大小等,里面还会有个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(self, html, headers):
'''
解析评论页面内容,从中提取css样式文件url,返回css文件源码
:param html: 评论页面内容
:param headers: 请求头
:return: css文件源码
'''
# svg_text_css = re.search(r'href="(.*?svgtextcss.*?css)">', html,re.S)
svg_text_css = re.findall(r'href="(.*?svgtextcss.*?css)">', html, re.M)
if not svg_text_css:
return False
css_url = 'http:' + svg_text_css[0]
print(css_url)
content = self.parse_url(css_url, headers)
return content
# 获取定义偏移量的css文件后将结果以字典形式存储
def get_css_offset(self, css_content):
"""
通过传入页面中任意css获取其对应的偏移量
:return: {'xxx': ['192', '1550']}
"""
offset_item = re.findall(r'\.([a-zA-Z0-9]{5,6}).*?background:-(.*?).0px -(.*?).0px', css_content)
if not offset_item:
return False
result = {}
for item in offset_item:
css_class = item[0]
x_offset = int(item[1])
y_offset = int(item[2])
result[css_class] = [x_offset, y_offset]
return result
构建隐藏文字与位置元素数值对应的映射关系:
def get_font_dict(self, css_class_dirt, svg_url_dict):
"""
构建class标签元素与位置元素数值对应的映射关系
获取css样式对应文字的字典
"""
#根据svg文件不同,匹配不同的svg文件地址,
#'ukj': [14, 'https://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/04cc0d28efdb7945a60af7aba24ed77b.svg']
#比如说某个隐藏字段的class为ukj开头,则它的明文对应应该在ukj后的svg文件中匹配
svg_kind_list = []
for css_class_name in css_class_dirt.keys():
svg = svg_url_dict.get(css_class_name[:3], None)
if not svg:
continue
svg_kind_list.append(css_class_name[:3])
svg_kind_list = list(set(svg_kind_list))
font_dict = {}
for svg_kind in svg_kind_list:
print(svg_kind)
# 根据偏移量来找到对应的数字
size = svg_url_dict[svg_kind][0]
svg_url = svg_url_dict[svg_kind][1] # svg_url地址
font_dict_by_offset, y_list = self.get_font_dict_by_offset(svg_url)
for css_class_name in css_class_dirt.keys():
# 根据css名称获取偏移量
x_offset, y_offset = css_class_dirt[css_class_name][0], css_class_dirt[css_class_name][1]
x = int(x_offset)
y = int(y_offset)
x_position = x // size
y_position = bisect.bisect(y_list, y)#利用bisect查找位置
try:
font_dict[css_class_name] = font_dict_by_offset[y_position]['text'][x_position]
except:
break
print(font_dict)
return font_dict
def get_font_dict_by_offset(self, url):
"""
获取坐标偏移的文字字典
"""
res = requests.get(url, headers=self.css_headers)
html = res.text
font_list = re.findall(r'<text.*?y="(.*?)">(.*?)<', html, re.S)
svg_list = [] # 存放svg页面源码中的内容,按照y值进行分行
y_list = [] # 存放svg页面源码中每行数据的y值
for item in font_list:
y_list.append(int(item[0]))
svg = {'y_key': int(item[0]), 'text': item[1]}
svg_list.append(svg)
return svg_list, y_list
进行 class 属性元素与隐藏文字的替换:
def get_conment_page(self, html, font_dict):
"""
请求评论页,并将<span></span>样式和<bb></bb>样式替换成文字
进行class属性元素与隐藏文字的替换
"""
class_set = set()
for span in re.findall(r'<svgmtsi class="([a-zA-Z0-9]{5,6})"></svgmtsi>', html):
class_set.add(span)
for class_name in class_set:
try:
html = re.sub('<svgmtsi class="%s"></svgmtsi>' % class_name, font_dict[class_name], html)
except:
html = re.sub('<svgmtsi class="%s"></svgmtsi>' % class_name, '', html)
return html
注意事项
1.大众点评评论需要登陆才能够爬取,这里解决的方法比较简单就是先登录,获取 cookies,添加到 requests 里面再进行爬取,但是一般爬取 200 页的时候会现滑块验证;所以可以准备一个相关 cookies 池让里面的 cookie 轮流访问;
2.虽然
评论区内容的爬取代码:
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)
content = self.parse_url(url, self.headers)
content_css = self.get_css(content, self.css_headers)
css_class_dirt = self.get_css_offset(content_css) # 偏移量字典存储
svg_url_dict = self.get_svg_url_dict(content_css) # svg的url dict储存
print(svg_url_dict)
font_dict = self.get_font_dict(svg_url_dict=svg_url_dict,css_class_dirt=css_class_dirt)
html = self.get_conment_page(content, font_dict)
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
本文作者为hresh,转载请注明。