手记

Python爬虫实战-使用Scrapy框架爬取土巴兔(四)

上一篇文章Python爬虫实战-使用Scrapy框架爬取土巴兔(三)我们为爬虫工程添加了下载中间件和IP代理池。接下来就要开始着手具体的爬取规则。


    该篇文章主要讲如何分析网页并通过Scrapy中Spider来定制爬取规则

    一.分析网页

    当你想要爬取某个网站内容时一定是先了解网站网页的翻页URL规则和阅读网页的html源代码。了解翻页规则我们就能确定需要爬取的网页url。阅读html源代码是为了确定能够爬取的内容并且确定数据结构。

    我们要爬取的是土巴兔中家居图册这个模块。那么我们就确定了起始的URL是http://xiaoguotu.to8to.com/tuce/往下拉点击第二页后可以看到,网址变为http://xiaoguotu.to8to.com/tuce/p_2.html 。 那么可以确定翻页规则匹配的正则就是"xiaoguotu.to8to.com/tuce/p_\d+.html"。按F12调出开发者工具,通过阅读html代码我们很容易就知道在html中哪个节点位置可以获取进入详情页的url。接下来点进某个图册,阅读源码我们很简单就能找出图片的链接。但是一个图册包括了多张图片。html源码中并没有展示所有图片的链接,那我们该怎么办,这是个疑问。依旧利用开发者工具点击XHR这个选项,接下来一目了然,原来土巴兔是通过AJAX请求的方式来加载数据的。http://xiaoguotu.to8to.com/getxgtjson.php?a2=0&a12=&a11=10043657&a6=&a3=&a10=2 这个url返回了json格式的数据。在json数据中我们可以看到cid为c10043657的图册,id为101,共有5张图片。我们也可以看到图片的url和标题,但是剩下的字段,例如sid:"19"、hxid:"7" ,我们都无法知道它具体代表什么。接下来就需要我们的耐心一点一点去寻找蛛丝马迹,终于在xiaoguotu_new.js这个请求中,我们看到了刚才说的sid:"19"、hxid:"7"的含义。


    二.数据结构

    经过一番周折,我们终于知道了该如何获取到关于图册的所有信息。
    确定数据结构,并编写继承scrapy.Item的Item实体和一些常量

    import scrapyclass DesignPictureItem(scrapy.Item):
        fid = scrapy.Field()  # 唯一标识
        title = scrapy.Field()  # 标题
        sub_title = scrapy.Field()  # 副标题
        html_url = scrapy.Field()  # 所爬取的网页链接
        tags = scrapy.Field()  # 标签
        description = scrapy.Field()  # 描述
        img_url = scrapy.Field()  # 图片URL
        img_width = scrapy.Field()  # 图片宽度
        img_height = scrapy.Field()  # 图片高度
        img_name = scrapy.Field()  # 图片名称
    ZONE_TYPE = {'1': '客厅', '2': '卧室', '3': '餐厅', '4': '厨房', '5': '卫生间', '6': '阳台', '7': '书房', '8': '玄关', '10': '儿童房', '11': '衣帽间', '12': '花园'}
    STYLE_ID = {'13': '简约', '15': '现代', '4': '中式', '2': '欧式', '9': '美式', '11': '田园', '6': '新古典', '0': '混搭', '12': '地中海', '8': '东南亚', '17': '日式',            '18': '宜家',            '19': '北欧', '20': '简欧'}
    COLOR_ID = {'1': '白色', '2': '黑色', '3': '红色', '4': '黑色', '5': '绿色', '6': '橙色', '7': '粉色', '8': '蓝色', '9': '灰色', '10': '紫色', '11': '棕色', '12': '米色',            '13': '彩色', '14': '原木色'}
    PART_ID = {'336': '背景墙', '16': '吊顶', '14': '隔断', '9': '窗帘', '340': '飘窗', '33': '榻榻米', '17': '橱柜', '343': '博古架', '333': '阁楼', '249': '隐形门', '21': '吧台',           '22': '酒柜', '23': '鞋柜', '24': '衣柜', '19': '窗户', '20': '相片墙', '18': '楼梯', '359': '其他'}
    AREA = {'1': '60㎡以下', '2': '60-80㎡', '3': '80-100㎡', '4': '100-120㎡', '5': '120-150㎡', '6': '150㎡以上'}
    HX_ID = {'1': '小户型', '7': '一居', '2': '二居', '3': '三居', '4': '四居', '5': '复式', '6': '别墅', '8': '公寓', '9': 'loft'}

    三.爬取规则

    Spider类定义了如何爬取、网站。包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item)。更详细的Spider说明请阅读官方文档
    Scrapy也在框架中定义不同用途的Spider,如:

    • CrawlSpider:定义了一些规则(rule)来提供跟进link的方便的机制

    • XMLFeedSpider:被设计用于通过迭代各个节点来分析XML源

    • CSVFeedSpider:该spider除了其按行遍历而不是节点之外其他和XMLFeedSpider十分类似

    • SitemapSpider:使您爬取网站时可以通过 Sitemaps 来发现爬取的URL

    这里显然CrawlSpider更适合我们来定义爬虫规则。
    下面是爬虫的完整代码:

    class DesignPictureSpider(CrawlSpider):
    
        #定义爬虫名称
        name = 'design_picture'
        #定义起始域名
        start_url_domain = 'xiaoguotu.to8to.com'
        #定义允许的域名
        allowed_domains = ['to8to.com']    #定义起始的URL
        start_urls = ['http://xiaoguotu.to8to.com/tuce/']    #定义翻页规则,及回调方法
        rules = (
            Rule(LinkExtractor(allow="/tuce/p_\d+.html"), follow=True, callback='parse_list'),
        )    #定义数据处理完后,处理Item的管道
        custom_settings = {        'ITEM_PIPELINES': {            'tubatu.pipelines.DesignPicturePipeline': 302,
            }
        }    #Service封装具体的数据处理逻辑
        design_picture_service = DesignPictureService()    
        #获取到网页数据后的回调方法
        def parse_list(self, response):
            selector = Selector(response)        #利用xpath提取元素
            items_selector = selector.xpath('//div[@class="xmp_container"]//div[@class="item"]')        for item_selector in items_selector:            #拼接详情页URL,格式为:http://xiaoguotu.to8to.com/c10037052.html
                cid = item_selector.xpath('div//a/@href').extract()[0][2:-6]
                title = item_selector.xpath('div//a/@title').extract()[0]            #拼接获取数据的URL,格式: http://xiaoguotu.to8to.com/getxgtjson.php?a2=0&a12=&a11=10037052&a1=0
                next_url = (constant.PROTOCOL_HTTP + self.start_url_domain + '/getxgtjson.php?a2=0&a12=&a11={cid}&a1=0').format(cid=cid)            #创建一个请求,反给Scrapy引擎
                yield scrapy.Request(next_url, self.parse_content, meta={'cid': cid, 'title': title})#通过meta来传递数据
        
        #解析详情方法
        def parse_content(self, response):
            uuid = utils.get_uuid()
            cid = response.meta['cid']
            title = response.meta['title']        try:            #解析json数据
                data = json.loads(response.text)        except:
                print("-----------------------获取到json:" + response.text + "------------------------------")            return
            data_img_list = data['dataImg']
            data_album_list = None
            for _data_img in data_img_list:            if _data_img['cid'] == cid:
                    data_album_list = _data_img['album']                break
            for data_album in data_album_list:
                data_img = data_album['l']            #取得单张图片URL http://pic.to8to.com/case/1605/05/20160505_f0af86a239d0b02e9635a47ih5l1riuq_sp.jpg
                img_url = 'http://pic.to8to.com/case/{short_name}'.format(short_name=data_img['s'])            #过滤重复URL
                if self.design_picture_service.is_duplicate_url(img_url):                break
                sub_title = data_img['t']
                original_width = data_img['w']
                original_height = data_img['h']
                tags = []            try:
                    zoom_type = ZONE_TYPE[data_img['zid']]                if zoom_type is not None or not zoom_type.strip() == '':
                        tags.append(zoom_type)            except KeyError:                pass
                try:
                    style_id = STYLE_ID[data_img['sid']]                if style_id is not None or not style_id.strip() == '':
                        tags.append(style_id)            except KeyError:                pass
                try:
                    area = AREA[data_img['a']]                if area is not None or not area.strip() == '':
                        tags.append(area)            except KeyError:                pass
                try:
                    color_id = COLOR_ID[data_img['coid']]                if color_id is not None or not color_id.strip() == '':
                        tags.append(color_id)            except KeyError:                pass
                try:
                    house_type = HX_ID[data_img['hxid']]                if house_type is not None or not house_type.strip() == '':
                        tags.append(house_type)            except KeyError:                pass
                try:
                    part = PART_ID[data_img['pid']]                if part is not None or not part.strip() == '':
                        tags.append(part)            except KeyError:                pass
                #创建Item对象并返回
                try:
                    design_picture_item = DesignPictureItem()  # type: DesignPictureItem
                    design_picture_item['fid'] = uuid
                    design_picture_item['html_url'] = response.url
                    design_picture_item['img_url'] = img_url
                    design_picture_item['tags'] = tags
                    design_picture_item['title'] = title
                    design_picture_item['sub_title'] = sub_title
                    design_picture_item['img_width'] = original_width
                    design_picture_item['img_height'] = original_height
                    design_picture_item['description'] = design_picture_item['title']                yield design_picture_item            except Exception as e:
                    print("-----------------------获取到json:" + response.text + "------------------------------")
                    log.warn("%s ( refer: %s )" % (e, response.url))                if config.USE_PROXY:
                        proxy_pool.add_failed_time(response.meta['proxy'].replace('http://', ''))

    当然这里还有一个重要的知识点是关于如何使用XPath 。XPath可以用来在XML文档中对元素和属性进行遍历,从而从网页中提取出我们需要的信息。更多XPath的语法知识可以参考http://www.w3school.com.cn/xpath/index.asp

    四.布隆过滤

    好奇的同学可能会有疑问,如果我们爬到重复的URL时怎么办呢?
    接下来就要引入布隆过滤。我们将爬取过的URL通过布隆算法存入Redis中,每次爬取一个新的URL时再通过布隆算法来识别是否是重复的URL,如果是重复的就不处理这个URL了。
    布隆过滤的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
    在URL去重上牺牲一定的准确性来降低Redis存储开销和提高爬虫整体效率也是值得的。

    from redis import StrictRedisclass SimpleHash(object):
        def __init__(self, cap, seed):        self.cap = cap        self.seed = seed    def hash(self, value):
            ret = 0
            for i in range(value.__len__()):
                ret += self.seed * ret + ord(value[i])        return (self.cap - 1) & retclass RedisBloomFilter(object):
        def __init__(self, redis_client: StrictRedis):        self.bit_size = 1 << 25
            self.seeds = [5, 7, 11, 13, 31, 37, 61]        self.redis = redis_client        self.hash_dict = []        for i in range(self.seeds.__len__()):            self.hash_dict.append(SimpleHash(self.bit_size, self.seeds[i]))    def is_contains(self, value, key):        if value is None:
                return False        if value.__len__() == 0:            return False
            ret = True        for f in self.hash_dict:
                loc = f.hash(value)
                ret = ret & self.redis.getbit(key, loc)        return ret    def insert(self, value, key):        for f in self.hash_dict:
                loc = f.hash(value)            self.redis.setbit(key, loc, 1)

    最后

    爬取规则也完成了,接下来就是我们的收尾工作, Python爬虫实战-使用Scrapy框架爬取土巴兔(五)通过Item Pipeline处理Item实体。

    附:

    详细的项目工程在Github中,如果觉得还不错的话记得Star哦。



    作者:imflyn
    链接:https://www.jianshu.com/p/8c5bc23f4fec


    0人推荐
    随时随地看视频
    慕课网APP