2021-02-04-scrapy爬虫案例1:爬取博客园新闻版块详情页-基础入门篇[通俗易懂]

2021-02-04-scrapy爬虫案例1:爬取博客园新闻版块详情页-基础入门篇[通俗易懂]作者:Barranzi_注:本文所有代码、案例测试环境:1.Linux–系统版本:Ubuntu20.04LTS2.windows–系统版本:WIN1064位家庭版所需第三方库安装pillowpipinstallpillow-ihttps://pypi.douban.com/simplemysqlclientpipinstallmysqlclient-ihttps://pypi.douban.com/simple新建scrapy项目

大家好,又见面了,我是你们的朋友全栈君。

作者:Barranzi_

在这里插入图片描述

注:本文所有代码、案例测试环境:1.Linux – 系统版本:Ubuntu20.04 LTS 2.windows – 系统版本:WIN10 64位家庭版

  • 所需第三方库安装

    • pillow

      pip install pillow -i https://pypi.douban.com/simple
      
    • mysqlclient

      pip install mysqlclient -i https://pypi.douban.com/simple
      
  • 新建scrapy项目

    • 创建项目:

      scrapy startproject demo01_jobbole
      
    • 新建爬虫:

      cd demo01_jobbole
      scrapy genspider jobbole http://news.cnblogs.com/
      
  • 项目初始配置及准备工作

    • 项目文件同级目录下新建main.py,用于run整个爬虫项目

    • main.py内编写如下代码:

      main.py
      
      # -*- coding:utf-8 _*-
      """ @version: author:weichao_an @time: 2021/01/29 @file: main.py @environment:virtualenv @email:awc19930818@outlook.com @github:https://github.com/La0bALanG @requirement: """
      import sys
      import os
      from scrapy.cmdline import execute
      
      #将当前文件添加到系统路径
      sys.path.append(os.path.dirname(os.path.abspath(__file__)))
      
      #脚本方式一键运行scrapy项目
      execute(['scrapy','crawl','jobbole'])
      
    • settings.py内进行初始配置:将是否遵循robots协议更改为false,以防止爬虫获取数据不完整:

      settings.py
      
      # Obey robots.txt rules
      ROBOTSTXT_OBEY = False
      
    • 项目文件内新建images文件夹,用于保存获取的图片;

    • settings.py中配置图片下载路径:

      settings.py
      
      IMAGES_URLS_FIELD = 'front_image_url'
      projects_dir = os.path.dirname(os.path.abspath(__file__))
      IMAGES_STORE = os.path.join(projects_dir,'images')
      
    • 新建数据库:article_spider(可以先预想好数据库名称,待后续items中定义完毕数据结构之后,再进行具体的建库操作,这一步,是为了先方便在settings.py中配置数据库的基本信息,方便最后的入库)

      settings.py
      
      MYSQL_HOST = '127.0.0.1'
      MYSQL_DBNAME = 'article_spider'
      MYSQL_USER = 'root'
      MYSQL_PASSWORD = '******'#写你自己本机数据库的密码就行了
      
  • 页面结构分析及所需数据结构简单分析

    这次我们要爬的是博客园的新闻内容详情页,url地址:http://news.cnblogs.com/

    我们先访问一下,打开对应的新闻首页:

    请添加图片描述

    看到首页的内容后,我们先简单看下页面的结构:

    • 新闻首页看上去是以“列表”的形式先呈现每一篇新闻的概要描述;
    • 其中的新闻标题可以点击,点击后直接跳转至新闻详情页面;

    以及看下后续几页的内容及其分页规律:

    请添加图片描述

    • 新闻页面通过分页技术展示概要详情;
    • 每一页之间的url应该是存在规律的(预先猜想,毕竟还没有实质调试页面及URL规律)

    随便选一个新闻我们点进去看一下:

    请添加图片描述

    这次我们想要的数据,就存在于每一个具体新闻页面内部。

    那么我们到底需要什么数据呢?我们需要每一个新闻内容的如下数据:

    • 新闻的标题
    • 新闻的发布时间
    • 该条新闻对应的详情页的请求路径(因为只有获得了请求路径我们才可能请求到每一个新闻的详情页面)
    • 详情页图片的下载连接
    • 详情页图片的保存路径
    • 该篇新闻的评论数
    • 该篇新闻的阅读数
    • 该篇新闻的推荐数
    • 该篇新闻的标签
    • 该篇新闻的内容主体

    先简单分析到这,毕竟这只是个简单的爬虫项目,我们确实没什么必要提前把数据模型设计做的非常到位,我们可以先明确主体数据结构,待后续分析页面及代码实现的过程中,如果还需要再新增何种数据,灵活再添加即可。

    现在,我们先定义好items:

    items.py
    
    # Define here the models for your scraped items
    #
    # See documentation in:
    # https://docs.scrapy.org/en/latest/topics/items.html
    
    import scrapy
    
    
    class Demo01JobboleItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        pass
    
    #不使用提供的模板,我们自己定义一个item,只需要像模板一样让自己的item类也继承scrapy.Item即可
    
    class JobBoleArticleItem(scrapy.Item):
        ''' 定义数据结构,对应到数据库中也就是所需字段 '''
    
        #新闻标题
        title = scrapy.Field()
    
        #新闻发布时间:因为要入库,预想到,很大可能是时间日期格式数据
        create_date = scrapy.Field()
    
        #每一个新闻详情页的请求URL
        url = scrapy.Field()
    
        #待定:后续讲解该字段的含义
        url_object_id = scrapy.Field()
    
        #详情页包含的图片下载URL
        front_image_url = scrapy.Field()
    
        #图片保存路径
        front_image_path = scrapy.Field()
    
        #点赞/推荐数量
        praise_nums = scrapy.Field()
        
        #评论数量
        comment_nums = scrapy.Field()
    
        #新闻阅读数量
        fav_nums = scrapy.Field()
        
        #新闻所属标签:预想到标签可能不止一个
        tags = scrapy.Field()
        
        #新闻页面的主体内容
        content = scrapy.Field()
    

    欧克,明确需求之后,接下来我们就开始详细的分析页面结构及URL规律。

  • 详细分析页面结构及URL规律

    第一步,我们先分析每个新闻列表页的URL规律。

    F12打开控制台,随意切换几页,观察URL规律如下:

    第一页:https://news.cnblogs.com/[n/page/1/]
    第二页:https://news.cnblogs.com/n/page/2/
    第三页:https://news.cnblogs.com/n/page/3/
    第四页:https://news.cnblogs.com/n/page/4/
    ...
    第n页:https://news.cnblogs.com/n/page/n/
    

    看上去规律挺简单的,使用自增1的数字表示第几页,所以看上去我们可以通过循环来模拟页数的变化,但是,这样做其实有一个局限:

    如果我们想进行全站爬取呢?

    什么意思,也就是:

    不管新闻列表页最大页数能达到多少,有几页数据,我就爬几页

    (通过上一步简单分析我们已经看出,博客园只把新闻列表页展示前100页的内容,但其实我们都知道,博客园运营了好几年,所发布的新闻也绝不止100页的内容吧?)

    那么如果是为了满足全站爬取的需求,那么我们的循环模拟页数,到底要循环至多大的数字呢?我们也不可能写一个死循环吧?所以,这种方式其实并不可取。

    那么,到底该如何能够实现不断的获取下一页的URL呢?

    我们留意一下页数:

    请添加图片描述

    我们发现,其实每一页想要跳转到下一页,我不仅可以选择手动去选择查看第几页,一个更好的方式是我们可以直接点击这个next,就自动跳转到下一页了。

    那也就是说,下一页的URL,一定是包含在next这个富文本内的。

    所以,我们可以先捋一下思路:

    • 我们先想办法获取第一页的数据;

    • 然后我们只要能解析到这个next内部的下一页的URL,不就ok了么?

    • 每一页我们都解析出下一页的URL,然后让解析函数递归执行,下一页的新闻列表内容,不也就解析出来了么?

    • 那如何才能获取到下一页新闻列表的URL呢?我们只需要从当前新闻列表页解析出来就可以了啊?

    至此,爬虫的第一步思路就出来了:

    • 先请求第一页新闻列表页;
    • 获取该列表页中每一条新闻的详情页面的URL(毕竟这才是我们获取所需数据的前提)交给scrapy进行下载并调用相应的解析方法
    • 获取下一页的URL交给scrapy进行下载,下载完毕下一页后调用当前解析函数继续执行(递归调用,也就是再一次执行解析第二页新闻列表页)

    至此,有关每一页新闻列表的URL解析我们就分析完了。

    继续,接下来分析新闻列表页的数据。

    F12打开控制台,我们使用抓手工具抓到任意一个新闻概要区域,如下图:
    请添加图片描述

    从上图及相关的标示中我们能够看出:

    • 每一个新闻概要内容,其实都对应一个class为news_block的div;
    • 而这么多个新闻概要内容其实都在一个大的,id为news_list的div内部;

    但是此时,我们需要注意:控制台elements选项中展示出来的页面结构,是经过服务器响应及浏览器渲染结束之后的页面,这里存在的数据,并不代表在我们的具体请求的响应体中也存在,所以,为了证明我们在这看到的数据是否真实存在,我们需要查看一下当前页面的源代码,或切换至页面请求的response区域验证一下。

    我们现在右键 ,点击查看网页源代码,我们随便检索一下这个news_block,看看所需数据是否真实存在:
    请添加图片描述

    欧克,经过验证,确实真实存在于源码中(也就是真实存在于页面的response);

    根据上述的分析,我们发现,其实每个news_block是news_list的子节点,所以我们可以先获取news_list节点,而后通过遍历该节点,就可以拿到每一个新闻概要的内容了。

    先分析到这,我们现在开始写代码。

  • 伴随页面的详细分析开始编码

    进入爬虫文件jobbole.py,我们所有的数据解析全部都在爬虫文件内实现。

    打开文件,我们发现scrapy已经为我们初始化好了基本的代码结构:

    class JobboleSpider(scrapy.Spider):
        name = 'jobbole'
        allowed_domains = ['news.cnblogs.com']
        
        #初始URL已经自动生成,parse方法即从初始URL开始访问
        start_urls = ['http://news.cnblogs.com/']
    
        def parse(self, response):
            pass
    

    接下来,我们开始在parse内部编写爬虫逻辑。

    首先,我们先明确需求:

        def parse(self, response):
            ''' 功能实现: 1.获取新闻列表页中的新闻url交给scrapy进行下载后调用相应的解析方法 2.获取下一页的url交给scrapy进行下载,下载完成后交给parse继续跟进 '''
    

    这里scrapy给我们默认注入好的response,即为自动请求页面URL之后页面的响应数据,接下来我们就根据页面的response开始解析数据。

    首先,我们先获取到news_list节点:

            post_nodes = response.css('#news_list .news_block')
    

    此时post_nodes拿到的就是news_list下的若干个news_block,也就是当前新闻列表页下的每一个新闻的概要内容,如下图:
    请添加图片描述

    拿到这每一个新闻的概要内容之后,我们先通过控制台看看,我们需要从中解析出哪些我们需要的数据:

    请添加图片描述

    我们需要两个数据:

    • 该新闻的详情页的URL,在h2内部的a标签的href中;
    • 该新闻的图片URL,在class为entry_summary的div内部的img标签的src属性中;

    接下来,我们遍历节点,针对每一个新闻概要内容,解析该两部分数据:

            for post_node in post_nodes:
            	#图片的URL
                image_url = post_node.css('.entry_summary a img::attr(src)').extract_first('')
                if image_url.startswith('//'):
                    image_url = 'https:' + image_url
                #新闻详情页的URL
                post_url = post_node.css('h2 a::attr(href)').extract_first('')
    

    但是在解析的过程中我们发现了如下隐藏的问题:

    有的时候,我们解析出来的图片URL是这样的:

    请添加图片描述

    而有的图片,解析出来的URL却是这样的:

    在这里插入图片描述

    也就是:

    • 解析得到的图片URL,有的完整,有的不完整,缺失“https:”

    为了规避该细节造成的后期run程序时的异常报错,我们做一个细节性处理,看上述代码逻辑,即:

    • 如果获取的URL开头是“//”,则手动为其拼接上“https:”

    继续。解析出来得到的新闻详情页的URL,其实也有问题:

    请添加图片描述

    • 我们得到的href的内容,只是详情页URL的路径部分,但前面的协议和主机地址内容却没有;

    所以,这个post_url我们还是得手动拼接上协议和主机地址,详情页的URL才算完整。

    如何拼接呢?直接使用format拼接?

    不行。

    为什么?吸取刚才图片URL的教训,虽然这里大部分我们看到的详情页的URL是不完整的,直接拼接肯定没问题,但你真保不齐哪个URL是完整的,对吗?那如果已经完整了,再去拼接主机地址,那得到的URL肯定就是错误的了。所以,为了灵活拼接,我们借助urllib库下的parse方法:

    url=parse.urljoin(response.url,post_url)
    

    ok,详情页的URL拼接完整之后,我们将URL交给scrapy进行下载,并为其设置回调函数进行页面解析:

                yield Request(url=parse.urljoin(response.url,post_url),meta={ 
         'front_image_url':image_url},callback=self.parse_detail)
    

    至于yield语句内的meta参数:这里参数传递的是图片的URL,这个图片即存在于列表内的新闻概述中,其实也存在于新闻详情页中,所以在这里其实我们不着急先去下载图片,我们可以把它放在详情页解析中去下载,所以暂时先把它传递到详情页解析的回调函数parse_detail中即可。

    至此,parse方法的第一个功能完成了:

    • 获取新闻列表页中的新闻url交给scrapy进行下载后调用相应的解析方法

    现在来完成第二个方法。

    我们还是找到next:
    请添加图片描述

    可以发现,next所处的a标签有很多,且都在class为pager的div内部;

    现在这个next确实找到了,可是我们看看它有什么问题。我们现在进入第100页:

    请添加图片描述

    我们会发现,第100页,就没这个next了,那既然是这样的情况,我可以通过:

    • 抓取class为pager的div下的最后一个a标签

    这种方式来获取这个next吗?显然就不行了,因为第100页是个例外,最后一个a不是next。

    那怎么获取呢?

    只好通过精准匹配咯,即:

    • 先获取class为pager的div下的最后一个a标签的文本;
    • 如果这个文本内容为“Next >”,则获取这个a标签的href属性值

    这样,我们就能确保精准拿到下一页新闻列表页的URL了:

            next_url = response.css('div.pager a:last-child::text').extract_first('')
            if next_url == 'Next >':
                next_url = response.css('div.pager a:last-child::attr(href)').extract_first('')
    

    还是一样的问题,这里拿到的next的URL仍然是不完整的,也需要先拼接成完整的URL:

    url=parse.urljoin(response.url,post_url)
    

    此时,拼接完毕下一页的URL之后,交给scrapy进行下载,下载完毕后交给parse继续解析下一页新闻列表:

                yield Request(url=parse.urljoin(response.url,post_url),meta={ 
         'front_image_url':image_url},callback=self.parse_detail)
    

    至此,新闻列表页的解析,就全部完成了,我先把parse方法的所有代码先展示一下,方便大家直接使用:

        def parse(self, response):
            ''' 功能实现: 1.获取新闻列表页中的新闻url交给scrapy进行下载后调用相应的解析方法 2.获取下一页的url交给scrapy进行下载,下载完成后交给parse继续跟进 '''
    
            # urls = response.css('div#news_list h2 a::attr(href)').extract()
    
            post_nodes = response.css('#news_list .news_block')
            for post_node in post_nodes:
                image_url = post_node.css('.entry_summary a img::attr(src)').extract_first('')
                if image_url.startswith('//'):
                    image_url = 'https:' + image_url
                post_url = post_node.css('h2 a::attr(href)').extract_first('')
                yield Request(url=parse.urljoin(response.url,post_url),meta={ 
         'front_image_url':image_url},callback=self.parse_detail)
    
            #提取下一页交给scrapy进行下载
            next_url = response.css('div.pager a:last-child::text').extract_first('')
    
            # next_url = response.xpath('a[contains(text(),"Next >")]/@href').extract_first('')
            # yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
    
            if next_url == 'Next >':
                next_url = response.css('div.pager a:last-child::attr(href)').extract_first('')
                yield Request(url=parse.urljoin(response.url, next_url),callback=self.parse)
    
  • 详情页数据解析及写入items

    还是先回看下parse方法。

    在parse方法中,我们两次yield出数据:

    • 第二次的yield不用管了,因为第二次是将解析得到的下一页新闻列表的URL提交给scrapy下载并再次执行列表页解析;
    • 第一次,我们将详情页URL提交给了scrapy进行下载,并调用parse_detail方法进行解析;

    这个方法还没有,所以我们先定义好:

        def parse_detail(self,response):#定义时一定要默认注入好response
    		pass
    

    接下来,我们就开始在parse_detail方法中,开始解析我们所需要的数据。

    我们先看下详情页的页面结构:

    请添加图片描述

    经过测试,详情页的源码也是直接响应出来的,所以数据也是真实存在。

    在正式开始解析之前,先给大家讲一个测试代码的小技巧:

    毕竟我们是在爬一个网站的数据,对吧,那每一次写完代码我们总归要测试一下看看是否正确,那如果每写一点代码,为了测试,我都去请求一下这个网站,那请求次数多了,难免触发网站的反爬措施,这样的话很有可能最后我们完全实现功能真的要开始爬数据的时候,网站可能已经不能爬了。所以为了避免这个问题,我们可以使用scrapy给我们提供的scrapy shell,来方便的测试代码。

    如何使用呢?简单。打开命令行,输入:

    scrapy shell 具体的详情页URL
    

    如下:

    (SpiderEnvs_space) C:\Users\anwc>scrapy shell https://news.cnblogs.com/n/687723/
    2021-02-04 16:09:25 [scrapy.utils.log] INFO: Scrapy 2.4.1 started (bot: scrapybot)
    2021-02-04 16:09:25 [scrapy.utils.log] INFO: Versions: lxml 4.6.2.0, libxml2 2.9.5, cssselect 1.1.0, parsel 1.6.0, w3lib 1.22.0, Twisted 20.3.0, Python 3.9.1 (tags/v3.9.1:1e5d33e, Dec  7 2020, 17:08:21) [MSC v.1927 64 bit (AMD64)], pyOpenSSL 20.0.1 (OpenSSL 1.1.1i  8 Dec 2020), cryptography 3.3.1, Platform Windows-10-10.0.18362-SP0
    2021-02-04 16:09:25 [scrapy.utils.log] DEBUG: Using reactor: twisted.internet.selectreactor.SelectReactor
    2021-02-04 16:09:25 [scrapy.crawler] INFO: Overridden settings:
    { 
    'DUPEFILTER_CLASS': 'scrapy.dupefilters.BaseDupeFilter',
    'LOGSTATS_INTERVAL': 0}
    2021-02-04 16:09:25 [scrapy.extensions.telnet] INFO: Telnet Password: 0681311bf88802b3
    2021-02-04 16:09:25 [scrapy.middleware] INFO: Enabled extensions:
    ['scrapy.extensions.corestats.CoreStats',
    'scrapy.extensions.telnet.TelnetConsole']
    2021-02-04 16:09:26 [scrapy.middleware] INFO: Enabled downloader middlewares:
    ['scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware',
    'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware',
    'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware',
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware',
    'scrapy.downloadermiddlewares.retry.RetryMiddleware',
    'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware',
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware',
    'scrapy.downloadermiddlewares.redirect.RedirectMiddleware',
    'scrapy.downloadermiddlewares.cookies.CookiesMiddleware',
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware',
    'scrapy.downloadermiddlewares.stats.DownloaderStats']
    2021-02-04 16:09:26 [scrapy.middleware] INFO: Enabled spider middlewares:
    ['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
    'scrapy.spidermiddlewares.offsite.OffsiteMiddleware',
    'scrapy.spidermiddlewares.referer.RefererMiddleware',
    'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware',
    'scrapy.spidermiddlewares.depth.DepthMiddleware']
    2021-02-04 16:09:26 [scrapy.middleware] INFO: Enabled item pipelines:
    []
    2021-02-04 16:09:26 [scrapy.extensions.telnet] INFO: Telnet console listening on 127.0.0.1:6023
    2021-02-04 16:09:26 [scrapy.core.engine] INFO: Spider opened
    2021-02-04 16:09:26 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://news.cnblogs.com/n/687723/> (referer: None)
    [s] Available Scrapy objects:
    [s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
    [s]   crawler    <scrapy.crawler.Crawler object at 0x000002210240F310>
    [s]   item       { 
    }
    [s]   request    <GET https://news.cnblogs.com/n/687723/>
    [s]   response   <200 https://news.cnblogs.com/n/687723/>
    [s]   settings   <scrapy.settings.Settings object at 0x000002210240C9D0>
    [s]   spider     <DefaultSpider 'default' at 0x2210274edf0>
    [s] Useful shortcuts:
    [s]   fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)
    [s]   fetch(req)                  Fetch a scrapy.Request and update local objects
    [s]   shelp()           Shell help (print this help)
    [s]   view(response)    View response in a browser
    >>>
    

    可以看到,一次请求后,scrapy shell返回了相应的对象,包含item啊,request啊什么的,我们只需要关注这个response就行,这里这个response,其实就是我们注入parse_detail的response,我们就可以直接使用相应的解析语句进行测试了,比如:

    我们先解析标题:

    >>> response.css('#news_title a::text').extract_first('')
    '网约车上播放自制短剧,滴滴进军娱乐圈?'
    >>>
    

    ok标题获取成功,那么这个语句我就可以直接复制粘贴到代码里了:

                title = response.css('#news_title a::text').extract_first('')
    

    继续,我们解析下新闻的发布时间:

                create_date = response.css('#news_info .time::text').extract_first('')
    

    我们先看下解析出来的时间格式:

    >>> response.css('#news_info .time::text').extract_first('')
    '发布于 2021-02-04 14:38'
    >>>
    

    也就是我们拿到的时间格式是一个字符串加后面的日期格式。但这个时间我们只想要后面的时间格式的数字,而不想要前面的字符串,那么我们需要对其进行再一次的正则匹配,取出想要的日期格式就行了:

                create_date = response.css('#news_info .time::text').extract_first('')
    match_res = re.match('.*?(\d+.*)',create_date)
    if match_res:
    create_date = match_res.group(1)
    

    继续,该解析页面的详情内容了。但此时又来了一个问题:

    • 新闻的详情内容,即包含文字,又包含图片,甚至可能包含音视频,但其实大部分内容都是文本;

    那如果我以获取文本的方式来解析详情内容,那详情中包含的图片,音视频等外部资源,可能就遗漏了,因为这些资源在解析文本时是没办法解析出来的。那怎么办呢?我们可以先暂时把整个详情内容的html全解析出来,至于其中的图片,音视频等外部资源,啥时候需要,我们再从数据库中读取出详情内容,进行二次解析就行了么,这一步,我们没必要把这些外部资源都解析出来呀,对吧。

    这样一分析,解析详情内容就简单了,直接获取全部的html不就行了么:

                content = response.css('#news_content').extract()[0]
    

    继续,接下来获取标签:

    >>> response.css('.news_tags a::text').extract()
    ['滴滴']
    >>>
    

    ok数据是能拿到,但是,我们看,返回的结果是一个list,而等会这些数据我们都是要入库的,但MySQL好像不支持list类型的数据吧?所以没办法,还是得给它转成字符串。

    但还有个小问题,这里我们测试的这个新闻,它只有一个标签,但保不齐别的新闻会有很多标签啊,那多个标签该如何一次入库呢?那就转字符串的时候拼接一下嘛:

                tags = ','.join(response.css('.news_tags a::text').extract())
    

    继续,我们提取新闻详情页中也包含的新闻概述内容中存在的那个图片,即通过parse第一次yield时提交的meta参数中的图片URL。

    可是此时仍然有几个小问题:

    • 图片我们是交给scrapy进行下载的,而scrapy需要的是一个URL队列,所以我得把取到的图片URL放入list结构内部;
    • 并不是每一个新闻,其概述内容中都有图片的吧?所以当没有图片时,这个异常情况仍然需要处理一下;

    经过上述分析,提取图片并做异常处理:

                #如果存在图片,即能取到meta参数传递过来的图片URL
    if response.meta.get('front_image_url',[]):
    front_image_url = [response.meta.get('front_image_url','')]
    #否则,如果该条新闻概述内没有图片,则给个空队列就行了
    else:
    front_image_url = []
    

    继续,接下来解析评论数:

    请添加图片描述

    >>> response.css('span.comment a::text').extract()
    []
    >>>
    

    emmmm?怎么为空?明明控制台能看到这个评论数啊?

    ahhhhh…可能动态加载了吧?

    先看下源码:

    请添加图片描述

    源码确实没有…

    那么经过测试,阅读数以及推荐数,在源码中也没有。

    所以,这三个数据,应该是异步加载了。

    没关系,我们现在查看下XHR数据包。经过筛选,检索,我们定位了这样的一个数据包:

    请添加图片描述

    其响应数据:

    请添加图片描述

    确实包含所需的三个数据。

    找到了就好,现在就是想办法获取这三个数据。

    但,问题来了。

    这三个数据,得去请求一个XHR接口才能获取,也就是说,这三个数据得单独下载,而我们前面解析出来的数据,在这一步之前,到底是yield还是不yield呢?

    最好还是把所有数据全部都解析到之后再yield到item吧。

    可是已经解析出了好多数据了,此时不yield,等到下载完毕三个异步加载的数据之后再yield,那就需要把已经解析得到的数据传递给异步数据下载方法,等待最后三个数据下载完毕后再统一yield至item,那这些已经解析完毕的数据怎么传递至异步数据下载方法啊?

    所以此时,我们先把已有的这部分数据先写入item吧,然后把item传递到异步数据下载方法,等最后三个数据也获取完了,并且也写入至item了,再最后统一全部yield至item吧,只能这样了。

    ok,思路明确之后,现在我们先将已解析的部分数据写入item。

    先把刚才我们定义好的item类导入并实例化:

    from demo01_jobbole.items import JobBoleArticleItem
    

    再在parse_detail中实例化item类:

                article_item = JobBoleArticleItem()
    

    然后,将我们刚才已经解析出的数据先入item:

                article_item['title'] = title
    article_item['create_date'] = create_date
    article_item['content'] = content
    article_item['tags'] = tags
    article_item['url'] = response.url
    if response.meta.get('front_image_url',[]):
    article_item['front_image_url'] = [response.meta.get('front_image_url','')]
    else:
    article_item['front_image_url'] = []
    

    现在,我们先准备好异步数据下载方法:

        def parse_nums(self,response):
    pass
    

    此时,我们就可以开始下载异步数据了。

    接下来我们分析下这个XHR接口的请求URL:

    https://news.cnblogs.com/NewsAjax/GetAjaxNewsInfo?contentId=687723
    

    前半部分:https://news.cnblogs.com/,其实包含在response.url中;

    后半部分直至:NewsAjax/GetAjaxNewsInfo?contentId=,这部分我们通过字符串写入;

    可关键是这个contentId,它是根据详情页随时变化的,这个ID在哪呢?也存在于response.url中。

    所以,我们可以先使用正则,从详情页的URL中解析出这个content_id,再拼接至‘NewsAjax/GetAjaxNewsInfo?contentId=’中,最后再灵活拼接上前面的协议和主机地址即可:

    match_re = re.match('.*?(\d+)',response.url)
    post_id = match_re.group(1)
    url=parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(post_id))
    

    XHR数据包的URL已经拼接完毕,下载异步数据我们需要将URL提供给scrapy进行下载并绑定回调函数进行数据解析,同时前面说到了,已经写入部分数据的item也要传递到parse_nums方法:

                yield Request(url=parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(post_id)), meta={ 
    'article_item':article_item},callback=self.parse_nums)
    

    ok,至此,parse_detail的职责完成:

    • 解析详情页的部分数据;
    • 写入item;
    • 将异步数据的URL提交给scrapy下载并将已有的item传递至回调函数;

    现在放上parse_detail的全部代码:

        def parse_detail(self,response):
    ''' 详情页面解析方法 '''
    match_re = re.match('.*?(\d+)',response.url)
    #这里为什么是这个逻辑呢?因为为了代码的逻辑严谨性,其实如果解析出的详情页URL没有contentid的话,整个页面的解析其实也会出错,既然是这样的情况,不如将contentid的解析放在逻辑判断内,如果能解析出详情页的contentid,则代表代码没有异常。
    if match_re:
    article_item = JobBoleArticleItem()
    title = response.css('#news_title a::text').extract_first('')
    create_date = response.css('#news_info .time::text').extract_first('')
    match_res = re.match('.*?(\d+.*)',create_date)
    if match_res:
    create_date = match_res.group(1)
    content = response.css('#news_content').extract()[0]
    # tag_list = response.css('.news_tags a::text').extract()
    tags = ','.join(response.css('.news_tags a::text').extract())
    post_id = match_re.group(1)
    # html = requests.get(url=parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(post_id)))
    # j_data = json.loads(html.text)
    article_item['title'] = title
    article_item['create_date'] = create_date
    article_item['content'] = content
    article_item['tags'] = tags
    article_item['url'] = response.url
    if response.meta.get('front_image_url',[]):
    article_item['front_image_url'] = [response.meta.get('front_image_url','')]
    else:
    article_item['front_image_url'] = []
    yield Request(url=parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(post_id)), meta={ 
    'article_item':article_item},callback=self.parse_nums)
    

    现在我们进入异步数据下载方法,实现异步数据的解析逻辑。

    通过刚才的分析我们知道,评论数,阅读数,推荐数这三个数据是通过异步加载的,且该接口响应的数据为json格式,那么我们就可以通过解析json数据的方式,方便的拿到这三个数据,并且将其写入item:

        def parse_nums(self,response):
    j_data = json.loads(response.text)
    #将传递过来的item取出,方便后续写入三个数据
    article_item = response.meta.get('article_item','')
    praise_nums = j_data['DiggCount']
    fav_nums = j_data['TotalView']
    comment_nums = j_data['CommentCount']
    article_item['praise_nums'] = praise_nums
    article_item['fav_nums'] = fav_nums
    article_item['comment_nums'] = comment_nums
    

    至此,前期分析的item应有的数据,我们都解析完毕并且写入至item了。

    但其实,我们还缺少一个数据。

    回看我们之前的代码,针对我们获取的每一个详情页的URL,其实不难发现,这些URL的长度都是不定的。往往我们在进行数据爬取时,对于请求的URL,为了实现方便管理,我们需要将其变为定长的格式,这里我们就需要使用到md5加密了。

    那具体怎么实现呢?思路其实很简单:

    • 定义一个md5加密算法,将传入的URL加密为定长格式;
    • 针对加密后定长的URL,将其写入item,作为需要解析的最后一项数据;

    接下来我们实现这个功能。

    在spider目录的同级目录下,新建一个目录:untils,在其中创建一个模块:common.py;在该模块内部实现加密算法:

    # -*- coding:utf-8 _*-
    """ @version: author:weichao_an @time: 2021/02/03 @file: common.py @environment:virtualenv @email:awc19930818@outlook.com @github:https://github.com/La0bALanG @requirement: """
    import hashlib
    def get_md5(url):
    if isinstance(url,str):
    url = url.encode('utf-8')
    m = hashlib.md5()
    m.update(url)
    return m.hexdigest()
    if __name__ == '__main__':
    print(get_md5('https://www.baidu.com'))
    

    回到我们的spider文件,先导入该算法:

    from demo01_jobbole.utils import common
    

    再将详情页URL传入,加密后写入item:

            article_item['url_object_id'] = common.get_md5(article_item['url'])
    

    至此,item所需数据全部解析完成,将其yield回items,后续即将开始数据的持久化操作。

    现放上parse_nums的所有代码:

        def parse_nums(self,response):
    j_data = json.loads(response.text)
    article_item = response.meta.get('article_item','')
    praise_nums = j_data['DiggCount']
    fav_nums = j_data['TotalView']
    comment_nums = j_data['CommentCount']
    article_item['praise_nums'] = praise_nums
    article_item['fav_nums'] = fav_nums
    article_item['comment_nums'] = comment_nums
    article_item['url_object_id'] = common.get_md5(article_item['url'])
    yield article_item
    
  • 数据入管道,进行入库等持久化操作

    • 对于图片的持久化-保存为本地文件

      pipelines.py
      class ArticleImagePipeline(ImagesPipeline):
      def item_completed(self, results, item, info):
      if 'front_image_url' in item:
      image_file_path = ''
      for ok,value in results:
      image_file_path = value['path']
      item['front_image_path'] = image_file_path
      return item
      
    • 解析得到的数据写入json文件-保存为本地json文件

      pipelines.py
      class JsonWithEncodingPipline(object):
      ''' 自定义json文件的导出 '''
      def __init__(self):
      self.file = codecs.open('article.json','a',encoding='utf-8')
      def process_item(self,item,spider):
      lines = json.dumps(dict(item),ensure_ascii=False) + '\n'
      self.file.write(lines)
      return item
      def spider_closed(self,spider):
      self.file.close()
      
    • 数据库建库-准备将数据写入MySQL

      create table jobbole_article
      (
      title            varchar(255)  not null,
      url              varchar(500)  not null,
      url_object_id    varchar(50)   not null
      primary key,
      front_image_path varchar(200)  null,
      front_image_url  varchar(500)  null,
      paise_nums       int default 0 not null,
      comment_nums     int           null,
      fav_nums         int           null,
      tags             varchar(255)  null,
      content          longtext      null,
      create_date      datetime      null
      );
      
    • 同步方式将数据写入MySQL

      pipelines.py
      class MysqlPipeline(object):
      def __init__(self):
      self.conn = MySQLdb.connect('127.0.0.1','root','185268','article_spider',charset='utf8',use_unicode=True)
      self.cursor = self.conn.cursor()
      def process_item(self,item,spider):
      insert_sql = """ insert into jobbole_article (title,url,url_object_id,front_image_path,front_image_url,paise_nums,comment_nums,fav_nums,tags,content,create_date) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE comment_nums=VALUES(comment_nums) """
      parmas = list()
      parmas.append(item.get('title',''))
      parmas.append(item.get('url',''))
      parmas.append(item.get('url_object_id',''))
      parmas.append(item.get('front_image_path',''))
      front_image = ','.join(item.get('front_image_url',[]))
      parmas.append(front_image)
      parmas.append(item.get('paise_nums',0))
      parmas.append(item.get('comment_nums',0))
      parmas.append(item.get('fav_nums',0))
      parmas.append(item.get('tags',''))
      parmas.append(item.get('content',''))
      parmas.append(item.get('create_date','1970-07-01'))
      self.cursor.execute(insert_sql,tuple(parmas))
      self.conn.commit()
      return item
      
    • 异步方式将数据写入MySQL

      pipelines.py
      class MysqlTwistedPipeline(object):
      def __init__(self,dbpool):
      self.dbpool = dbpool
      @classmethod
      def from_settings(cls,settings):
      dbparms = dict(
      host = settings['MYSQL_HOST'],
      db = settings['MYSQL_DBNAME'],
      user = settings['MYSQL_USER'],
      passwd = settings['MYSQL_PASSWORD'],
      charset = 'utf8',
      cursorclass = DictCursor,
      use_unicode = True
      )
      dbpool = adbapi.ConnectionPool('MySQLdb',**dbparms)
      return cls(dbpool)
      def process_item(self,item,spider):
      query = self.dbpool.runInteraction(self.do_insert,item)
      query.addErrback(self.handle_error,item,spider)
      return item
      def handle_error(self,failure,item,spider):
      print(failure)
      def do_insert(self,cursor,item):
      insert_sql = """ insert into jobbole_article (title,url,url_object_id,front_image_path,front_image_url,paise_nums,comment_nums,fav_nums,tags,content,create_date) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE comment_nums=VALUES(comment_nums) """
      parmas = list()
      parmas.append(item.get('title', ''))
      parmas.append(item.get('url', ''))
      parmas.append(item.get('url_object_id', ''))
      parmas.append(item.get('front_image_path', ''))
      front_image = ','.join(item.get('front_image_url', []))
      parmas.append(front_image)
      parmas.append(item.get('paise_nums', 0))
      parmas.append(item.get('comment_nums', 0))
      parmas.append(item.get('fav_nums', 0))
      parmas.append(item.get('tags', ''))
      parmas.append(item.get('content', ''))
      parmas.append(item.get('create_date', '1970-07-01'))
      cursor.execute(insert_sql,tuple(parmas))
      
    • 管道配置至settings

      settings.py
      ITEM_PIPELINES = { 
      
      'demo01_jobbole.pipelines.ArticleImagePipeline':1,
      'demo01_jobbole.pipelines.JsonWithEncodingPipline':2,
      'demo01_jobbole.pipelines.MysqlTwistedPipeline': 3,
      'demo01_jobbole.pipelines.Demo01JobbolePipeline': 300
      }
      

    至此,本爬虫案例所有编码全部完成。

  • 测试结果

    • json文件(数据量较大,只展示一条数据)

      { 
      "title": "微信红包封面背后的生意经", "create_date": "2021-02-03 14:00", "content": "<div id=\"news_content\">\n <div id=\"news_body\">\n \n<p>  12 月 9 号,微信开放了视频号的自制红包封面权限,从此定制红包封面不再是大型企业或机构的专利。从微信的角度看,开放视频号的红包封面制作权限可以吸引更多用户使用视频号,从而壮大自己的视频号生态。尤其农历新年即将到来,谁又不想在过年「红包大战」时在亲朋好友面前秀出「极具个性」的红包封面呢?</p>\r\n<p style=\"text-align: center;\"><img title=\"\" src=\"//img2020.cnblogs.com/news/66372/202102/66372-20210203141942833-1016059283.png\" alt=\"\"></p>\r\n<p>  不过事情的发展似乎超出了微信的预料,尽管在微信红包封面的微信指数在十二月底与农历新年前屡创新高,在 2 月 1 日更是超过了 1.3 亿万,日环比高达 216.54%,但视频号的微信指数除了 12 月 28 日突发破亿以外,其余时间都异常平静,停留在 800 万上下。</p>\r\n<p>  那问题来了,按照微信的逻辑,红包封面应该是和视频号相互关联的两个热点,即使考虑到机构红包等因素,红包封面的微信指数也不应该领先视频号这么多。这些「多出来的」的红包封面,究竟从何而来?</p>\r\n<p>  很简单,用户花钱买来的。</p>\r\n<p>  <strong>微信红包,里面外面都是生意</strong></p>\r\n<p>  其实早在 12 月 9 号微信红包封面开放视频号制作之前,网上就有出售,或在特定条件下「免费发放」红包封面的情况。按照最初的设定,「红包封面」应该是各企业买给自己员工的福利,或是向公众派发的广告赠品。毕竟这些红包封面上都有着大大的品牌标志,用户每次发红包都等于给品牌做广告。</p>\r\n<p>  微信的红包封面已经成为大众茶余饭后的谈资之一,抢夺数量稀少的红包,甚至比抢购热门数码产品、奢侈品更加困难。日前,微信派官方表示,将会在春节周期内发放超过 5000 万的红包封面,但根据此前红包封面被一扫而空的情况来看,这 5000 万个红包封面似乎也是杯水车薪。</p>\r\n<p style=\"text-align: center;\"><img title=\"\" src=\"//img2020.cnblogs.com/news/66372/202102/66372-20210203141943072-960441720.png\" alt=\"\"></p>\r\n<p>  <strong>但正所谓「物以稀为贵」,限量派发的品牌红包很快就成为大家互相「炫耀」的资本,之后更是成为了少数人的「商品」。比如 LV、Gucci、Supreme 等品牌的红包封面,在闲鱼与「红包贩子」手中就一直是畅销商品——一个免费领取的红包序列码,甚至可以在闲鱼上转手以 30 元出售,可谓是「无本万利」。</strong></p>\r\n<p>  而 12 月 9 号的开放的个人红包封面,则是将各「商机」彻底点燃——随着红包封面制作成本的直线下降(10 元/个降低至 1 元/个),一条关于红包封面的成熟「产业链」也浮出了水面。</p>\r\n<p>  <strong>简单来说,一条红包封面的「产业链」从源头的「图源」到末端的「消费者」,大致可以分为「图源」「制作」「分销」「二级分销」「消费者」这五个角色。</strong>其中「图源」顾名思义就是提供红包封面原始素材的画师或摄影师,但就像大家想的那样,由于国内没人注重的版权规定,市面上流通的绝大多数红包封面都「省略」的图片采购这个步骤,基本上都是制作者在互联网上随便找来的图。</p>\r\n<p>  <strong>有了图源,接下来就是「制作」,这也是整条产业链中存在不可避免固定成本的环节——根据微信红包封面页面的指引,每提交一个红包封面,微信将收取一元的费用。尽管这个「硬性成本」不可避免,但在高达 500% 的利润率面前,这点费用还真算不上什么。</strong></p>\r\n<p>  审核通过意味着红包封面进入到「分销」环节。「分销商」会从制作者手中大量采购序列号,或者干脆自己一手包办制作与分销两个环节,以将利润空间掌握在自己手里。<strong>不同于各位在微信朋友圈或微博上见到的「二级分销」,这里的「一级」分销商无论是规模还是利润空间都大得多,部分</strong><strong>分</strong><strong>销商的利润甚至足以支撑他们为此专门建立一个「线上商城」。</strong></p>\r\n<p style=\"text-align: center;\"><img title=\"\" src=\"//img2020.cnblogs.com/news/66372/202102/66372-20210203141943077-1530096933.jpg\" alt=\"\"></p>\r\n<p>  以某红包封面商城为例,尽管这只是一个基于快速建站模板的小网站,交互界面也非常简陋,但在简陋的背后分销商已经打通了一条完整的支付路径:<strong>不支持微信支付以避开微信监管,使用其他类目(水果店)的支付宝收款端口掩人耳目,通过 QQ 发放序列号再次绕开微信监管。这套成熟的分销体系将分销商与终端消费者隔离开来,让经销商在赚取高额利润的同时「卸下」自己的风险。</strong></p>\r\n<p style=\"text-align: center;\"><img title=\"\" src=\"//img2020.cnblogs.com/news/66372/202102/66372-20210203141943077-1588516174.jpg\" alt=\"\"></p>\r\n<p>  <strong>赚的就是信息差</strong></p>\r\n<p>  刚刚说过,我们在微信朋友圈或微博、闲鱼等平台见到的一般都只是「二级分销商」,或者说「二道贩子」。<strong>他们不参与红包制作与采购,只对终端消费者「负责」,但这并不意味着他们技术含量低——在我看来,这些「二级分销商」可以说极具互联网思维:他们的利润,完全建立在「信息差」这一概念上。</strong></p>\r\n<p>  回到刚才说的商城,这其实也是二级分销「拿货」的地方。尽管网站的右下角有登录的功能,但整个购买流程其实完全不做任何要求;除此之外,网站也没有最低购买数量的限制,也就是不存在批发与零售的区别。</p>\r\n<p style=\"text-align: center;\"><img title=\"\" src=\"//img2020.cnblogs.com/news/66372/202102/66372-20210203141942826-1022245698.jpg\" alt=\"\"></p>\r\n<p>  <strong>换句话说,二级分销商既不需要「交钱进群」,也不需要「垫资进货」,他们只要把商城里的图片下载下来,再加价卖给终端消费者就够了。</strong>当然,他们也要承担一定的「风险」:因同行举报或版权原因,已经生成的红包封面后可能会「失效」,此时终端买家第一个找上的肯定就是二级分销。</p>\r\n<p>  <strong>但刚刚说过,二级分销是一个完全基于信息差的,赚多赚少只看胆子的「无本万利」行业,在足够高的利润率面,即使给买家「全额退款」又能有什么损失呢?</strong></p>\r\n<p>  当然了,除了这条成熟的产业链外,「私域流量」概念的兴起也催生出另一条「致富道路」:部分公众号会以「免费红包封面」为噱头要求买家加微信并完成某些「推广任务」。对于账号运营者来说,这其实就是在用红包封面「买人头」。</p>\r\n<p>  <strong>千篇一律的「个性化」</strong></p>\r\n<p>  <strong>看到这里,可能有人会觉得奇怪?不就是一个微信红包的封面吗?连红包都不是,究竟有什么吸引人的地方?在我看来,红包封面的流行,本质上是封闭环境内群众个性化需求的体现。</strong></p>\r\n<p>  最初在接触这个选题时,由于刻板的固有印象,我以为红包封面的用户画像应该是中年群体——<strong>他们有发红包的刚性需求,同时对互联网也相对陌生,不能自制红包封面,而且「花钱买红包」这个逻辑在过去十年里也是他们日常生活的一部分,不需要重新建立消费习惯。</strong></p>\r\n<p>  但没想到的是,在与一位「兼职封面卖家」的交流中得知,<strong>红包封面的消费主力更多的是年轻群体,生肖与猫相关的红包封面也占据销量榜首。</strong>发红包对年轻群体来说并不是什么「硬性任务」,猫相比过年红包,也更像是个人喜好的展现。<strong>也就是说,这些「高价」买来的封面,其实就像 QQ 的气泡、微博的卡片、视频网站的头像框一样,是一个基于「个性化」需求创造出的市场空缺。</strong></p>\r\n<p style=\"text-align: center;\"><img title=\"\" src=\"//img2020.cnblogs.com/news/66372/202102/66372-20210203141943076-1009299782.jpg\" alt=\"\"></p>\r\n<p>  但问题是,从别人手中买来的「个性」,还算得上「个性」吗?你能买,别人也能买,这种流水线批量「生产」的虚拟商品,真的是追求个性的年轻人彰显个人品味的正确方法吗?延伸开来,那些在广东、浙江甚至是东南亚血汗工厂批量生产的「成衣」潮牌,又算得上是个性的表现吗?</p>\r\n<p>  这个问题我无法回答,但我能肯定的是,红包封面的「产业链」不仅不会随着新年假期的结束而消失,还将继续「壮大」下去,让更多人的账户余额实现「10W+」,而在红包封面的「盛世」中,视频号将是唯一的「输家」。</p> </div><!--end: news_body -->\n <div id=\"news_otherinfo\">\n <div id=\"up_down\">\n <div class=\"diggit\" οnclick=\"VoteNews(687629,'agree')\">\n <span class=\"diggnum\" id=\"digg_num_687629\"></span>\n </div>\n <div class=\"buryit\" οnclick=\"VoteNews(687629,'anti')\">\n <span class=\"burynum\" id=\"bury_num_687629\"></span>\n </div>\n <div class=\"clear\"></div>\n <div id=\"digg_tip_687629\" class=\"digg_tip_detail\"> </div>\n </div>\n <div id=\"come_from\">\n 来自:\n 雷科技网 </div><!--end: come_from -->\n <div class=\"clear\"></div>\n <div id=\"article_A4area\">\n <span id=\"shareA4\" class=\"fl\">\n <a href=\"https://brands.cnblogs.com/aws/register\" target=\"_blank\"><b>注册AWS账号,立享12个月免费套餐</b></a>\n </span>\n <span id=\"sharebox\">\n <a οnclick=\"PutInWz();return false;\" href=\"javascript:void(0);\">\n <img border=\"0\" title=\"收藏至网摘\" src=\"/Images/icon_wz.png\" alt=\"收藏\">\n </a>\n <a rel=\"nofollow\" οnclick=\"ShareToTsina();return false;\" href=\"javascript:void(0)\">\n <img border=\"0\" title=\"转发至新浪微博\" src=\"/Images/icon_sina.gif\" alt=\"新浪微博\">\n </a>\n <a rel=\"nofollow\" οnclick=\"ShareToTweixin(687629);return false;\" href=\"javascript:void(0)\">\n <img border=\"0\" title=\"分享至微信\" src=\"/Images/icon_weixin.gif\" alt=\"分享至微信\">\n </a>\n </span>\n <div class=\"clear\">\n </div>\n </div><!--end: share block-->\n <div class=\"clear\"></div>\n <div id=\"e4\" style=\"height:60px; width:468px;\">\n <div id=\"div-gpt-ad-1533633736227-3\" class=\"e4-dfp\" style=\"height:60px; width:468px;\"></div>\n </div>\n <div id=\"news_more_info\">\n <div class=\"news_tags\">标签: <a href=\"/n/tag/%E5%BE%AE%E4%BF%A1/\" class=\"catalink\">微信</a></div>\n <input type=\"hidden\" name=\"tagsId\" id=\"tagsId\" value=\"微信\">\n </div>\n </div><!--end: news_otherinfo -->\n </div>", "tags": "微信", "url": "https://news.cnblogs.com/n/687629/", "front_image_url": ["https://img2018.cnblogs.com/news_topic/20191011113837921-1266267629.png"], "praise_nums": 0, "fav_nums": 287, "comment_nums": 0, "url_object_id": "9b1094ca99579b94af75266bc8fa3907", "front_image_path": "full/0b51367f1c0e48bea4ab14f6512739300db9cf3f.jpg"}
      
    • MySQL写入:

      请添加图片描述

    • 图片下载:

      请添加图片描述

  • 本案例所有源码

    • settings.py

      
      import os
      import sys
      # Scrapy settings for demo01_jobbole project
      #
      # For simplicity, this file contains only settings considered important or
      # commonly used. You can find more settings consulting the documentation:
      #
      # https://docs.scrapy.org/en/latest/topics/settings.html
      # https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
      # https://docs.scrapy.org/en/latest/topics/spider-middleware.html
      BOT_NAME = 'demo01_jobbole'
      SPIDER_MODULES = ['demo01_jobbole.spiders']
      NEWSPIDER_MODULE = 'demo01_jobbole.spiders'
      # Crawl responsibly by identifying yourself (and your website) on the user-agent
      #USER_AGENT = 'demo01_jobbole (+http://www.yourdomain.com)'
      # Obey robots.txt rules
      ROBOTSTXT_OBEY = False
      # Configure maximum concurrent requests performed by Scrapy (default: 16)
      #CONCURRENT_REQUESTS = 32
      # Configure a delay for requests for the same website (default: 0)
      # See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
      # See also autothrottle settings and docs
      #DOWNLOAD_DELAY = 3
      # The download delay setting will honor only one of:
      #CONCURRENT_REQUESTS_PER_DOMAIN = 16
      #CONCURRENT_REQUESTS_PER_IP = 16
      # Disable cookies (enabled by default)
      #COOKIES_ENABLED = False
      # Disable Telnet Console (enabled by default)
      #TELNETCONSOLE_ENABLED = False
      # Override the default request headers:
      #DEFAULT_REQUEST_HEADERS = { 
      
      # 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
      # 'Accept-Language': 'en',
      #}
      # Enable or disable spider middlewares
      # See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
      #SPIDER_MIDDLEWARES = { 
      
      # 'demo01_jobbole.middlewares.Demo01JobboleSpiderMiddleware': 543,
      #}
      # Enable or disable downloader middlewares
      # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
      #DOWNLOADER_MIDDLEWARES = { 
      
      # 'demo01_jobbole.middlewares.Demo01JobboleDownloaderMiddleware': 543,
      #}
      # Enable or disable extensions
      # See https://docs.scrapy.org/en/latest/topics/extensions.html
      #EXTENSIONS = { 
      
      # 'scrapy.extensions.telnet.TelnetConsole': None,
      #}
      # Configure item pipelines
      # See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
      ITEM_PIPELINES = { 
      
      'demo01_jobbole.pipelines.ArticleImagePipeline':1,
      'demo01_jobbole.pipelines.JsonWithEncodingPipline':2,
      'demo01_jobbole.pipelines.MysqlTwistedPipeline': 3,
      'demo01_jobbole.pipelines.Demo01JobbolePipeline': 300
      }
      # Enable and configure the AutoThrottle extension (disabled by default)
      # See https://docs.scrapy.org/en/latest/topics/autothrottle.html
      #AUTOTHROTTLE_ENABLED = True
      # The initial download delay
      #AUTOTHROTTLE_START_DELAY = 5
      # The maximum download delay to be set in case of high latencies
      #AUTOTHROTTLE_MAX_DELAY = 60
      # The average number of requests Scrapy should be sending in parallel to
      # each remote server
      #AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
      # Enable showing throttling stats for every response received:
      #AUTOTHROTTLE_DEBUG = False
      # Enable and configure HTTP caching (disabled by default)
      # See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
      #HTTPCACHE_ENABLED = True
      #HTTPCACHE_EXPIRATION_SECS = 0
      #HTTPCACHE_DIR = 'httpcache'
      #HTTPCACHE_IGNORE_HTTP_CODES = []
      #HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
      #
      # print(os.path.dirname(os.path.abspath(__file__)))
      IMAGES_URLS_FIELD = 'front_image_url'
      projects_dir = os.path.dirname(os.path.abspath(__file__))
      IMAGES_STORE = os.path.join(projects_dir,'images')
      MYSQL_HOST = '127.0.0.1'
      MYSQL_DBNAME = 'article_spider'
      MYSQL_USER = 'root'
      MYSQL_PASSWORD = '185268'
      
    • jobbole.py

      import re
      import json
      from urllib import parse
      import scrapy
      import requests
      from scrapy import Request
      from demo01_jobbole.items import JobBoleArticleItem
      from demo01_jobbole.utils import common
      class JobboleSpider(scrapy.Spider):
      name = 'jobbole'
      allowed_domains = ['news.cnblogs.com']
      start_urls = ['http://news.cnblogs.com/']
      def parse(self, response):
      ''' 功能实现: 1.获取新闻列表页中的新闻url交给scrapy进行下载后调用相应的解析方法 2.获取下一页的url交给scrapy进行下载,下载完成后交给parse继续跟进 '''
      # urls = response.css('div#news_list h2 a::attr(href)').extract()
      post_nodes = response.css('#news_list .news_block')
      for post_node in post_nodes:
      image_url = post_node.css('.entry_summary a img::attr(src)').extract_first('')
      if image_url.startswith('//'):
      image_url = 'https:' + image_url
      post_url = post_node.css('h2 a::attr(href)').extract_first('')
      yield Request(url=parse.urljoin(response.url,post_url),meta={ 
      'front_image_url':image_url},callback=self.parse_detail)
      #提取下一页交给scrapy进行下载
      next_url = response.css('div.pager a:last-child::text').extract_first('')
      # next_url = response.xpath('a[contains(text(),"Next >")]/@href').extract_first('')
      # yield Request(url=parse.urljoin(response.url, next_url), callback=self.parse)
      if next_url == 'Next >':
      next_url = response.css('div.pager a:last-child::attr(href)').extract_first('')
      yield Request(url=parse.urljoin(response.url, next_url),callback=self.parse)
      def parse_detail(self,response):
      ''' 详情页面解析方法 '''
      match_re = re.match('.*?(\d+)',response.url)
      if match_re:
      article_item = JobBoleArticleItem()
      title = response.css('#news_title a::text').extract_first('')
      create_date = response.css('#news_info .time::text').extract_first('')
      match_res = re.match('.*?(\d+.*)',create_date)
      if match_res:
      create_date = match_res.group(1)
      content = response.css('#news_content').extract()[0]
      # tag_list = response.css('.news_tags a::text').extract()
      tags = ','.join(response.css('.news_tags a::text').extract())
      post_id = match_re.group(1)
      # html = requests.get(url=parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(post_id)))
      # j_data = json.loads(html.text)
      article_item['title'] = title
      article_item['create_date'] = create_date
      article_item['content'] = content
      article_item['tags'] = tags
      article_item['url'] = response.url
      if response.meta.get('front_image_url',[]):
      article_item['front_image_url'] = [response.meta.get('front_image_url','')]
      else:
      article_item['front_image_url'] = []
      yield Request(url=parse.urljoin(response.url, '/NewsAjax/GetAjaxNewsInfo?contentId={}'.format(post_id)), meta={ 
      'article_item':article_item},callback=self.parse_nums)
      # praise_nums = j_data['DiggCount']
      # fav_nums = j_data['TotalView']
      # comment_nums = json['CommentCount']
      def parse_nums(self,response):
      j_data = json.loads(response.text)
      article_item = response.meta.get('article_item','')
      praise_nums = j_data['DiggCount']
      fav_nums = j_data['TotalView']
      comment_nums = j_data['CommentCount']
      article_item['praise_nums'] = praise_nums
      article_item['fav_nums'] = fav_nums
      article_item['comment_nums'] = comment_nums
      article_item['url_object_id'] = common.get_md5(article_item['url'])
      yield article_item
      
    • items.py

      # Define here the models for your scraped items
      #
      # See documentation in:
      # https://docs.scrapy.org/en/latest/topics/items.html
      import scrapy
      class Demo01JobboleItem(scrapy.Item):
      # define the fields for your item here like:
      # name = scrapy.Field()
      pass
      #不使用提供的模板,我们自己定义一个item,只需要像模板一样让自己的item类也继承scrapy.Item即可
      class JobBoleArticleItem(scrapy.Item):
      ''' 定义数据结构,对应到数据库中也就是所需字段 '''
      #新闻标题
      title = scrapy.Field()
      #新闻发布时间:因为要入库,预想到,很大可能是时间日期格式数据
      create_date = scrapy.Field()
      #每一个新闻详情页的请求URL
      url = scrapy.Field()
      #待定:后续讲解该字段的含义
      url_object_id = scrapy.Field()
      #详情页包含的图片下载URL
      front_image_url = scrapy.Field()
      #图片保存路径
      front_image_path = scrapy.Field()
      #点赞/推荐数量
      praise_nums = scrapy.Field()
      #评论数量
      comment_nums = scrapy.Field()
      #新闻阅读数量
      fav_nums = scrapy.Field()
      #新闻所属标签:预想到标签可能不止一个
      tags = scrapy.Field()
      #新闻页面的主体内容
      content = scrapy.Field()
      
    • pipelines.py

      # Define your item pipelines here
      #
      # Don't forget to add your pipeline to the ITEM_PIPELINES setting
      # See: https://docs.scrapy.org/en/latest/topics/item-pipeline.html
      import codecs
      import json
      import MySQLdb
      from twisted.enterprise import adbapi
      from MySQLdb.cursors import DictCursor
      from scrapy.pipelines.images import ImagesPipeline
      # useful for handling different item types with a single interface
      from itemadapter import ItemAdapter
      class Demo01JobbolePipeline(object):
      def process_item(self, item, spider):
      return item
      class MysqlPipeline(object):
      def __init__(self):
      self.conn = MySQLdb.connect('127.0.0.1','root','185268','article_spider',charset='utf8',use_unicode=True)
      self.cursor = self.conn.cursor()
      def process_item(self,item,spider):
      insert_sql = """ insert into jobbole_article (title,url,url_object_id,front_image_path,front_image_url,paise_nums,comment_nums,fav_nums,tags,content,create_date) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE comment_nums=VALUES(comment_nums) """
      parmas = list()
      parmas.append(item.get('title',''))
      parmas.append(item.get('url',''))
      parmas.append(item.get('url_object_id',''))
      parmas.append(item.get('front_image_path',''))
      front_image = ','.join(item.get('front_image_url',[]))
      parmas.append(front_image)
      parmas.append(item.get('paise_nums',0))
      parmas.append(item.get('comment_nums',0))
      parmas.append(item.get('fav_nums',0))
      parmas.append(item.get('tags',''))
      parmas.append(item.get('content',''))
      parmas.append(item.get('create_date','1970-07-01'))
      self.cursor.execute(insert_sql,tuple(parmas))
      self.conn.commit()
      return item
      class MysqlTwistedPipeline(object):
      def __init__(self,dbpool):
      self.dbpool = dbpool
      @classmethod
      def from_settings(cls,settings):
      dbparms = dict(
      host = settings['MYSQL_HOST'],
      db = settings['MYSQL_DBNAME'],
      user = settings['MYSQL_USER'],
      passwd = settings['MYSQL_PASSWORD'],
      charset = 'utf8',
      cursorclass = DictCursor,
      use_unicode = True
      )
      dbpool = adbapi.ConnectionPool('MySQLdb',**dbparms)
      return cls(dbpool)
      def process_item(self,item,spider):
      query = self.dbpool.runInteraction(self.do_insert,item)
      query.addErrback(self.handle_error,item,spider)
      return item
      def handle_error(self,failure,item,spider):
      print(failure)
      def do_insert(self,cursor,item):
      insert_sql = """ insert into jobbole_article (title,url,url_object_id,front_image_path,front_image_url,paise_nums,comment_nums,fav_nums,tags,content,create_date) values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ON DUPLICATE KEY UPDATE comment_nums=VALUES(comment_nums) """
      parmas = list()
      parmas.append(item.get('title', ''))
      parmas.append(item.get('url', ''))
      parmas.append(item.get('url_object_id', ''))
      parmas.append(item.get('front_image_path', ''))
      front_image = ','.join(item.get('front_image_url', []))
      parmas.append(front_image)
      parmas.append(item.get('paise_nums', 0))
      parmas.append(item.get('comment_nums', 0))
      parmas.append(item.get('fav_nums', 0))
      parmas.append(item.get('tags', ''))
      parmas.append(item.get('content', ''))
      parmas.append(item.get('create_date', '1970-07-01'))
      cursor.execute(insert_sql,tuple(parmas))
      class JsonWithEncodingPipline(object):
      ''' 自定义json文件的导出 '''
      def __init__(self):
      self.file = codecs.open('article.json','a',encoding='utf-8')
      def process_item(self,item,spider):
      lines = json.dumps(dict(item),ensure_ascii=False) + '\n'
      self.file.write(lines)
      return item
      def spider_closed(self,spider):
      self.file.close()
      class ArticleImagePipeline(ImagesPipeline):
      def item_completed(self, results, item, info):
      if 'front_image_url' in item:
      image_file_path = ''
      for ok,value in results:
      image_file_path = value['path']
      item['front_image_path'] = image_file_path
      return item
      
    • common.py

      # -*- coding:utf-8 _*-
      """ @version: author:weichao_an @time: 2021/02/03 @file: common.py @environment:virtualenv @email:awc19930818@outlook.com @github:https://github.com/La0bALanG @requirement: """
      import hashlib
      def get_md5(url):
      if isinstance(url,str):
      url = url.encode('utf-8')
      m = hashlib.md5()
      m.update(url)
      return m.hexdigest()
      if __name__ == '__main__':
      print(get_md5('https://www.baidu.com'))
      
    • main.py

      # -*- coding:utf-8 _*-
      """ @version: author:weichao_an @time: 2021/01/29 @file: main.py @environment:virtualenv @email:awc19930818@outlook.com @github:https://github.com/La0bALanG @requirement: """
      import sys
      import os
      from scrapy.cmdline import execute
      sys.path.append(os.path.dirname(os.path.abspath(__file__)))
      execute(['scrapy','crawl','jobbole'])
      
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/153129.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号