测试开发程序员我的Python自学之路

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

2017-03-18  本文已影响1597人  imflyn

上一篇文章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 scrapy

class 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更适合我们来定义爬虫规则。
下面是爬虫的完整代码:

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 StrictRedis

class 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) & ret

class 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哦。

上一篇下一篇

猜你喜欢

热点阅读