作者:崔庆才
本篇会通过介绍一个简单的项目,走一遍 Scrapy 抓取流程,通过这个过程,可以对 Scrapy 对基本用法和原理有大体的了解,作为入门。
在本篇开始之前,假设已经安装成功了 Scrapy,如果尚未安装,请参照上一节安装课程。
本节要完成的任务有:
在抓取之前,你必须要先创建一个 Scrapy 项目,可以直接用
命令生成,命令如下:
- scrapy
- scrapy startproject tutorial
在任意文件夹运行都可以,如果提示权限问题,可以加
运行。这个命令将会创建一个名字为 tutorial 的文件夹,文件夹结构如下:
- sudo
- |____scrapy.cfg # Scrapy部署时的配置文件
- |____tutorial# 项目的模块,引入的时候需要从这里引入| |______init__.py| |______pycache__| |____items.py# Items的定义,定义爬取的数据结构| |____middlewares.py# Middlewares的定义,定义爬取时的中间件| |____pipelines.py# Pipelines的定义,定义数据管道| |____settings.py# 配置文件| |____spiders# 放置Spiders的文件夹| ||______init__.py
- || |______pycache__
Spider 是由你来定义的 Class,Scrapy 用它来从网页里抓取内容,并将抓取的结果解析。不过这个 Class 必须要继承 Scrapy 提供的 Spider 类
,并且你还要定义 Spider 的名称和起始请求以及怎样处理爬取后的结果的方法。
- scrapy.Spider
创建一个 Spider 也可以用命令生成,比如要生成 Quotes 这个 Spider,可以执行命令。
- cd tutorial
- scrapy genspider quotes
首先进入到刚才创建的 tutorial 文件夹,然后执行
这个命令,第一个参数是 Spider 的名称,第二个参数是网站域名。执行完毕之后,你会发现在 spiders 文件夹中多了一个
- genspider
,这就是你刚刚创建的 Spider,内容如下:
- quotes.py
- # -*- coding: utf-8 -*-import scrapyclassQuotesSpider(scrapy.Spider):
- name ="quotes"
- allowed_domains = ["quotes.toscrape.com"]
- start_urls = ['http://quotes.toscrape.com/']defparse(self, response):pass
可以看到有三个属性,name,
,
- allowed_domains
,另外还有一个方法 parse
- start_urls
允许爬取的域名,如果初始或后续的请求链接不是这个域名下的,就会被过滤掉。
- allowed_domains
,包含了 Spider 在启动时爬取的 url 列表,初始请求是由它来定义的。
- start_urls
里面的链接构成的请求完成下载后,返回的 response 就会作为唯一的参数传递给这个函数,该方法负责解析返回的 response,提取数据或者进一步生成要处理的请求。
- start_urls
Item 是保存爬取数据的容器,它的使用方法和字典类似,虽然你可以用字典来表示,不过 Item 相比字典多了额外的保护机制,可以避免拼写错误或者为定义字段错误。
创建 Item 需要继承
类,并且定义类型为
- scrapy.Item
的类属性来定义一个 Item。观察目标网站,我们可以获取到到内容有 text, author, tags
- scrapy.Field
所以可以定义如下的 Item,修改
如下:
- items.py
- import scrapyclassQuoteItem(scrapy.Item):
- text = scrapy.Field()
- author = scrapy.Field()
- tags = scrapy.Field()
定义了三个 Field,接下来爬取时我们会使用它。
在上文中说明了 parse 方法的参数 resposne 是
里面的链接爬取后的结果。所以在 parse 方法中,我们可以直接对 response 包含的内容进行解析,比如看看请求结果的网页源代码,或者进一步分析源代码里面包含什么,或者找出结果中的链接进一步得到下一个请求。
- start_urls
观察网站,我们可以看到网页中既有我们想要的结果,又有下一页的链接,所以两部分我们都要进行处理。
首先看一下网页结构,每一页都有多个 class 为 quote 的区块,每个区块内都包含 text,author,tags,所以第一部需要找出所有的 quote,然后对每一个 quote 进一步提取其中的内容。
提取的方式可以选用 CSS 选择器或 XPath 选择器,在这里我们使用 CSS 选择器进行选择,parse 方法改写如下:
- defparse(self, response):
- quotes = response.css('.quote')for quotein quotes:
- text = quote.css('.text::text').extract_first()
- author = quote.css('.author::text').extract_first()
- tags = quote.css('.tags .tag::text').extract()
在这里使用了 CSS 选择器的语法,首先利用选择器选取所有的 quote 赋值为 quotes 变量。然后利用 for 循环对每个 quote 遍历,解析每个 quote 的内容。
对 text 来说,观察到它的 class 为 text,所以可以用
来选取,这个结果实际上是整个带有标签的元素,要获取它的内容,可以加
- .text
来得到。这时的结果是大小为 1 的数组,所以还需要用
- ::text
方法来获取第一个元素,而对于 tags 来说,由于我们要获取所有的标签,所以用 extract 方法获取即可。
- extract_first
以第一个 quote 的结果为例,各个选择方法及结果归类如下:
- class="quote" itemscope="" itemtype="http://schema.org/CreativeWork">class="text" itemprop="text">"The worldas we have created itis a process of our thinking. It cannot be changed without changing our thinking."byclass="author" itemprop="author">Albert Einstein"/author/Albert-Einstein">(about)class="tags">
- Tags:class="keywords" itemprop="keywords" content="change,deep-thoughts,thinking,world">class="tag" href="/tag/change/page/1/">changeclass="tag" href="/tag/deep-thoughts/page/1/">deep-thoughtsclass="tag" href="/tag/thinking/page/1/">thinkingclass="tag" href="/tag/world/page/1/">world</a>
- </div>
- </div>
- quote.css('.text')
- ["descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' text ')]" data='"text" itemprop="text">"The '>]
- quote.css('.text::text')
- ["descendant-or-self::*[@class and contains(concat(' ', normalize-space(@class), ' '), ' text ')]/text()" data='"The world as we have created it is a pr'>]
- quote.css('.text').extract()
- ['"text" itemprop="text">"The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking."</span>']
- quote.css('.text::text').extract()
- ['"The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking."']
- quote.css('.text::text').extract_first()
- "The world as we have created itis a processof our thinking. It cannot be changed without changing our thinking."
所以,对于 text,要获取第一个元素即可,所以使用
方法,对于 tags,要获取所有元素,使用
- extract_first()
方法。
- extract()
刚才定义了 Item,接下来就要轮到使用它了,你可以把它理解为一个字典,不过在声明的时候需要实例化。然后依次对刚才解析的结果赋值,返回即可。
接下来 QuotesSpider 改写如下:
- import scrapyfrom tutorial.itemsimport QuoteItemclassQuotesSpider(scrapy.Spider):
- name ="quotes"
- allowed_domains = ["quotes.toscrape.com"]
- start_urls = ['http://quotes.toscrape.com/']defparse(self, response):
- quotes = response.css('.quote')for quotein quotes:
- item = QuoteItem()
- item['text'] = quote.css('.text::text').extract_first()
- item['author'] = quote.css('.author::text').extract_first()
- item['tags'] = quote.css('.tags .tag::text').extract()yield item
如此一来,首页的所有内容就解析出来了,并赋值成了一个个 QuoteItem。
如上的操作实现了从初始页面抓取内容,不过下一页的内容怎样继续抓取?这就需要我们从该页面中找到信息来生成下一个请求,然后下一个请求的页面里找到信息再构造下一个请求,这样循环往复迭代,从而实现整站的爬取。
观察到刚才的页面拉到最下方,有一个 Next 按钮,查看一下源代码,可以发现它的链接是
,实际上全链接就是
- /page/2/
,通过这个链接我们就可以构造下一个请求。
- http://quotes.toscrape.com/page/2
构造请求时需要用到
,在这里我们传递两个参数,url 和 callback。
- scrapy.Request
在这里,由于 parse 就是用来解析 text,author,tags 的方法,而下一页的结构和刚才已经解析的页面结构是一样的,所以我们还可以再次使用 parse 方法来做页面解析。
好,接下来我们要做的就是利用选择器得到下一页链接并生成请求,在 parse 方法后追加下面的代码。
- next = response.css('.pager .next a::attr(href)').extract_first()
- url = response.urljoin(next)yield scrapy.Request(url=url, callback=self.parse)
第一句代码是通过 CSS 选择器获取下一个页面的链接,需要获取
超链接中的 href 属性,在这里用到了
- <a>
操作,通过
- ::attr(href)
加属性名称我们可以获取属性的值。然后再调用 extract_first 方法获取内容。
- ::attr
第二句是调用了 urljoin 方法,它可以将相对 url 构造成一个绝对的 url,例如获取到的下一页的地址是
,通过 urljoin 方法处理后得到的结果就是
- /page/2
- http://quotes.toscrape.com/page/2/
第三句是通过 url 和 callback 构造了一个新的请求,回调函数 callback 依然使用的 parse 方法。这样在完成这个请求后,response 会重新经过 parse 方法处理,处理之后,得到第二页的解析结果,然后生成第二页的下一页,也就是第三页的请求。这样就进入了一个循环,直到最后一页。
通过几行代码,我们就轻松地实现了一个抓取循环,将每个页面的结果抓取下来了。
现在改写之后整个 Spider 类是这样的:
- import scrapyfrom tutorial.itemsimport QuoteItemclassQuotesSpider(scrapy.Spider):
- name ="quotes"
- allowed_domains = ["quotes.toscrape.com"]
- start_urls = ['http://quotes.toscrape.com/']defparse(self, response):
- quotes = response.css('.quote')for quotein quotes:
- item = QuoteItem()
- item['text'] = quote.css('.text::text').extract_first()
- item['author'] = quote.css('.author::text').extract_first()
- item['tags'] = quote.css('.tags .tag::text').extract()yield item
- next = response.css('.pager .next a::attr("href")').extract_first()
- url = response.urljoin(next)yield scrapy.Request(url=url, callback=self.parse)
接下来让我们试着运行一下看看结果,进入目录,运行如下命令:
- scrapy crawl quotes
就可以看到 Scrapy 的运行结果了。
- 2017-02-1913:37:20 [scrapy.utils.log] INFO: Scrapy1.3.0 started (bot: tutorial)2017-02-1913:37:20 [scrapy.utils.log] INFO: Overridden settings: {'NEWSPIDER_MODULE':'tutorial.spiders','SPIDER_MODULES': ['tutorial.spiders'],'ROBOTSTXT_OBEY':True,'BOT_NAME':'tutorial'}2017-02-1913:37:20 [scrapy.middleware] INFO: Enabled extensions:
- ['scrapy.extensions.logstats.LogStats','scrapy.extensions.telnet.TelnetConsole','scrapy.extensions.corestats.CoreStats']2017-02-1913:37:20 [scrapy.middleware] INFO: Enabled downloader middlewares:
- ['scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware','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.stats.DownloaderStats']2017-02-1913:37:20 [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']2017-02-1913:37:20 [scrapy.middleware] INFO: Enabled item pipelines:
- []2017-02-1913:37:20 [scrapy.core.engine] INFO: Spider opened2017-02-1913:37:20 [scrapy.extensions.logstats] INFO: Crawled0 pages (at0 pages/min), scraped0 items (at0 items/min)2017-02-1913:37:20 [scrapy.extensions.telnet] DEBUG: Telnet console listening on127.0.0.1:60232017-02-1913:37:21 [scrapy.core.engine] DEBUG: Crawled (404) (referer: None)2017-02-1913:37:21 [scrapy.core.engine] DEBUG: Crawled (200) (referer: None)2017-02-1913:37:21 [scrapy.core.scraper] DEBUG: Scrapedfrom <200 http://quotes.toscrape.com/>
- {'author':u'Albert Einstein','tags': [u'change',u'deep-thoughts',u'thinking',u'world'],'text':u'\u201cThe world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.\u201d'}2017-02-1913:37:21 [scrapy.core.scraper] DEBUG: Scrapedfrom <200 http://quotes.toscrape.com/>
- {'author':u'J.K. Rowling','tags': [u'abilities',u'choices'],'text':u'\u201cIt is our choices, Harry, that show what we truly are, far more than our abilities.\u201d'}
- ...2017-02-1913:37:27 [scrapy.core.engine] INFO: Closing spider (finished)2017-02-1913:37:27 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
- {'downloader/request_bytes':2859,'downloader/request_count':11,'downloader/request_method_count/GET':11,'downloader/response_bytes':24871,'downloader/response_count':11,'downloader/response_status_count/200':10,'downloader/response_status_count/404':1,'dupefilter/filtered':1,'finish_reason':'finished','finish_time': datetime.datetime(2017,2,19,5,37,27,227438),'item_scraped_count':100,'log_count/DEBUG':113,'log_count/INFO':7,'request_depth_max':10,'response_received_count':11,'scheduler/dequeued':10,'scheduler/dequeued/memory':10,'scheduler/enqueued':10,'scheduler/enqueued/memory':10,'start_time': datetime.datetime(2017,2,19,5,37,20,321557)}2017-02-1913:37:27 [scrapy.core.engine] INFO: Spider closed (finished)
在这里贴出部分运行结果,中间的一些抓取结果输出已省略。
首先 Scrapy 输出了当前的版本号,启动的项目。其次输出了当前在 settings.py 中的一些重写后的配置。然后输出了当前所应用的 middlewares 和 pipelines,middlewares 是默认启用的,如果要修改,我们可以在
中修改,pipelines 默认是空,同样也可以在 settings.py 中配置,后面会进行讲解。
- settings.py
再接下来就是输出各个页面的抓取结果了,可以看到它一边解析,一边翻页,直至将所有内容抓取完毕,然后终止。
在最后 Scrapy 输出了整个抓取过程的统计信息,如请求的字节数,请求次数,响应次数,完成原因等等。
这样整个 Scrapy 程序就成功运行完毕了。
可以发现我们通过非常简单的一些代码就完成了一个网站内容的爬取,相比之前自己一点点写程序是不是简洁太多了?
刚才运行完 Scrapy 后,我们只在控制台看到了输出结果,如果想将结果保存该怎么办呢?
比如最简单的形式,将结果保存成 Json 文件。
要完成这个其实不需要你写任何额外的代码,Scrapy 提供了 Feed Exports 可以轻松地将抓取结果输出,例如我们想将上面的结果保存成 Json 文件,可以执行如下命令:
- scrapycrawlquotes-oquotes.json
运行后发现项目内就会多了一个 quotes.json 文件,里面包含的就是刚才抓取的所有内容,是一个 Json 格式,多个项目由中括号包围,是一个合法的 Json 格式。
另外你还可以每一个 Item 一个 Json,最后的结果没有中括号包围,一行对应一个 Item,命令如下:
- scrapycrawlquotes-oquotes.jl
或
- scrapycrawlquotes-oquotes.jsonlines
另外还支持很多格式输出,例如 csv,xml,pickle,marshal 等等,还支持 ftp,s3 等远程输出,另外还可以通过自定义 ItemExporter 来实现其他的输出。
例如如下命令分别对应输出为 csv,xml,pickle,marshal,格式以及 ftp 远程输出:
- scrapy crawl quotes -o quotes.csv
- scrapy crawl quotes -o quotes.xml
- scrapy crawl quotes -o quotes.pickle
- scrapy crawl quotes -o quotes.marshal
- scrapy crawl quotes -o ftp://user:pass@ftp.example.com/path/to/quotes.csv
其中 ftp 输出需要你正确配置好你的用户名,密码,地址,输出路径,否则会报错。
通过 Scrapy 提供的 Feed Exports 我们可以轻松地输出抓取结果到文件,对于一些小型项目这应该是足够了,不过如果想要更复杂的输出,如输出到数据库等等,你可以使用 Item Pileline 更方便地实现。
至此,你已经可以成功地完成抓取并将结果保存了,如果你想进行更复杂的操作,如将结果保存到数据库,如 MongoDB,或者筛选某些有用的 Item,可以定义 Item Pileline 来实现。
Item Pipeline 意为项目管道,当生成 Item 后,它会自动被送到 Item Pipeline 进行处理,我们常用它来做如下操作:
要实现一个 Item Pipeline 很简单,只需要定义一个类并实现 process_item 方法即可,启用后,Item Pipeline 会自动调用这个方法,这个方法必须返回包含数据的字典或是 Item 对象,或者抛出 DropItem 异常。
这个方法由两个参数,一个是 item,每次 Spider 生成的 Item 都会作为参数传递过来,另一个是 spider,就是 Spider 的实例。
好,接下来我们实现一个 Item Pipeline,筛掉 text 长度大于 50 的 Item 并将结果保存到 MongoDB。
修改项目里的 pipelines.py 文件,之前自动生成的可以删掉,增加一个 TextPipeline 类,内容如下:
- from scrapy.exceptionsimport DropItemclassTextPipeline(object):def__init__(self):
- self.limit =50defprocess_item(self, item, spider):if item['text']:if len(item['text']) > self.limit:
- item['text'] = item['text'][0:self.limit].rstrip() +'...'return itemelse:return DropItem('Missing Text')
在构造方法里面定义了限制长度,长度限制为 50,然后实现了 process_item 方法,参数是 item 和 spider,首先判断 item 的 text 属性是否存在,如果不存在,那就跑出 DropItem 异常,如果存在,再判断长度是否大于 50,如果大于,那就截断然后拼接省略号,再将 item 返回即可。
接下来,我们再将处理后的 item 存入 MongoDB,如果你还没有安装,请先安装好 MongoDB。
另外还需要安装一个 MongoDB 开发包 pymongo,利用 pip 安装即可:
- pip3 install pymongo
接下来定义另外一个 Pipeline,同样在 pipelines.py 中,实现另一个类 MongoPipeline,内容如下:
- import pymongoclassMongoPipeline(object):def__init__(self, mongo_uri, mongo_db):
- self.mongo_uri = mongo_uri
- self.mongo_db = mongo_db@classmethoddeffrom_crawler(cls, crawler):return cls(
- mongo_uri=crawler.settings.get('MONGO_URI'),
- mongo_db=crawler.settings.get('MONGO_DB')
- )defopen_spider(self, spider):
- self.client = pymongo.MongoClient(self.mongo_uri)
- self.db = self.client[self.mongo_db]defprocess_item(self, item, spider):
- name = item.__class__.__name__
- self.db[name].insert(dict(item))return itemdefclose_spider(self, spider):
- self.client.close()
在这个类中,实现了 API 定义的另外几个方法。
标识,是一种依赖注入的方式,方法的参数就是 crawler,通过 crawler 这个我们可以拿到全局配置的每个配置信息,在全局配置
- @classmethod
中我们可以定义
- settings.py
和
- MONGO_URI
来指定 MongoDB 连接需要的地址和数据库名称,拿到配置信息之后返回类对象即可。所以这个方法的定义主要是用来获取
- MONGO_DB
中的配置的。
- settings.py
,当 spider 被开启时,这个方法被调用。在这里主要进行了一些初始化操作。
- open_spider
,当 spider 被关闭时,这个方法会调用,在这里将数据库连接关闭。
- close_spider
那么最主要的
方法则执行了数据插入操作。
- process_item
好,定义好这两个类后,我们需要在
中使用他们,还需要定义 MongoDB 的连接信息。
- settings.py
在
中加入如下内容:
- settings.py
- ITEM_PIPELINES = {'tutorial.pipelines.TextPipeline': 300,'tutorial.pipelines.MongoPipeline': 400,
- }
- MONGO_URI='localhost'
- MONGO_DB='tutorial'
赋值
字典,键名是 Pipeline 的类名称,键值是调用优先级,数字越小越先被调用。
- ITEM_PIPELINES
定义好了之后,再重新执行爬取,命令如下:
- scrapy crawl quotes
爬取结束后,可以观察到 MongoDB 中创建了一个 tutorial 的数据库,QuoteItem 的表。
到现在,我们就通过抓取 quotes 完成了整个 Scrapy 的简单入门,但这只是冰山一角,还有很多内容等待我们去探索,后面会进行讲解。
本节代码:https://github.com/Germey/ScrapyTutorial
【有奖讨论】程序员,怎么应对三十岁? 点击查看详情
来源: http://www.cnblogs.com/qcloud1001/p/6826573.html