作者: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