五、scrapy实战之云起书院小说信息抓取

  1. scrapy项目结构与各个组件的作用之前已经讨论过了,需要多多掌握的是scrapy内部运行机理,请求如何处理,这样我们才能理解中间件的概念,各个函数的作用。此次项目目标是爬去云起书院小说信息,存入mongodb数据库,使用redis去重,由于使用了redis数据库,可以将爬虫分布式运行。使用scrapy新建爬虫项目
    scrapy startproject yunqiCrawl

  2. 页面分析
    云起书院小说条目基本如下,需要从中提取小说标题、作者、分类、状态、更新时间、总字数、小说图片url、小说id信息



    在点击小说具体信息后,进入小说页面,可以看到小说人气如下,同样抓取人气信息



    通过scrapy shell url进入scrapy的shell中,使用该response慢慢调试需要构造的xpath语句,使用正确的xpath语句从页面中提取信息,取定xpath语句
  3. 项目需要两种类型的Item,一种是小说列表页面的小说信息组成的Item,另一种是小说人气信息,所以先定义两个Item,代码如下,定义了需要的域,存放信息
    vim yunqiCrawl/items.py

    import scrapy
    
    
    class YunqiBookListItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        novelId = scrapy.Field()
        title = scrapy.Field()
        author = scrapy.Field()
        link = scrapy.Field()
        status = scrapy.Field()
        updateTime = scrapy.Field()
        wordsCount = scrapy.Field()
        imageUrl = scrapy.Field()
        novelType = scrapy.Field()
    
    
    class YunqiBookDetialItem(scrapy.Item):
        novelId = scrapy.Field()
        allClick = scrapy.Field()
        allLike = scrapy.Field()
        weekLike = scrapy.Field()
    
  4. 编写spider模块,爬取页面信息,设置start_url等信息。讨论一些request对象的构造,spider返回request对象后,会经过调度器和下载器处理,返回response,继续交给spider处理,spider中可以定义多个处理函数,在request构造时,通过给callback参数传如处理函数,可以指定,该request经过处理返回的response使用哪个函数来解析。rules定义了一组从网页中提取链接的规则,LinkExtractor对象从页面中找到url,指定callback函数,直接以request的形式返回,使我们直接跳过了提取该页面url的步骤。spider对象的name指定了爬虫名称,allowed_domains指定了允许的域名,start_urls指定了爬虫程序的起点。程序时用yield关键字写成生成器的方式,可迭代

    # -*- coding: utf-8 -*-
    import scrapy
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    from yunqiCrawl.items import YunqiBookListItem,YunqiBookDetialItem
    
    class YunqiQqComSpider(CrawlSpider):
        name = 'yunqi.qq.com'
        allowed_domains = ['yunqi.qq.com']
        start_urls = ['http://yunqi.qq.com/bk/so2/n30p1']
    
        rules = (
            Rule(LinkExtractor(
                 allow=r'/bk/so2/n30p\d+'),
                 callback='parse_book_list',
                 follow=True),
        )
    
        def parse_book_list(self, response):
            books = response.xpath('.//div[@class="book"]')
            for book in books:
                novelId = book\
                    .xpath('./div[@class="book_info"]/h3/a/@id').extract_first()
                novelImageUrl = book\
                    .xpath('./a/img/@src').extract_first()
                novelLink = book\
                    .xpath('./div[@class="book_info"]/h3/a/@href').extract_first()
                novelTitle = book\
                    .xpath('./div[@class="book_info"]/h3/a/text()').extract_first()
                novelInfos = book\
                    .xpath('./div[@class="book_info"]/dl/dd[@class="w_auth"]')
                if len(novelInfos) > 4:
                    novelAuthor = novelInfos[0].xpath('./a/text()').extract_first()
                    novelTypeB = novelInfos[1].xpath('./a/text()').extract_first()
                    novelStatus = novelInfos[2].xpath('./text()').extract_first()
                    novelUpdateTime = novelInfos[3].xpath('./text()')\
                        .extract_first()
                    novelWordsCount = novelInfos[4].xpath('./text()')\
                        .extract_first()
                else:
                    novelAuthor = ''
                    novelTypeB = ''
                    novelStatus = ''
                    novelUpdateTime = ''
                    novelWordsCount = ''
                bookItem = YunqiBookListItem(
                    novelId=novelId,
                    title=novelTitle,
                    link=novelLink,
                    author=novelAuthor,
                    status=novelStatus,
                    updateTime=novelUpdateTime,
                    wordsCount=novelWordsCount,
                    novelType=novelTypeB,
                    imageUrl=novelImageUrl
                )
                yield bookItem
                newRequest = scrapy.Request(
                    url=novelLink,
                    callback=self.parse_book_detail
                )
                print 'send request',novelLink
                newRequest.meta['novelId'] = novelId
                yield newRequest
    
        def parse_book_detail(self, response):
            novelId = response.meta['novelId']
            tdlist = response.xpath('.//div[@class="num"]/table/tr/td')
            novelAllClick = tdlist[0]\
                .xpath('./text()').extract_first().split(u':')[1]
            novelAllLike = tdlist[1]\
                .xpath('./text()').extract_first().split(u':')[1]
            novelWeekLike = tdlist[2]\
                .xpath('./text()').extract_first().split(r':')[1]
            bookDetialItem = YunqiBookDetialItem(
                novelId=novelId,
                allClick=novelAllClick,
                allLike=novelAllLike,
                weekLike=novelWeekLike
            )
            yield bookDetialItem
    
  5. pipeline
    在ItemPipeline中,可以完成数据存储操作,由于需要 将数据存储在mongodb中,在pipeline中同时完成mongodb数据库的连接。我们需要了解mongodb数据基本操作语法。ItemPipeline有一个特殊的from_crawler类方法,该方法,接受一个crawler对象,返回一个该类的实例,crawler是正在处理的spider,通过这个spider,可以获取到全局信息,比如settings.py文件中的设置信息,在该方法中获取配置信息,open_spider函数在打开spider时执行,close_spider函数在关闭spider时运行,在本例中,分别写入了打开mongodb连接和关闭mongodb连接操作。process_item是处理item的主要方法,在其中进行数据的存储操作

    import pymongo
    from yunqiCrawl.items import YunqiBookListItem, YunqiBookDetialItem
    
    
    class YunqicrawlPipeline(object):
        def __init__(self, mongo_db, mongo_uri):
            self.mongo_uri = mongo_uri
            self.mongo_db = mongo_db
    
        @classmethod
        def from_crawler(cls, crawler):
            return cls(
                mongo_db=crawler.settings.get('MONGO_DATABASE', 'yunqi'),
                mongo_uri=crawler.settings.get('MONGO_URI')
            )
    
        def open_spider(self, spider):
            self.client = pymongo.MongoClient(self.mongo_uri)
            self.db = self.client[self.mongo_db]
    
        def close_spider(self, spider):
            self.client.close()
    
        def process_item(self, item, spider):
            self.db.bookInfo.insert(dict(item))
    

    在settings中添加mongodb的信息


  6. 针对反爬
    使用随机user-agent头防止爬虫被发现,修改request请求头的操作,应该在下载器下载之前完成,对的,就是下载器中间件,在下载器中间件中,获取settings中的User-agent列表,使用random模块随机选择一个User-Agent头对request头进行修改,随后在settings文件中启用该中间件
    User-Agents列表内容较长,就是一个包含各种agents的列表,写入settings.py文件中即可



    编写下载器中间件对request中的User-Agent进行修改,注意理解from_crawler函数,该函数可通过crawler参数获取爬虫全局信息,经常用于获取配置信息,下面的类时在middlewires文件中添加的。

    class RandomUserAgent(object):
        def __init__(self, agents):
            self.agents = agents
    
        @classmethod
        def from_crawler(cls, crawler):
            return cls(crawler.settings.getlist('USER_AGENTS'))
    
        def process_request(self, request, spider):
            request.headers.setdefault('User-Agent', random.choice(self.agents))
    

    随后在settings.py文件中启用该中间件,下载器中间件,系统默认会启用一些,如果需要禁用它们,需要在配置文件中说明,如下,禁用系统本来的user-agent中间件,使用我们自己编写的user-agent中间件



    此外我们可以设置自定义request的headers信息


    禁用cookie,在配置文件中说明即可


  7. 去重
    使用redis缓存去重的方式较为简单,配置redis服务器信息,安装scrapy_redis,随后在配置文件中添加


    需要说明的是,该去重方式实际上使用了set集合元素的单一性,效率堪忧。关于去重,有专门的BloomFilter算法可以使用,也可以在redis中使用该算法去重,使用github前辈写好的工程项目即可

  8. 分布式
    在不使用redis队列时,原始的爬虫数据流动如下图



    在使用redis队列调度之后,爬虫数据流如下



    request队列存入redis服务器中,通过redis服务器来管理,这样可以将一个项目放在多台主机上,使用同一台redis服务器,可以达到分布式爬虫的效果。使用redis的步骤也不算繁琐,首先python的redis模块必不可少,使用pip安装即可,另外scrapy与redis对接的模块scrapy_redis也需要提前安装。接下来在配置文件中配置即可,即编辑settings.py文件

    第一行指定了使用redis调度,第二行指定了使用redis队列,第三行是说状态信息会得到保存,以便于停止,重新运行,然后指定了redis主机信息。程序需要mongodb服务与redis服务,最后在命令行启动该爬虫即可。

    tips:该项目源码来自《python爬虫开发与项目实战》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容