目录
一. 目标
以一个目标例子来实战理解。目标如下:(1)创建一个 Scrapy 项目。(2)创建一个 Spider 来抓取站点和处理数据。(3)通过命令行将抓取的内容导出。(4)将抓取的内容保存到 MongoDB 数据库。
抓取的目标站点为:Quotes to Scrape,要爬取该网站所以框中的信息。以下为网站首尾部分内容:
二. 准备工作
安装好 Scrapy、MongoDB 和 pymongo 库。MongoDB 和 pymongo 库的安装见4.5 MongoDB 文档存储-CSDN博客。
三. 开始入门
1. 创建项目
直接用命令生成。如果你想要将项目放置在特定目录下,则在特定目录 cmd 命令。例如我在这个目录下 cmd。(注:如果 cmd 是在一个 python 项目中会影响引入、会报错,错误见:15.0 Scrapy 使用中出现的错误分享-CSDN博客)
之后在命令框中输入以下命令:
scrapy startproject 项目名称
例:scrapy startproject tutorial
会出现一个 tutorial 文件夹,完全打开之后如下所示:
在 pycharm 中
对此文件夹结构做出解释:
tutorial # scrapytutorial文件夹
tutorial # 项目的模块,引入的时候需要从这里引入(此文件与 .idea同一目录时)
spiders # 放置 Spiders 的文件夹
__init__.py
__init__.py
items.py # Items 的定义,定义爬取的数据结构
middlewares.py # Middlewares 的定义,定义爬取时的中间件
pipelines.py # Pipelines 的定义,定义数据管道
settings.py # 配置文件
scrapy.cfg # Scrapy 部署时的配置文件
2. 创建 Spider
Spider 是自己定义的类,Scrapy 用它来从网页里抓取内容,并解析抓取的结果。
在 tutorial 文件夹中 cmd 进入,输入命令:
scrapy genspider Spider程序名 域名
例如:scrapy genspider quotes quotes.toscrape.com
之后在 spiders 文件中就会出现 quotes.py,也会多出__pycache__文件夹。
在 quptes.py 中
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes" # 每个项目唯一名字,用来区分不同的 Spider
allowed_domains = ["quotes.toscrape.com"] # 允许爬取的域名
start_urls = ["http://quotes.toscrape.com/"] # Spider在启动时爬取的 url列表
def parse(self, response): # Spider 的一个方法
pass
这个 QuotesSpider 类就是刚才命令行自动创建的 Spider,它继承了 Scrapy 提供的 Spider 类 scrapy.Spider,还有三个属性 ——name、allowed_domains 和 start_urls,还有一个方法 parse。
allowed_domains,如果初始或后续的请求链接不是这个域名下的,则请求链接会被过滤掉。
start_urls,初始请求是由它来定义的。
parse,默认情况下,被调用时 start_urls 里面的链接构成的请求完成下载执行后,
返回的响应就会作为唯一的参数传递给这个函数。该方法负责解析返回的响应、提取数据或者进一步生成要处理的请求。
3. 创建 Item
Item 是保存爬取数据的容器,定义了爬取结果的数据结构。它的使用方法和字典类似。不过,相比字典,Item 多了额外的保护机制,可以避免拼写错误或者定义字段错误。
创建 Item 需要继承 scrapy.Item 类,并且定义类型为 scrapy.Field 的字段(在 items.py 中可看到)。观察目标网站,我们可以获取到的内容有 text、author、tags。
这样我们爬取的每条数据中都包含这3个字段,则我们就可以定义 Item,此时将 items.py修改如下:
import scrapy
class QuoteItem(scrapy.Item):
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()
pass
将类的名称修改为 QuoteItem,使用 Field 定义了三个字段,接下来爬取时我们会使用到这个 Item。
4. 解析 Response
在前面的 Spider 中,parse() 方法的参数 response 是 start_urls 里面的链接爬取后的结果。所以在 parse() 方法中,我们可以直接对 response 变量包含的内容进行解析,比如浏览请求结果的网页源代码,或者进一步分析源代码内容,或者找出结果中的链接而得到下一个请求。
首先看看网页结构,如下图所示。每一页都有多个 class 为 quote 的区块,每个区块内都包含 text、author、tags。那么我们先找出所有的 quote,然后提取每一个 quote 中的内容。
提取的方式可以是 CSS 选择器或 XPath 选择器。在这里我们使用 CSS 选择器进行选择,可直接借助 response 的 css 方法或 xpath 方法实现。parse() 方法的改写如下所示:
def parse(self, response):
quotes = response.css('.quote')
for quote in 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 选择器来选取,这个结果实际上是整个带有标签的节点,要获取它的正文内容,可以加::text 来获取。这时的结果是长度为 1 的列表,所以还需要用 extract_first() 方法来获取第一个元素。而对于 tags 来说,由于我们要获取所有的标签,所以用 extract() 方法获取整个列表即可。
5. 使用 Item
上文定义了 Item,接下来就要使用它了。Item 可以理解为一个字典,但其本身是一个类,所以在声明的时候需要实例化。然后依次用刚才解析的结果赋值 Item 的每一个字段,最后将 Item 返回即可。quotes.py修改如下:
import scrapy
from fifthScrapy.scrapytutorial.scrapytutorial.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response):
quotes = response.css('.quote')
for quote in 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
这里可以看到我的引入很长,因为我是在 BookStudyCrawler 这个 python 项目中 fifthScrapy 文件夹下 cmd 的, .idea文件与 fifthScrapy 同级。
这样,首页的所有内容被解析出来,并被赋值成了一个个 QuoteItem,每个都包含名言的内容、作者和标签。
yield 可参考:python中yield的用法(生成器的讲解)-CSDN博客
6. 后续 Request
上面的操作实现了从初始页面抓取内容。那么,下一页的内容该如何抓取?这就需要我们从当前页面中找到信息来生成下一个请求,然后在下一个请求的页面里找到信息再构造再下一个请求。这样循环往复迭代,从而实现整站的爬取。
下拉网站到底部,检查,找到链接。
可以发现它的链接是 /page/2/,实际上全链接就是:http://quotes.toscrape.com/page/2/。通过这个链接我们就可以构造下一个请求。
构造请求时需要用到 scrapy.Request。这里我们传递两个参数 ——url 和 callback,这两个参数的说明如下。
-
url:它是请求链接。
-
callback:它是回调函数。当指定了该回调函数的请求完成之后,获取到响应,引擎会将该响应作为参数传递给这个回调函数。回调函数进行解析或生成下一个请求,回调函数如上文的 parse() 所示。
由于 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 选择器获取下一个页面的链接,即要获取 a 超链接中的 href 属性。这里用到了::attr(href) 操作。然后再调用 extract_first() 方法获取内容。
第二句代码调用了 urljoin() 方法,urljoin() 方法可以将相对 URL 构造成一个绝对的 URL。例如,获取到的下一页地址是 /page/2,urljoin() 方法处理后得到的结果就是:http://quotes.toscrape.com/page/2/。
第三句代码通过 url 和 callback 变量构造了一个新的请求,回调函数 callback 依然使用 parse() 方法。这个请求完成后,响应会重新经过 parse 方法处理,得到第二页的解析结果,然后生成第二页的下一页,也就是第三页的请求。这样爬虫就进入了一个循环,直到最后一页。
通过几行代码,我们就轻松实现了一个抓取循环,将每个页面的结果抓取下来了。现在,改写之后的整个 Spider 类如下所示:
import scrapy
from fifthScrapy.scrapytutorial.scrapytutorial.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = "quotes"
allowed_domains = ["quotes.toscrape.com"]
start_urls = ["https://quotes.toscrape.com/"]
def parse(self, response, **kwargs):
quotes = response.css('.quote')
for quote in 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)
7. 运行
接下来,cmd 进入项目目录 D:\study\bookStudyProject\tutorial,运行如下命令:
scrapy crawl quotes
就可以看到结果了,部分运行结果如下:
D:\study\bookStudyProject\tutorial>scrapy crawl quotes
2024-03-11 11:33:32 [scrapy.utils.log] INFO: Scrapy 2.8.0 started (bot: tutorial) # 当前的版本号以及正在启动的项目名称。
2024-03-11 11:33:32 [scrapy.utils.log] INFO: Versions: lxml 4.9.3.0, libxml2 2.10.4, cssselect 1.1.0, parsel 1.6.0, w3lib 1.21.0, Twisted 22.10.0, Python 3.11.5 | packaged by Anaconda, Inc. | (main, Sep 11 2023, 13:26:23) [MSC v.1916 64 bit (AMD64)], pyOpenSSL 23.2.0 (OpenSSL 3.0.13 30 Jan 2024), cryptography 41.0.3, Platform Windows-10-10.0.19045-SP0
2024-03-11 11:33:32 [scrapy.crawler] INFO: Overridden settings: # setting.py重写后的配置
{'BOT_NAME': 'tutorial',
'FEED_EXPORT_ENCODING': 'utf-8',
.........,
'TWISTED_REACTOR': 'twisted.internet.asyncioreactor.AsyncioSelectorReactor'}
2024-03-11 11:33:32 [asyncio] DEBUG: Using selector: SelectSelector
........
2024-03-11 11:33:32 [scrapy.middleware] INFO: Enabled extensions:
['scrapy.extensions.corestats.CoreStats',
'scrapy.extensions.telnet.TelnetConsole',
'scrapy.extensions.logstats.LogStats']
2024-03-11 11:33:32 [scrapy.middleware] INFO: Enabled downloader middlewares:
['scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware',
...........
'scrapy.downloadermiddlewares.stats.DownloaderStats']
2024-03-11 11:33:32 [scrapy.middleware] INFO: Enabled spider middlewares:
['scrapy.spidermiddlewares.httperror.HttpErrorMiddleware',
..........
'scrapy.spidermiddlewares.depth.DepthMiddleware']
2024-03-11 11:33:32 [scrapy.middleware] INFO: Enabled item pipelines:
[]
2024-03-11 11:33:32 [scrapy.core.engine] INFO: Spider opened
......
2024-03-11 11:33:34 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/>
{'author': 'Albert Einstein',
'tags': ['change', 'deep-thoughts', 'thinking', 'world'],
'text': '“The world as we have created it is a process of our thinking. It '
'cannot be changed without changing our thinking.”'}
2024-03-11 11:33:34 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/>
{'author': 'J.K. Rowling',
'tags': ['abilities', 'choices'],
'text': '“It is our choices, Harry, that show what we truly are, far more '
'than our abilities.”'}
...........
2024-03-11 11:33:38 [scrapy.core.scraper] DEBUG: Scraped from <200 https://quotes.toscrape.com/page/10/>
{'author': 'George R.R. Martin',
'tags': ['books', 'mind'],
'text': '“... a mind needs books as a sword needs a whetstone, if it is to '
'keep its edge.”'}
2024-03-11 11:33:38 [scrapy.dupefilters] DEBUG: Filtered duplicate request: <GET https://quotes.toscrape.com/page/10/> - no more duplicates will be shown (see DUPEFILTER_DEBUG to show all duplicates)
2024-03-11 11:33:38 [scrapy.core.engine] INFO: Closing spider (finished)
2024-03-11 11:33:38 [scrapy.statscollectors] INFO: Dumping Scrapy stats:
{'downloader/request_bytes': 3000,
'downloader/request_count': 11,
'downloader/request_method_count/GET': 11,
......
'scheduler/enqueued': 10,
'scheduler/enqueued/memory': 10,
'start_time': datetime.datetime(2024, 3, 11, 3, 33, 32, 859140)}
2024-03-11 11:33:38 [scrapy.core.engine] INFO: Spider closed (finished)
这里只是部分运行结果,中间一些抓取结果已省略。
首先,Scrapy 输出了当前的版本号以及正在启动的项目名称。接着输出了当前 settings.py 中一些重写后的配置。然后输出了当前所应用的 Middlewares 和 Pipelines。Middlewares 默认是启用的,可以在 settings.py 中修改。Pipelines 默认是空,同样也可以在 settings.py 中配置。后面会对它们进行讲解。
接下来就是输出各个页面的抓取结果了,可以看到爬虫一边解析,一边翻页,直至将所有内容抓取完毕,然后终止。
最后,Scrapy 输出了整个抓取过程的统计信息,如请求的字节数、请求次数、响应次数、完成原因等。
整个 Scrapy 程序成功运行。我们通过非常简单的代码就完成了一个网站内容的爬取,这样相比之前一点点写程序简洁很多。
8. 保存文件
运行完 Scrapy 输出结果后,我们只在控制台看到了。如果想保存结果该怎么办呢?
要完成这个任务其实不需要任何额外的代码,Scrapy 提供的 Feed Exports 可以轻松将抓取结果输出。例如,我们想将上面的结果保存成 JSON 文件,可以执行如下命令:
scrapy crawl quotes -o quotes.json
命令运行后,项目内多了一个 quotes.json 文件,文件包含了刚才抓取的所有内容,内容是 JSON 格式。
另外我们还可以每一个 Item 输出一行 JSON,输出后缀为 jl,为 jsonline 的缩写,命令如下所示:
scrapy crawl quotes -o quotes.jl
或者:
scrapy crawl quotes -o quotes.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:[email protected]/path/to/quotes.csv
其中,ftp 输出需要正确配置用户名、密码、地址、输出路径,否则会报错。
通过 Scrapy 提供的 Feed Exports,我们可以轻松地输出抓取结果到文件。对于一些小型项目来说,这应该足够了。不过如果想要更复杂的输出,如输出到数据库等,我们可以使用 Item Pileline 来完成。
9. 使用 Item Pipeline
如果想进行更复杂的操作,如将结果保存到 MongoDB 数据库,或者筛选某些有用的 Item,则我们可以定义 Item Pipeline 来实现。
Item Pipeline 为项目管道。当 Item 生成后,它会自动被送到 Item Pipeline 进行处理,我们常用 Item Pipeline 来做如下操作。
- 清洗 HTML 数据
- 验证爬取数据,检查爬取字段
- 查重并丢弃重复内容
- 将爬取结果储存到数据库
要实现 Item Pipeline 很简单,只需要定义一个类并实现 process_item() 方法即可。启用 Item Pipeline 后,自动调用这个方Item Pipeline 会法。process_item() 方法必须返回包含数据的字典或 Item 对象,或者抛出 DropItem 异常。
process_item() 方法有两个参数。一个参数是 item,每次 Spider 生成的 Item 都会作为参数传递过来。另一个参数是 spider,就是 Spider 的实例。
接下来,我们实现一个 Item Pipeline,筛掉 text 长度大于 50 的 Item,并将结果保存到MongoDB。
修改项目里的 pipelines.py 文件,之前用命令行自动生成的文件内容可以删掉,增加一个 TextPipeline 类,内容如下所示:
from scrapy.exceptions import DropItem
class TextPipeline(object):
def __init__(self):
self.limit = 50
def process_item(self, item, spider):
if item['text']:
if len(item['text']) > self.limit:
item['text'] = item['text'][0:self.limit].rstrip() + '...'
return item
else:
return DropItem('Missing Text')
这段代码在构造方法里定义了限制长度为 50,实现了 process_item() 方法,其参数是 item 和 spider。首先该方法判断 item 的 text 属性是否存在,如果不存在,则抛出 DropItem 异常;如果存在,再判断长度是否大于 50,如果大于,那就截断然后拼接省略号,再将 item 返回即可。
接下来,我们将处理后的 item 存入 MongoDB,定义另外一个 Pipeline。同样在 pipelines.py 中,我们实现另一个类 MongoPipeline,内容如下所示:
import pymongo
class MongoPipeline(object):
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(mongo_uri=crawler.settings.get('MONGO_URI'), # 指定连接地址
mongo_db=crawler.settings.get('MONGO_DB') # 连接数据库名称
)
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def process_item(self, item, spider):
name = item.__class__.__name__
self.db[name].insert(dict(item))
return item
def close_spider(self, spider):
self.client.close()
MongoPipeline 类实现了 API 定义的另外几个方法。
- from_crawler,这是一个类方法,用 @classmethod 标识,是一种依赖注入的方式,方法的参数就是 crawler,通过 crawler 这个我们可以拿到全局配置的每个配置信息,在全局配置 settings.py 中我们可以定义 MONGO_URI 和 MONGO_DB 来指定 MongoDB 连接需要的地址和数据库名称,拿到配置信息之后返回类对象即可。所以这个方法的定义主要是用来获取 settings.py 中的配置的。
- open_spider,当 Spider 被开启时,这个方法被调用。在这里主要进行了一些初始化操作。
- close_spider,当 Spider 被关闭时,这个方法会调用,在这里将数据库连接关闭。
最主要的 process_item() 方法则执行了数据插入操作。
定义好 TextPipeline 和 MongoPipeline 这两个类后,我们需要在 settings.py 中使用它们。MongoDB 的连接信息还需要定义。
我们在 settings.py 中加入如下内容:
ITEM_PIPELINES = {
'tutorial.pipelines.TextPipeline': 300,
'tutorial.pipelines.MongoPipeline': 400,
}
MONGO_URI='localhost'
MONGO_DB='tutorial'
赋值 ITEM_PIPELINES 字典,键名是 Pipeline 的类名称,键值是调用优先级,是一个数字,数字越小则对应的 Pipeline 越先被调用。
再重新执行爬取,命令如下所示:
scrapy crawl quotes
爬取结束后,MongoDB (下载等见:4.5 MongoDB 文档存储-CSDN博客)中创建了一个 tutorial 的数据库、QuoteItem 的表,如下图所示:
长的 text 已经被处理并追加了省略号,短的 text 保持不变,author 和 tags 也都相应保存。
这个流程似乎比较复杂,入门的话多看看,先理解学会,与套公式一样。
文章到此结束,本人新手,若有错误,欢迎指正;若有疑问,欢迎讨论。若文章对你有用,点个小赞鼓励一下,谢谢大家,一起加油吧!
标签:11,15.2,入门,item,text,Scrapy,quotes,scrapy,Item From: https://blog.csdn.net/weixin_51100340/article/details/136603114