欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

大众点评爬虫

程序员文章站 2022-05-02 22:14:17
...

大众点评爬虫文档

一,开发环境

1, Scrapy-redis爬虫框架
2, pycharm开发工具

二,项目创建

1,创建项目:scrapy startproject +项目名称
2,创建爬虫:scrapy genspider +爬虫文件名 + 允许爬取的网站域名

三,修改配置文件

1,在配置文件settings.py文件中添加USER_AGENT参数,不添加UA参数无法获取到页面,先复制使用本机浏览器的UA,在会出现滑块验证码时只需在本地浏览器滑动解锁即可。
2,scrapy-redis的配置信息在完成整体爬虫流程之后配置修改。

四,爬取页面,获取页面数据

1,因为大众点评最重要的是对文字进行了加密,使用xpath无法获取到数据,所以先使用response.text将响应数据转换成unicode 型的文本数据
2,请求页面会出现验证中心的滑块验证界面,所以首先判断页面是否是验证中心,如果是验证界面则回调函数到本函数重新请求(后面会添加UA代理中间件与IP代理中间件),如果不是验证界面即可获取数据。
3,由于大众点评只能显示50页数据,所以我们需要将爬取范围缩小。因此先获取分类名称与分类地址(url)数据。遍历请求该分类地址,然后回调至下一个函数。
4,获取行政区的名称与地址(url)数据,遍历请求该行政区地址,然后回调至下一个函数。
5,获取当前分类下的当前行政区下的景区列表数据,使用正则匹配到需求数据,并获取下一页的地址,回调到本函数。遍历列表景区地址回调到详情数据获取函数。
6,编写详情页数据获取函数,使用正则匹配所需的数据。

五,文字解密

1,首先匹配到当前页面的加密css文件地址,然后请求该css地址,使用正则匹配到加密文件的地址,请求该加密文件地址,将加密文件下载到本地。
2,景区列表页与景区详情页面的字体加密文件(.woff)不同,每个景区列表页与每个景区详情页面的字体加密文件可能不同。
3,使用正则匹配我们需要的已经被加密的字符串,例如地址数据使用正则匹配到下面的字符串:
    地址:</span> <div id="J_map-show" class="map_address" > <span class="item" itemprop="street-address" id="address">
    <e class="address">&#xe16f;</e><e class="address">&#xefb4;</e>
    <e class="address">&#xe2f8;</e>1<d class="num">&#xe2db;</d>
    <e class="address">&#xed47;</e><d class="num">&#xe79c;</d>
    <e class="address">&#xe484;</e>(<e class="address">&#xeddc;</e>
    <e class="address">&#xf8d0;</e><e class="address">&#xe6b0;</e>
    <e class="address">&#xf708;</e><e class="address">&#xe037;</e>
    <e class="address">&#xe23d;</e>妇<e class="address">&#xf4e9;</e>
    <e class="address">&#xf7c5;</e><e class="address">&#xe16f;</e>
    <e class="address">&#xe35b;</e><e class="address">&#xe90e;</e>
    <e class="address">&#xe559;</e><e class="address">&#xefb4;</e>
    <e class="address">&#xf10e;</e>) </span> <div class="addressIcon">
4,对正则匹配出来的字符串进行抽取分离出来,然后对部分字符串进行替换(&#x--->uni)分离结果如下:
    ['unie16f', 'uniefb4', 'unie2f8', '1', 'unie2db', 'unied47',
    'unie79c', 'unie484', '(', 'unieddc', 'unif8d0', 'unie6b0',
    'unif708', 'unie037', 'unie23d', '妇', 'unif4e9', 'unif7c5',
    'unie16f', 'unie35b', 'unie90e', 'unie559', 'uniefb4', 'unif10e', ') ', ' ']
5,将获取到的.woff字体文件生成.xml文件,然后对.xml文件生成 {编码:索引} 的字典格式,如下:
    {'unie136': 0, 'unie79c': 1, 'uniee2b': 2, 'unif711': 3, 'unif597': 4, ......
6,根据字体文件位置对应生成字符串如下:
    woff_string = '''
        1234567890店中美家馆
        小车大市公酒行国品发电金心业商司
        超生装园场食有新限天面工服海华水
        房饰城乐汽香部利子老艺花专东肉菜
        学福饭人百餐茶务通味所山区门药银
        农龙停尚安广鑫一容动......'''
7,将上述的woff_string字符串切割成列表形式,如下所示:
	['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '店', '中', '美', '家',
    '馆', '小', '车', '大', '市', '公', '酒', '行', '国', '品', '发', '电', '金',
     '心', '业', ......]
8,将正则匹配切割好的加密数据列表(上述4步骤)分别对应在.woff生成的字典中(上述5步骤)找到对应的索引,然后在上述7步骤中按照索引找到对应的数据。
9,将解密的数据进行拼接成最终的解密数据

六,地址数据解密

	地址数据的加密方式与其他的加密方式稍有不同,其中地址数据中的中文(汉字)使用的加密字体文件与数字使用的字体加密文件不同,使用的是两种字体加密的混合。
1,使用正则获取地址数据
2,对字符串进行切割与替换,其中给定三个列表,
    text_list列表是用来存放每一位地址信息的,如下:[{'belong': 'address_list', 'index': 0, 'value': 'uniebf7'}, {'belong': 'address_list', 'index': 1, 'value': 'unie530'}...]
    chinese_list列表是存放的是中文切割替换出来的字符串,如下:['uniebf7', 'unie530', 'unie8f4', 'unie49b', 'unie252', 'unie593', 'unie9b6', 'unie384', 'unie980']
    num_list列表是存放的是数字切割替换出来的字符串,如下:['unif036', 'unif132', 'unif132']
3,将中文与数字分开进行解密,解密完成后根据text_list中对应的字典与索引进行匹配各位解密的数字,然后进行拼接。
4,注意:地址数据的切割与替换函数需要重写,与其他数据的切割与替换步骤不同。
5,地址的数据解密跟其他数据的解密方式不同,因为涉及到两个字体文件的处理。

七,将Scrapy更改为Scrapy-redis

1,导入模块:from scrapy_redis.spiders import RedisSpider
2,修改spiders文件中类的继承类
3,动态获取允许的域
    def __init__(self, *args, **kwargs):
    domain = kwargs.pop("domains", "")
    self.alllowed_domains = filter(None, domain.split(','))
    super(DzdpSpider, self).__init__(*args, **kwargs)
4,添加存放起始请求url的键:redis_key='start_url'
5,settings中添加以下配置信息
    # 设置重复过滤器模块,使重复过滤器使用redis中的集合进行去重
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    # 设置调度器模块,是调度器能够使用redis中的列表作为任务队列,储存和使用请求对象
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    SCHEDULER_PERSIST = True
    # 设置redis连接地址
    REDIS_URL = "redis://127.0.0.1:6379"
    DOWNLOAD_DELAY = 1      # 下载延迟

八,代理中间件(由于代理收费,该中间件未启用)

1,UA中间件
	(1)安装模块 fake_useragent
	(2)导入模块 from fake_useragent import UserAgent
	(3)编写UA中间件代码
    class UserAgentMiddleware(object):
        def process_request(self, request, spider):
            user_agent = UserAgent().chrome
            print('当前UA是{}'.format(user_agent))
            request.headers['User-Agent'] = 		user_agent
    (4)settings.py中**UA中间件
2,IP代理中间件
	我们使用的是阿布云代理(收费),代码如下:
        # 代理服务器
        proxyServer = "http://http-dyn.abuyun.com:9020"
        # 代理隧道验证信息
        proxyUser = "HPX8KNGF4B4R17GD"
        proxyPass = "F7B4421CB49EE54C"
        # for Python3
        proxyAuth = "Basic " + base64.urlsafe_b64encode(bytes((proxyUser + ":" + proxyPass), "ascii")).decode("utf8")

        class ProxyMiddleware(object):
            def process_request(self, request, spider):
                request.meta["proxy"] = proxyServer
                print('IP{}'.format(request.meta["proxy"]))
                request.headers["Proxy-Authorization"] = proxyAuth

            def process_response(self, request, response, spider):
                if response.status != '200':
                    request.dont_filter = True  # 重新发送的请求对象能够再次进入队列
                    return request
	

九,注意事项

1,数据获取中,评分数据是在列表页面进行获取,但是部分景区的评分数据是在详情页面,经过对比发现,虽然评分的名称不同,但是评分规则相同,因此我们在景区列表页面获取评分数据,在详情页面也获取评分数据,加入逻辑判断,在主页为获取到评分数据,而详情页面获取到评分数据时使用详情页获取的评分数据。
2,景区列表页面的加密字体匹配与详情页面的加密字体匹配机制略有不同,并不是同一个匹配函数。
3,景区列表页获取到的加密字体的切割替换与景区详情的加密字体的切割替换是不同的,是单独的函数。
4,景区列表页的界面函数与景区详情页面的数据界面函数是不同的,是单独的函数。
5,加密的方式大同小异,短时间内可能会发生更改,加密字体文件会发生改变,只要匹配机制完善就可以。

十,启动爬虫文件

1,cmd连接redis客户端,lpush start_url http://.....(起始url)
2,cmd打开项目目录,到spiders目录下执行命令:scrapy runspider 爬虫文件名.py

十一,数据的正则匹配代码示例

 # 景区名称
 name = re.findall('<h4>(.*?)</h4>', i, re.S)
 print(name[0])
 # 景区的点评数
 dianping = re.findall('<b>(.*?)</b>.*?条点评</a>', i, re.S)
 # 人均价格
 price = re.findall('<b>¥(.*?)</b>', i, re.S)
 # 总分
 total_points = re.findall('<span >总分(.*?)</span>', i, re.S)
 # 环境
 environment = re.findall('<span >环境(.*?)</span>', i, re.S)
 # 服务
 serve = re.findall('<span >服务(.*?)</span>', i, re.S)

十二,主页的加密字体文件的匹配与下载

def get_svg_url(self, soup):
    try:
        # 正则匹配有字体文件的css文件名
        svgtextcss = re.search(r'href="([^"]+svgtextcss[^"]+)"', soup, re.M)
        # 使用正则分组提取并拼接请求路径
        woff_url = 'http:' + svgtextcss.group(1)
        # 请求css路径,获取响应
        svg_html = requests.get(woff_url).text
        # 对响应中的数据进行切割
        lines = svg_html.split('PingFangSC-')
        # 在css响应中以url开头,以shopNum结尾的字符串中包含的是字体文件位置
        partern = re.compile(r',(url.*shopNum)')
        for line in lines:
            out = partern.findall(line)
            if len(out) > 0:
                woff = re.compile('\((.*?)\)')
                list_page_url = 'http:' + woff.findall(out[0])[0].replace('"', '')
                path = r'D:\code\DZDP\DZDP\spiders\woff\list_page.woff'
                with open(path, 'wb') as f:
                    f.write(requests.get(list_page_url).content)
                    print('当前列表页加密字体文件已下载完成!')
            else:
                pass
    except Exception as e:
        pass

十三,获取到的加密字符串的分离与替换

def split_str(self, str):
    if str:
        text_split = str.split('</svgmtsi>')
        # print(text_split)
        try:
            text_split.remove('')  # 有的分割后末尾是一个空字符串,需要删掉
        except ValueError:
            pass
        text_list = []
        for i in text_split:
            # print(i)
            try:
                dex = i.index('<')
            except ValueError:
                text_list.append(i)
                continue
            if dex != 0:
                text_list.append(i[:dex])
            tag = re.findall('.*?">&#x(.*?);', i[dex:])
            if tag:
                text_list.append('uni' + tag[0])
        # 如果有的标签第一位是编码,前面的逻辑是处理不了的,所以我们在判断一下把没替换的编码替换了
        final_list = []
        for t in text_list:
            if t.startswith('&#x'):
                new_s = 'uni' + re.findall('&#x(.*?);', t)[0]
                final_list.append(new_s)
            else:
                final_list.append(t)
        return final_list
    else:
        return

十四,主页数据的解密函数

def woff_change(self, woffdict):
    woff_string = '''
            1234567890店中美家馆
            小车大市公酒行国品发电金心业商司
            超生装园场食有新限天面工服海华水
            房饰城乐汽香部利子老艺花专东肉菜
            学福饭人百餐茶务通味所山区门药银
            农龙停尚安广鑫一容动南具源兴鲜记
            时机烤文康信果阳理锅宝达地儿衣特
            产西批坊州牛佳化五米修爱北养卖建
            材三会鸡室红站德王光名丽油院堂烧
            江社合星货型村自科快便日民营和活
            童明器烟育宾精屋经居庄石顺林尔县
            手厅销用好客火雅盛体旅之鞋辣作粉
            包楼校鱼平彩上吧保永万物教吃设医
            正造丰健点汤网庆技斯洗料配汇木缘
            加麻联卫川泰色世方寓风幼羊烫来高
            厂兰阿贝皮全女拉成云维贸道术运都
            口博河瑞宏京际路祥青镇厨培力惠连
            马鸿钢训影甲助窗布富牌头四多妆吉
            苑沙恒隆春干饼氏里二管诚制售嘉长
            轩杂副清计黄讯太鸭号街交与叉附近
            层旁对巷栋环省桥湖段乡厦府铺内侧
            元购前幢滨处向座下臬凤港开关景泉
            塘放昌线湾政步宁解白田町溪十八古
            双胜本单同九迎第台玉锦底后七斜期
            武岭松角纪朝峰六振珠局岗洲横边济
            井办汉代临弄团外塔杨铁浦字年岛陵
            原梅进荣友虹央桂沿事津凯莲丁秀柳
            集紫旗张谷的是不了很还个也这我就
            在以可到错没去过感次要比觉看得说
            常真们但最喜哈么别位能较境非为欢
            然他挺着价那意种想出员两推做排实
            分间甜度起满给热完格荐喝等其再几
            只现朋候样直而买于般豆量选奶打每
            评少算又因情找些份置适什蛋师气你
            姐棒试总定啊足级整带虾如态且尝主
            话强当更板知己无酸让入啦式笑赞片
            酱差像提队走嫩才刚午接重串回晚微
            周值费性桌拍跟块调糕'''
    woffs = [i for i in woff_string if i != '\n' and i != ' ']
    list_pagefont = TTFont(r'D:\code\DZDP\DZDP\spiders\woff\list_page.woff')
    list_pagefont.saveXML(r'D:\code\DZDP\DZDP\spiders\woff\list_page.xml')
    list_page_TTGlyphs = list_pagefont['cmap'].tables[0].ttFont.getGlyphOrder()[2:]
    list_page_dict = {}
    for i, x in enumerate(list_page_TTGlyphs):
        list_page_dict[x] = i
    # 取出传递过来的字典中的数据
    dianping_list = woffdict['dianping']
    price_list = woffdict['price']
    total_points_list = woffdict['total_points']
    enviroment_list = woffdict['enviroment']
    serve_list = woffdict['serve']
    # 对点评数进行解密
    if dianping_list:
        dianping = ''
        for char in dianping_list:
            text = char.replace('&#x', 'uni')
            if text in list_page_dict:
                content = woffs[list_page_dict[text]]
            else:
                content = char
            dianping += ''.join(content)
    else:
        dianping = '暂无'
    # 对价格数进行评价
    if price_list:
        price = ''
        for char in price_list:
            text = char.replace('&#x', 'uni')
            if text in list_page_dict:
                content = woffs[list_page_dict[text]]
            else:
                content = char
            price += ''.join(content)
    else:
        price = '暂无'
    # 对总分进行解密
    if total_points_list:
        total_points = ''
        for char in total_points_list:
            text = char.replace('&#x', 'uni')
            if text in list_page_dict:
                content = woffs[list_page_dict[text]]
            else:
                content = char
            total_points += ''.join(content)
    else:
        total_points = '暂无'
    # 对环境分进行解密
    if enviroment_list:
        enviroment = ''
        for char in enviroment_list:
            text = char.replace('&#x', 'uni')
            if text in list_page_dict:
                content = woffs[list_page_dict[text]]
            else:
                content = char
            enviroment += ''.join(content)
    else:
        enviroment = '暂无'
    # 对服务分进行解密
    if serve_list:
        serve = ''
        for char in serve_list:
            text = char.replace('&#x', 'uni')
            if text in list_page_dict:
                content = woffs[list_page_dict[text]]
            else:
                content = char
            serve += ''.join(content)
    else:
        serve = '暂无'
    result = {
        'dianping': dianping,
        'price': price,
        'total_points': total_points,
        'enviroment': enviroment,
        'serve': serve
    }
    return result

十五,切割地址数据函数

def split_address(self, str):
    text_list = []
    chinese_list = []
    num_list = []
    # 地址
    if str:
        text_split = re.split('</\S+>', str)
        try:
            text_split.remove('')  # 有的分割后末尾是一个空字符串,需要删掉
        except ValueError:
            pass
        # 处理的是字符串第一位不是编码的字符串
        for i in text_split:
            try:
                dex = i.index('<')
            except ValueError:
                if i != ' ':
                    s_dict = {'belong': None,
                              'index': None,
                              'value': i}
                    text_list.append(s_dict)
                continue
            # 将里面的夹杂的未进行加密的挑出来
            if dex != 0:
                if i[:dex] == ' ':
                    pass
                else:
                    s_dict = {'belong': None,
                              'index': None,
                              'value': i[:dex]}
                    text_list.append(s_dict)
            # 匹配 < 后面的加密的字段
            tag = re.findall('.*?">&#x(.*?);', i[dex:])
            if tag:
                val = 'uni' + tag[0]
            if 'address' in i[dex:]:
                chinese_list.append(val)
                s_dict = {'belong': 'address_list',
                          'index': chinese_list.index(val),
                          'value': val}
            elif 'num' in i[dex:]:
                num_list.append(val)
                s_dict = {'belong': 'num_list',
                          'index': num_list.index(val),
                          'value': val}
            else:
                s_dict = {'belong': None,
                          'index': None,
                          'value': val}
            text_list.append(s_dict)
    return text_list, chinese_list, num_list

十六,详情页数据的获取函数

def detail_page(self, response):
    item = response.meta['huihui']
    html = response.text
    t = html.find('验证中心')
    if t == -1:
        print('未遇到验证模块,开始获取加密字体')
        print('当前所在分类==={}===位置==={}==='.format(item['classify_name'], item['administrative_region_name']))
        # 星级
        start = re.findall('<span title=\"(.*)\" class=\"mid-rank-stars .*\"></span>', html, re.S)
        # 地址
        address = re.findall('<span .*? id="address">(.*?)</span>', html, re.S)
        # 电话
        phone = re.findall('<p class=\"expand-info tel\">(.*?)</p>', html, re.S)
        # 划算评分
        huasuan = re.findall('<span class="item">划算: (.*?)</span>', html, re.S)
        # 服务评分
        serve_score = re.findall('<span class="item">服务: (.*?)</span>', html, re.S)
        # 环境评分
        huanjing = re.findall('<span class="item">环境: (.*?)</span>', html, re.S)
        # 判断电话是否为空
        if phone:
            phone_list = self.split_list_page(phone[0])
        else:
            phone_list = []
        # 判断划算评分
        if huasuan:
            huasuan_list = self.split_list_page(huasuan[0])
        else:
            huasuan_list = []
        # 判断服务评分
        if serve_score:
            serve_score_list = self.split_list_page(serve_score[0])
        else:
            serve_score_list = []
        # 判断环境评分
        if huanjing:
            huanjing_list = self.split_list_page(huanjing[0])
        else:
            huanjing_list = []
        data = {
            'phone': phone_list,
            'huasuan': huasuan_list,
            'serve_score': serve_score_list,
            'huanjing': huanjing_list
        }
        # 判断地址知否为空
        if address:
            # 其中text_list是一个列表中存放字典,如下所示:
            # [{'belong': 'address_list', 'index': 0, 'value': 'uniebf7'},{'belong': 'address_list', 'index': 1, 'value': 'unie530'},
            # address_list 中存放的是中文的已经加密的字符列表
            # num_list 中存放的是数字的已经加密的字符列表
            text_list, address_list, num_list = self.split_address(address[0])
        else:
            text_list = []
            address_list = []
            num_list = []
        # 找到.woff文件并且下载到本地
        self.get_detailwoff_url(html)
        self.get_adress_url(html)  # 下载地址加密文件
        # todo 详情页数据解密
        result = self.detail_change(data)
        # todo 对主页的评分与详情的评分进行判断---判断列表页获取到的评分为空的时候就用详情中获取到的评分进行替换
        if item['total_points'] == '暂无':
            item['total_points'] = result['huasuan']
        if item['environment'] == '暂无':
            item['environment'] = result['huanjing']
        if item['serve_number'] == '暂无':
            item['serve_number'] = result['serve_score']
        try:
            item['start'] = start[0]
        except Exception as e:
            item['start'] = '该用户暂无星级'
        item['phone'] = result['phone'].strip().replace('&nbsp;', '')
        if text_list:
            result_address, result_num = self.address_change(address_list=address_list, num_list=num_list)
            end_address = ''
            for code in text_list:
                if code['belong'] == 'address_list':
                    str = result_address['address'][code['index']]
                    end_address += str
                elif code['belong'] == 'num_list':
                    str = result_num['num'][code['index']]
                    end_address += str
                elif code['belong'] is None:
                    str = code['value']
                    end_address += str
        else:
            end_address = '暂无'
        item['address'] = end_address
        print(item)
        yield item
    else:
        print('详情页正确请求地址url:{}'.format(response.meta['redirect_urls'][0]))
        print('出现验证码时的请求地址为:{}'.format(response.url))
        # 因为是分布式请求,因此并不能立即停止所有请求
        # self.crawler.engine.close_spider(self, '详情请求页面获取出错关闭爬虫')
        # 如果出现了滑块,及时终止进行手动验证
        time.sleep(10)
        # 请求失败,重新请求
        url = response.meta['redirect_urls'][0]
        yield scrapy.Request(url=url,
                             callback=self.detail_page,
                             headers=DEFAULT_REQUEST_HEADERS,
                             meta={'huihui': item},
                             dont_filter=False
                             )