zoukankan      html  css  js  c++  java
  • scrapy实战伯乐网文章爬虫

    scrapy实战伯乐网爬虫

    因为我们要对scrapy进行调试,所以我们建立一个main函数来达到调试的目的,以后每次调试只要debug这个main文件就行了

    from scrapy.cmdline import execute
    import sys
    import os
    sys.path.append(os.path.dirname(os.path.abspath(__file__)))
    
    execute(["scrapy", 'crawl', 'jobbole'])
    

    在spider文件夹中初始化爬虫之后,可以看到一个parse函数,这个是用来处理具体的网页内容的,可以用Xpath对网页源码进行解析,其中的response参数表示scrapy返回的网页

    我们要爬取所有的文章,就要先找到所有文章的存放地点,我们将class JobboleSpider里面的start_urls改为http://blog.jobbole.com/all-posts/,这个页面存放了所有的posts内容

    从第一页开始,每次爬取该页所有posts的链接,进入每一个链接进行处理,要处理一个链接,就是将这个链接yield出来,首先我们先编写在每一页中提取出所有posts的方法,在parse函数中,先找到所有存放posts文件的地方:通过chrome的元素选择快捷键(如下图所示),找到所有的存放posts文件的链接

    response_nodes = response.css('#archive .floated-thumb .post-thumb a')
    

    然后我们在找到的response_nodes中进行循环,并找到其中的首页图片地址和post地址,并将posts地址yield出去,交给scrapy.http.Request处理:

    for response_node in response_nodes:
        post_url = response_node.css('::attr(href)').extract_first(default="")
        image_url = response_node.css('img::attr(src)').extract_first(default="")
        yield Request(url=post_url, meta={'front_image_url':image_url},
                      callback=self.parse_detail)
    

    其中的回调函数是用于具体处理网页内容的函数,meta用于传送首页的图片地址,传送的形式是字典的形式

    接下来编写解析下一页的方法:

    通过找到下一页这个标签的地址,来进行下一页的访问,首先我们通过css选择器选择出下一页的标签,如果存在下一页,那么我们就将下一页yield到Request来处理,回调函数就是这个函数本身:

    #jobbole.py  parse
    next_page_url = response.css('a[class="next page numbers"]::attr(href)').extract_first(default="")
            if next_page_url:
                yield Request(url=next_page_url, callback=self.parse)
    

    接下来完成具体的parse_detail函数,用于具体解析每一页的posts内容的函数:

    首先拿出来meta里面的内容,为了防止意外报错,我们使用字典的get函数,并将默认值设置为空

    front_image_url = response.meta.get('front_image_url','')
    

    然后通过css或者是xpath解析器依次解析自己需要的内容

    接下来需要通过items.py建立自己的item,这个item就是你最后想要保存下来的数据:

    默认系统会自动帮你建立一个跟工程名字一样的类,继承的是scrapy.Item,如果你自己需要一个新的item的话,只需要按照相同的方法,在item.py中新建一个类,继承scrapy.Item,然后把你需要的字段一个个定义出来,定义的方法是字段名 = scrapy.Field(),具体代码如下:

    #items.py
    class JobBoleArticleItem(scrapy.Item):
        head = scrapy.Field()
        post_time = scrapy.Field()
        url = scrapy.Field()
        url_id = scrapy.Field()
        front_image_url = scrapy.Field()
        front_image_path = scrapy.Field()
        vote_num = scrapy.Field()
        comment_num = scrapy.Field()
        collection_num = scrapy.Field()
        tags = scrapy.Field()
        content = scrapy.Field()
    

    然后我们需要在爬虫文件中引入定义的item,在parse_detail中实例化item类,并将每一个字段都赋上从网页上解析出来的值,赋值方法是类似于字典的赋值方法:

    #jobbole.py
    from ..items import JobBoleArticleItem
    def parse_detail():
      article_item = JobBoleArticleItem()
      article_item['url'] = response.url
      article_item['head'] = head
      yield article_item
    

    剩余字段的赋值方法跟上面这两个是一样的,最后把item实例yield出来,交给pipelines处理,我们定义了一个专门用于处理图片的pipeline,继承的是scrapy.pipelines.images.ImagesPipeline,重构其中的item_completed函数,参数results中的value表示的是在settings.py中设置的跟image相关的参数,取出其中的path,赋值给item['front_image_path'],最后return item即可:

    #pipelines.py
    class articleImagePipeline(ImagesPipeline):
        def item_completed(self, results, item, info):
            for ok,value in results:
                image_file_path = value['path']
                item['front_image_path'] = os.path.abspath(image_file_path)
            return item
    

    settings.py中取消掉ITEM_PIPELINES的注释,并增加新的自己定义的articleImagePipeline,后面的数字表示进入pipelines的顺序,数字越小,越早进入。

    此时设置好IMAGES_URLS_FIELDIMAGES_STORE,这样就可以开始下载图片

    #settings.py
    ITEM_PIPELINES = {
       'bole.pipelines.BolePipeline': 300,
        # 'scrapy.pipelines.images.ImagesPipeline': 1,
        'bole.pipelines.articleImagePipeline': 1
    }
    IMAGES_URLS_FIELD = 'front_image_url'
    IMAGES_STORE = '../IMAGES'
    

    最后还有把url进行hash成固定长度的过程,建立一个python包叫做utils,里面建立一个common.py,在其中建立get_md5函数,其中的url要以utf8传入,所以一开始要检测其是不是unicode,python3里面的str就是unicode:

    #common.py
    def get_md5(url):
        if isinstance(url, str):
            url = url.encode('utf8')
        m = hashlib.md5()
        m.update(url)
        return m.hexdigest()
    

    接下来解决数据保存的问题,在这里我们使用两种方式,分别是:json文件和mysql数据库保存

    I. json的保存

    json保存有两种方式,一种是自己写json,一种是利用scrapy.exporter提供的JsonItemExporter

    ①自定义json文件,先用codecs打开json文件(这样打开不会有编码错误问题),然后重写process_item方法,将item先dumps为json,其中设置ensure_ascii=False以支持中文,最后写一个close_spider方法,关闭文件

    import codecs, json
    
    class MyjsonPipelines(object):
        #自定义的json导出
        def __init__(self):
          #打开文件
            self.file = codecs.open('myarticle.json', mode='w', encoding='utf8')
            
        def process_item(self, item, spider):
          #写入数据
            line = json.dumps(dict(item),ensure_ascii=False)
            self.file.write(line)
            return item
          
        def spider_close(self,spider):
          #关闭文件
            self.file.close()
    

    ②利用scrapy提供的JsonItemExporter,先定义打开的文件以及exporter,重写处理数据的方法process_item,最后关闭spider

    from scrapy.exporters import JsonItemExporter
    class jsonPipelines(JsonItemExporter):
        #调用scrapy提供的json exporter来导出json文件
        def __init__(self):
            self.file = open('article.json', 'wb')
            self.exporter = JsonItemExporter(self.file, encoding='utf8', ensure_ascii=False)
            self.exporter.start_exporting()
            
        def close_spider(self):
            self.exporter.finish_exporting()
            self.file.close()
            
        def process_item(self, item, spider):
            self.exporter.export_item(item)
            return item
    

    II. 写入mysql

    写入MySQL同样有两种方法,第一种是自己写函数同步写入,第二种是利用twisted.enterprise框架提供的adbapi异步写入,第二种写入的方法更快,但也更复杂

    ①利用MySQLdb,建立连接,数据库部分可以参考之前写的数据库基础教程

    import MySQLdb
    class MysqlPipeline(object):
        def __init__(self):
          #建立连接和cursor
            self.conn = MySQLdb.connect(host='127.0.0.1', user='root', password='123456',
                                        database='scrapy', 			
                                        port=3306,charset='utf8',use_unicode=True)
            self.cursor = self.conn.cursor()
            
        def process_item(self, item, spider):
          #数据插入的sql语句并执行和提交(excecute & commit)
            insert_sql = """
                INSERT INTO article (head, post_time, url, url_id) VALUES (%s,%s,%s,%s)
            """
            self.cursor.execute(insert_sql,(item['head'],item['post_time'],item['url'],item['url_id']))
            self.conn.commit()
    

    ②利用twisted.enterprise提供的adbapi插入数据到mysql,这里用到了一个@classmethod的方法,主要是用于初始化类之前,先进行一个操作的函数,比如在这里我们在初始化twisted_mysql_pipelines之前,先连接了数据库,我们将连接数据库的参数都放在了settings.py里面,要将其取出来要用到def from_settings(cls, settings),第二个参数是一个字典类型,取值可以通过字典的方法来取。

    from twisted.enterprise import adbapi
    class twisted_mysql_pipelines(object):
        def __init__(self, dbpool):
            self.dbpool = dbpool
    
        @classmethod
        def from_settings(cls, settings):
          #连接数据库,返回dbpool
            dbparm = dict(host=settings['MYSQL_HOST'],
                          user=settings['MYSQL_USER'],
                          password=settings['MYSQL_PASSWORD'],
                          database=settings['MYSQL_DBNAME'],
                          cursorclass = MySQLdb.cursors.DictCursor,
                          charset='utf8',
                          use_unicode=True)
            dbpool = adbapi.ConnectionPool('MySQLdb', **dbparm)#建立连接池
            return cls(dbpool)
    
        def process_item(self, item, spider):
            query = self.dbpool.runInteraction(self.do_insert, item)#开始异步插入数据
            query.addErrback(self.error_handler)#处理异常
    
        def error_handler(self, error):
          #异常处理函数
            print(error)
    
        def do_insert(self, cursor, item):
          #插入数据的sql语句
            insert_sql = """
                        INSERT INTO article (head, post_time, url, url_id) VALUES (%s,%s,%s,%s)
                    """
            cursor.execute(insert_sql, (item['head'], item['post_time'], item['url'], item['url_id']))
    

    itemloader

    之前的item是直接用字典的形式进行赋值的,如果使用itemloader会使得整个css查询过程看起来更加简洁清晰,具体使用方法如下:

    ①先在item.py中新建一个myItemLoader类,继承scrapy.loader.ItemLoader,修改其默认的输出处理函数为TakeFirst()(因为默认输出时一个列表,所以我们需要从里面取第一个)

    item.py
    from scrapy.loader import ItemLoader
    from scrapy.loader.processors import TakeFirst
    class myItemLoader(ItemLoader):
        default_output_processor = TakeFirst()
    

    ②在jobbole.py中的parse_detail函数中实例化item_loader,并使用add_cssadd_value方法,分别直接添加值或者是通过css寻找值,

    jobbole.py
    from ..items import myItemLoader
    from ..items import JobBoleArticleItem
    def parse_detail(self,response):
        item_loader = myItemLoader(item=JobBoleArticleItem(),response=response)
        item_loader.add_css('head', '.entry-header h1::text')
        item_loader.add_value('url', response.url)
        item_loader.add_value('url_id', get_md5(response.url))
        item_loader.add_css('post_time','.entry-meta-hide-on-mobile::text')
        item_loader.add_css('comment_num','a[href="#article-comment"] span::text')
        item_loader.add_css('vote_num','.vote-post-up h10::text')
        item_loader.add_css('collection_num','span.bookmark-btn::text')
        item_loader.add_css('tags','p.entry-meta-hide-on-mobile a::text')
        front_image_url = response.meta.get('front_image_url','')
        item_loader.add_value('front_image_url',front_image_url)
        article_item = item_loader.load_item()
    

    ③此时通过css找出来的是原始的数据,需要在item.py中写处理方法

    item.py
    from scrapy.loader.processors import MapCompose,TakeFirst,Join
    import re
    def post_time_handle(value):
        time_pattern = re.compile(r'd{4}/d{2}/d{2}')
        match = re.findall(time_pattern, value.strip())
        if match:
            print(match)
            post_time = match[0]
            return post_time
    
    def get_nums(value):
        match_re = re.match(".*?(d+).*", value)
        if match_re:
            nums = int(match_re.group(1))
        else:
            nums = 0
        return nums
    
    def remove_comment_tags(value):
        #去掉tag中提取的评论
        if "评论" in value:
            return ""
        else:
            return value
    

    ④修改item.pyJobBoleArticleItem类的input_processor,使其等于MapCompose(function),其中tags用到的output_processorscrapy.loader.processors.Join,将各个值用逗号连接起来

    class JobBoleArticleItem(scrapy.Item):
        head = scrapy.Field(input_processor = MapCompose(lambda x:x+'jobbole'))
        post_time = scrapy.Field(input_processor=MapCompose(post_time_handle))
        url = scrapy.Field()
        url_id = scrapy.Field()
        front_image_url = scrapy.Field()
        front_image_path = scrapy.Field()
        vote_num = scrapy.Field(input_processor=MapCompose(get_nums))
        comment_num = scrapy.Field(input_processor=MapCompose(get_nums))
        collection_num = scrapy.Field(input_processor=MapCompose(get_nums))
        tags = scrapy.Field(input_processor=MapCompose(remove_comment_tags),
                            output_processor=Join(','))
        content = scrapy.Field()
    
    

    Xpath语法

    • article:选取所有article元素的所有子节点
    • /article:选取根元素article
    • article/a:选取属于article的子元素(只能是子节点,不能是后辈节点)的a元素
    • //div:选取所有div子元素(不论出现在文档任何地方)
    • article//div:选取所有属于article元素后代的div元素
    • //@class:选取所有名为class的属性
    • /article/div[1]:选取属于article子元素的第一个div元素
    • /article/div[last()]:属于article的最后一个div
    • /article/div[last()-1]:倒数第二个
    • //div[@lang]:拥有lang属性的div元素
    • //div[@lang='eng']:选取所有lang属性为eng的div元素
    • /div/*:div元素的所有子节点
    • //*:选取所有元素
    • //div[@*]:所有带有属性的div元素
    • /div/a | //div/p:所有div元素的a或p元素
    • //sapn | //ul:所有文档中的span和ul元素
    • article/div/p | //span:所有属于article元素的div元素的p元素以及文档中所有span元素

    用xpath进行提取的方法类似于beautifulsoup,但是xpath的提取速度更快,提取的例子如下:

    1、我要提取页面中的title信息,通过F12打开网页控制,点击选择元素,点中需要爬取的部分,可以找到他的源码,右键复制xpath或者是自己写xpath进行爬取(要爬取内容的话在xpath后面要加上/xpath),之后通过extract()提取为列表,选择第[0]个元素,但是此时有可能列表为空,所以使用extract_first(default=0),表示提取第一个元素如果为空则返回0,写法如下:

    head_selector = response.xpath('//*[@class="entry-header"]/h1/text()')
    head = head_selector.extract()[0]
    
    
    post_time_selector = response.xpath('//p[@class="entry-meta-hide-on-mobile"]/text()')
    time_pattern = re.compile(r'd{4}/d{2}/d{2}')
    

    //*:表示选取所有的任意元素

    //p:选取所有的p元素

    //p[@class="dd"]:表示选取所有类为dd的p标签

    //p[contains(@class,"dd")]:选取类名包含dd的p元素

    CSS选择器

    • *:选择所有节点
    • #container:选择id为container的节点
    • .container:选取所有class中包含container的节点
    • li a:选取所有li下面的所有a节点
    • ul + p:选取ul后面的第一个p元素
    • div#container > ul :选取id为container的div的第一个ul子元素
    • ul ~ p:选取与ul相邻的所有p元素
    • a[title]:选择所有有title属性的a元素
    • a[href="http://jobbole.com"]:选取所有href属性为http://jobbole.com的a元素
    • a[href*="jobbole"]:选取所有href属性包含jobbole的a元素
    • a[href^="http"]:选取所有href以http开头的a元素
    • a[href$=".jpg"]:选取所有href以.jpg结尾的a元素
    • input[type=radio]:checked:选择选中的radio元素
    • div:not(#container):选择id不是container的div属性
    • li:nth-child(3):选取第三个li元素
    • tr:nth-child(2n):选择第偶数个tr

    pycharm单步调试的快捷键是F8

  • 相关阅读:
    HTML学习笔记
    "IIS无法启动"问题解决方法
    NET访问MySQl数据库中文乱码解决
    珍爱生命,远离肥胖,远离过劳死
    Bcp 命令注意事项
    阿里云万郁香:多样付费选择构筑成本最优的弹性体验
    性能提升40%!阿里云神龙大数据加速引擎获TPCxBB世界排名第一
    阿里云王志坤:强劲可靠、无处不在的云,为创新保驾护航
    发现新视界——视觉计算将如何改变生产方式
    Soul运维总监尤首智:企业如何从0到1建设云上运维体系
  • 原文地址:https://www.cnblogs.com/drawon/p/8520611.html
Copyright © 2011-2022 走看看