python爬取大众点评酒店数据

hresh 999 0

python爬取大众点评酒店数据

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酒店为例

    酒店评论

    在该页面主要爬取用户名称,酒店详细地址,评论内容,图片。

    其中难点在于酒店详细地址和评论内容的提取。地址 和评论内容 内的 class 标签文字的替换就是这次破解反爬的关键

    酒店信息

    点击 标签,点进去会发现源码的右边会出现关于这一元素的基本格式:位置、大小、字体大小等,里面还会有个url。

    python爬取大众点评酒店数据插图5

    根据 标签属性中的 url 可以打开 svg 文件,显示如下:

    python爬取大众点评酒店数据插图6

    这里应该就是隐藏的字符集,但是我们需要找到文字的替换规则,打开这个网页的源码:

    python爬取大众点评酒店数据插图7

    这里要有两个点需要我们注意:一个是文字的大小 font-size,另一个就是每一行都会有一个 y 标签会对应一个数值;这里我看了网上的一些教程,了解到与这两个点对应的就是图二中下标签内元素对应的 background 对应的两个位置元素(区别是位置元素里面的数值是负值);定位规则就是第二个元素对应的是 y 值决定该 class 标签替换的元素在哪一行,而第一个元素数值与文字大小之比对应的是这一行的第几个即为偏移量,从而形成映射关系;

    下面举例说明,我们以 ,从图中可以看出它对应的应该是“房”字,它的 css 属性为 {background:-280.0px -2369.0px}。

    python爬取大众点评酒店数据插图8

    根据 svg 文件源码分析:

    python爬取大众点评酒店数据
    python爬取大众点评酒店数据

    接下来分析文字对应关系,根据 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.虽然 标签和 标签对应不同的 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)
    
                    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

  • 发表评论 取消回复
    表情 图片 链接 代码

    分享