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

本文介绍了使用Scrapy爬虫框架爬取博客园新闻详情页的过程,包括设置环境、分析页面结构、解析数据、处理URL规律、数据库配置以及异常处理。内容涵盖从获取新闻标题、发布时间、详情页URL、图片链接到解析评论数和阅读数等数据,并讲解了如何处理动态加载数据。文章最后讨论了图片保存、数据存储到JSON文件和MySQL数据库的方法。
摘要由CSDN通过智能技术生成

作者: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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

运维法拉令

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值