本文为译文,原文见地址:https://docs.scrapy.org/en/latest/intro/tutorial.html
Scrapy教程
在本教程中,我们假设Scrapy已经在你的系统上成功安装了。如果没有,请跳转到安装向导。
我们将前往http://quotes.toscrape.com/,一个列出了著名作家名言的网站。
本教程会向你展示如下任务:
- 创建一个新的Scrapy工程
- 编写一个爬虫,用来爬取一个网站和提取数据
- 使用命令行来导出爬取到的数据
- 修改爬虫,使其能够递归跟踪链接
- 使用爬虫参数
Scrapy是使用Python编写的。如果你不熟悉Python语言,你可能需要首先学习Python语言。
如果你已经对其他语言比较熟悉,为了更加快速地学习Python,我们推荐阅读Div Into Python3。或者,你可以学习Python Tutorial。
如果你是编程方面的新手,并且你想从Python开始学习,你可以在Learn Python The Hard Way找到很多有用的在线书籍。你也可以在这里找到针对非程序员的Python资源列表。
创建一个工程
在你开始爬数据之前,你必须先建立一个新的Scrapy工程。先找到(也可以新建)一个文件夹,用于存储代码,然后在控制台中进入这个文件夹,并运行下面的命令:
scrapy startproject tutorial
这个命令将创建一个tutorial文件夹,这个文件夹中有下述的内容:
tutorial/
scrapy.cfg # 部署的配置文件
tutorial/ # 工程的Python模块,你将在这里导入你的代码
init.py
items.py # 工程的数据项定义文件
middlewares.py # 工程的中间件文件
pipelines.py # 工程的管道文件
settings.py # 工程的配置文件
spiders/ # 文件夹下将存储之后你会编写的爬虫
init.py
我们的第一个爬虫
爬虫是一些类,这些类是由你定义的,并且Scrapy会使用这些类来爬取一个网站(或者一组网站)的信息。这些爬虫类必须是scrapy.Spider的子类,并且定义了要发出的初始请求,可以选择如何跟踪页面中的链接,以及如何解析下载的页面内容以提取数据。
下面展示我们的第一个爬虫代码。将这些代码保存在quote_spider.py文件中,将这个文件存储在项目中的tutorial/spiders文件夹下:
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'quotes'
def start_requests(self):
urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
for url in urls:
yield scrapy.Request(url=url, callback=self.parse)
def parse(self, response):
page = response.url.split('/')[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
self.log('Saved file %s' % filename)
正如你所见的,我们的爬虫继承于scrapy.Spider,并且定义了一些属性和方法:
- name:爬虫的标识。在一个工程中,这个值必须是唯一的,你不能在不同的爬虫中设定相同的名字。
- start_requests():必须返回一个关于请求对象的迭代器(你可以返回请求的列表,或者编写一个生成器函数),爬虫将会从这些请求对象开始爬取。后续请求将从这些初始请求中依次生成。
- parse():这个函数将被用来处理每一个请求下载的响应。response参数是TextResponse的一个实例,它保存了页面内容,并提供了更多有用的方法来处理它。
parse()函数通常用来解析响应,提取爬取到的数据到字典中,并且也能找到新的URL来创建新的请求(Request)。
怎样运行我们的爬虫?
为了让我们的爬虫能够工作,先跳转到最顶层的文件夹并且执行下面的命令:
scrapy crawl quotes
这个命令运行了一个名叫quotes的爬虫(我们之前命名的那个),这个爬虫将会发送一些请求到quotes.toscrape.com的域。你会获取到一个类似于下面的输出:
... (省略)
2018-12-24 09:51:36 [scrapy.core.engine] INFO: Spider opened
2018-12-24 09:51:36 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2018-12-24 09:51:36 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2018-12-24 09:51:38 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2018-12-24 09:51:38 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2018-12-24 09:51:39 [quotes] DEBUG: Saved file quotes-1.html
2018-12-24 09:51:39 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2018-12-24 09:51:39 [quotes] DEBUG: Saved file quotes-2.html
2018-12-24 09:51:39 [scrapy.core.engine] INFO: Closing spider (finished)
...(省略)
现在,检查当前目录中的文件。你应该注意到已经创建了两个新文件:quote-1.html和quote-2.html,这两个文件正如我们的parse函数所指示的那样,包含了相应URL的内容。
刚刚在爬虫引擎下发生了什么?
Scrapy调度了从爬虫的start_requests函数返回的scrapy.Request对象。在接收到每一个响应后,Scrapy将实例化Response对象并调用与请求关联的回调函数(在本例中,指parse函数),并将响应对象作为参数传递给回调函数。
start_requests的快捷函数
除了使用strat_requests()函数从URL生成scrapy.Request对象之外,你还可以定义用于存储URL列表的类属性start_urls。随后,你的爬虫将默认使用这个列表用来实现start_requests():
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'quotes'
start_urls = [
'http://quotes.toscrape.com/page/1/',
'http://quotes.toscrape.com/page/2/',
]
def parse(self, response):
page = response.url.split('/')[-2]
filename = 'quotes-%s.html' % page
with open(filename, 'wb') as f:
f.write(response.body)
调用parse()函数来处理URL的每一个请求,即使我们并没有显示告诉Scrapy要这么做。这是因为parse()是Scrapy默认的回调函数,如果在发起请求时没有显示赋值回调函数,那么将默认使用此回调函数。
提取数据
学习Scrapy提取数据的最好方式是在Scrapy shell使用选择器:
scrapy shell ‘http://quotes.toscrape.com/page/1/’
注意:当从命令行运行Scrapy shell时,请记住始终将url括在引号中,否则url将包含参数(比如&字符)不会工作。在Windows中,使用双引号。
你将看到类似下面的输出:
[ ... Scrapy log here ... ]
2018-12-25 09:27:55 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler <scrapy.crawler.Crawler object at 0x000002379DA02B00>
[s] item {}
[s] request <GET http://quotes.toscrape.com/page/1/>
[s] response <200 http://quotes.toscrape.com/page/1/>
[s] settings <scrapy.settings.Settings object at 0x000002379DA02CC0>
[s] spider <DefaultSpider 'default' at 0x2379dc85d30>
[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
>>>
使用shell,你可以尝试使用CSS来选择响应对象的某些元素:
>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
运行response.css(‘title’)将返回一个类似于列表的对象SelectorList,这个对象表示一个包装在XML/HTML元素周围的Selector对象列表,它允许你运行进一步的查询来细化选择或提取数据。
为了提取网页中的title文本,你可以这样做:
>>> response.css('title::text').extract()
['Quotes to Scrape']
这里有两点需要注意:一是我们在CSS查询中添加了::text,这意味着我们只希望选择在
>>> response.css('title').extract()
['<title>Quotes to Scrape</title>']
另外需要注意的是,调用.extract()的结果是一个列表,因为我们正在处理SelectorList的实例。当你知道你仅仅需要第一个结果,在这个示例中,你可以这样做:
>>> response.css('title::text').extract_first()
'Quotes to Scrape'
另一种写法是:
>>> response.css('title::text')[0].extract()
'Quotes to Scrape'
使用.extract_first()可以避免一个IndexError异常,并且当它没有找到任何匹配选择器的元素时将返回None。
这里有一个教训:对于大多数抓取代码,你希望它能够对由于在页面上找不到东西而导致的错误保持弹性,这样即使某些部分不能被爬取,你至少可以获得一些数据。
除了extract()和extract_first()函数,你也可以使用re()函数来使用正则表达式提取数据:
>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']
为了找到要使用的CSS选择器,你可能会发现使用view(respones)在web浏览器的shell中打开请求页面是非常有用的。你可以使用浏览器开发工具或者扩展,比如Firebug(请参阅使用Firebug进行抓取和使用Firefox进行抓取的部分)。
Selector Gadget也是一个很好的工具,可以快速找到CSS选择器的视觉上选定的元素,这可以在许多浏览器上工作。
XPath:一个简短的介绍
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').extract_first()
'Quotes to Scrape'
XPath表达式很强大,并且这也是Scrapy选择器的基础。实际上,CSS选择器将被转换为XPath。如果仔细阅读shell中选择器对象的文本,你就会看到这一点。
虽然XPath表达式可能没有CSS选择器那么流行,但它提供了更加强大的功能,因为除了导航结构之外,它还可以查看内容。使用XPath,你可能需要选择这种东西:选择包含了文本内容“Next Page”的链接。这使得XPath非常适合于抓取任务,我们鼓励你学习XPath,即使你已经知道如何构造CSS选择器,它也会使抓取更加容易。
我们在这里不会介绍太多XPath,但是你可以在这里阅读关于使用XPath和Scrapy选择器的更多信息。要了解更多关于XPath的知识,我们推荐本教程通过示例学习XPath,以及如何使用XPath的思考。
提取名言和作者
现在你已经对选择和提取有了一些了解,让我们通过编写代码从web页面中提取引号来完成爬虫。
在http://quotes.toscrape.com中的每一条名言都类似于下面的格式:
<div class="quote">
<span class="text">“The world as we have created it is a process of our
thinking. It cannot be changed without changing our thinking.”</span>
<span>
by <small class="author">Albert Einstein</small>
<a href="/author/Albert-Einstein">(about)</a>
</span>
<div class="tags">
Tags:
<a class="tag" href="/tag/change/page/1/">change</a>
<a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a>
<a class="tag" href="/tag/thinking/page/1/">thinking</a>
<a class="tag" href="/tag/world/page/1/">world</a>
</div>
</div>
打开scrapy shell,尝试提取我们希望的数据:
$ scrapy shell 'http://quotes.toscrape.com
我们将得到名言的HTML元素的选择器:
>>> response.css("div.quote")
上面查询返回的每一个选择器都允许我们在其子元素上运行进一步的查询。首先我们将选择器赋值给一个变量quote,这样我们就可以直接使用quote作为CSS选择器:
>>> quote = response.css("div.quote")[0]
现在,我们可以从刚刚创建的quote对象中提取title,author和tags:
>>> title = quote.css("span.text::text").extract_first()
>>> author = quote.css("small.author::text").extract_first()
>>> title
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author
'Albert Einstein'
考虑到标签是一个列表字符串,我们可以使用.extract()函数来获取所有的标签:
>>> tags = quote.css("a.tag::text").extract()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']
知道了如何提取每一位的方式,现在我们可以遍历所有的名言元素,并且将它们放到一个Python的字典中:
>>> for quote in response.css("div.quote"):
... text = quote.css("span.text::text").extract_first()
... author = quote.css("small.author::text").extract_first()
... tags = quote.css("a.tag::text").extract()
... print(dict(text=text, author=author, tags=tags))
...
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”',
'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'ta
gs': ['abilities', 'choices']}
{'text': '“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything i
s a miracle.”', 'author': 'Albert Einstein', 'tags': ['inspirational', 'life', 'live', 'miracle', 'miracles']}
{'text': '“The person, be it gentleman or lady, who has not pleasure in a good novel, must be intolerably stupid.”', 'author':
'Jane Austen', 'tags': ['aliteracy', 'books', 'classic', 'humor']}
{'text': "“Imperfection is beauty, madness is genius and it's better to be absolutely ridiculous than absolutely boring.”", 'au
thor': 'Marilyn Monroe', 'tags': ['be-yourself', 'inspirational']}
{'text': '“Try not to become a man of success. Rather become a man of value.”', 'author': 'Albert Einstein', 'tags': ['adulthoo
d', 'success', 'value']}
{'text': '“It is better to be hated for what you are than to be loved for what you are not.”', 'author': 'André Gide', 'tags':
['life', 'love']}
{'text': "“I have not failed. I've just found 10,000 ways that won't work.”", 'author': 'Thomas A. Edison', 'tags': ['edison',
'failure', 'inspirational', 'paraphrased']}
{'text': "“A woman is like a tea bag; you never know how strong it is until it's in hot water.”", 'author': 'Eleanor Roosevelt'
, 'tags': ['misattributed-eleanor-roosevelt']}
{'text': '“A day without sunshine is like, you know, night.”', 'author': 'Steve Martin', 'tags': ['humor', 'obvious', 'simile']
}
>>>
在爬虫中提取数据
现在回到我们的爬虫。直到现在,它还没有提取任何的数据,仅仅是保存了整个HTML页面到本地的文件中。让我们将上面的提取逻辑整合到我们的爬虫中。
Scrapy爬虫通常生成许多包含从页面提取的数据的字典。为此,我们在回调函数中使用Python的yield关键字,如下所示:
2018-12-25 11:23:56 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”',
'author': 'Albert Einstein', 'tags': ['change', 'deep-thoughts', 'thinking', 'world']}
2018-12-25 11:23:56 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”', 'author': 'J.K. Rowling', 'ta
gs': ['abilities', 'choices']}
...(在此省略)
存储爬取到的数据
存储爬取到的数据,最简单的方式是使用Feed exports,使用下面的命令:
scrapy crawl quotes -o quotes.json
这种方式将生成一个quotes.json文件,这个文件将包含所有爬取到的项,这些项是以JSON序列化的。
由于历史原因,Scrapy会将内容附加到给定的文件而不是直接覆盖内容。如果你运行这个命令两次,但是没有在第二次运行前删除这个文件,最终你会得到一个损坏的JSON文件。
你也可以使用其他格式,比如JSON Lines:
scrapy crawl quotes -o quotes.jl
JSON Lines格式是很有用的,因为它类似于流,你可以很容易向其追加新纪录。当你运行两次而不删除这个文件时,它不会像JSON一样有相同的问题。另外,由于每个记录都是单独的一行,所以你不必将所有内容都存储在内存中的方式来处理大文件。有一些工具,比如JQ,可以帮助你在命令行中做到这一点。
在小项目中(比如这个教程的项目),这种方式是足够使用的。然而,如果你希望对爬取到的项执行更多复杂的操作,你可以编写一个管道(Item Pipeline)。文件tutorial/pipelines.py是创建项目时已经为你创建好的项目管道的占位符文件。不过,如果只想存储已爬取的项,则不需要实现任何项目管道。
跟踪链接
现在我们希望爬取整个网站http://quotes.toscrape.com的名人名言,而不是爬取前两页。
现在我们知道怎么从页面提取出数据,接下来我们就看看怎么跟踪这些页面的链接。
首先,我们需要提取出页面中我们希望跟踪的链接。检查页面,我们可以看到在下一页的标签处确实有下一页的标记:
<ul class="pager">
<li class="next">
<a href="/page/2/">Next <span aria-hidden="true">→</span></a>
</li>
</ul>
我们可以尝试在shell中提取:
>>> response.css('li.next a').extract_first()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'
这里获取到了锚点元素,但是我们希望获取的是锚点的href属性。为了这个目标,Scrapy支持一个CSS扩展,这个扩展可以使我们获取属性的内容,如下所示:
>>> response.css('li.next a::attr(href)').extract_first()
'/page/2/'
接着我们修改我们的爬虫来递归爬取下一页的链接,并提取对应页面的数据:
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'quotes'
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').extract_first(),
'author': quote.css('small.author::text').extract_first(),
'tags': quote.css('a.tag::text').extract(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
现在,在提取数据后,parse()函数会寻找下一页的链接,使用urljoin()函数可以构建一个完整的绝对URL(因为链接可以是相对的),并且产生了一个新的请求到下一页,并将它自己注册到回调函数,以便继续提取页面的数据和爬行遍历所有页面。
在这里,你看到了Scrapy爬取后续链接的机制:当你在回调函数中yield一个请求时,Scrapy将调度要发送的请求,并注册一个回调函数,这个回调函数将在请求完成的时候被执行。
使用这种方法,你可以构建更加复杂的爬行器,根据你定义的规则来跟踪链接,并根据访问页面的不同类型来提取各种数据。
在我们的示例中,它创建了一个有序的循环,这个循环将一直跟踪所有下一页的链接直到找不到下一页的链接为止——这对于爬行博客、论坛和其他网站时非常方便的。
创建请求的快捷方式
创建请求对象你可以使用response.follow这种快捷方式:
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'quotes'
start_urls = [
'http://quotes.toscrape.com/page/1/',
]
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').extract_first(),
'author': quote.css('small.author::text').extract_first(),
'tags': quote.css('a.tag::text').extract(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
与scrapy.Request不同的是,response.follow支持相对URL——这意味着你不需要调用urljoin。注意,response.follow只返回一个请求实例,因此你还是必须yield这个请求。
你也可以传递一个选择器给response.follow来替换这个字符串,这个选择器应该提取必须的属性:
for href in response.css('li.next a::attr(href)'):
yield response.follow(href, callback=self.parse)
对于标签a元素而言,有一个快捷方式:response.follow会自动地使用它们的href属性。因此代码可以精简如下:
for a in response.css('li.next a'):
yield response.follow(a, callback=self.parse)
注意:response.follow(response.css(‘li.next a’))是无效的,因为response.css返回一个类列表对象,这个对象具有所有结果的选择器,而不是一个单独的选择器。例如上面例子中的for循环,或者response.follow(response.css(‘li.next a’)[0])是正确的。
更多示例和模式
这里有另一个爬虫示例,这个示例阐明了回调函数和跟踪链接,现在将爬取作者信息:
import scrapy
class AuthorSpider(scrapy.Spider):
name = 'author'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
# 跟踪链接到作者页面
for href in response.css('.author + a::attr(href)'):
yield response.follow(href, self.parse_author)
# 跟踪页码链接
for href in response.css('li.next a::attr(href)'):
yield response.follow(href, self.parse)
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).extract_first().strip()
yield {
'name': extract_with_css('h3.author-title::text'),
'birthdate': extract_with_css('.author-born-date::text'),
'bio': extract_with_css('.author-description::text'),
}
这个爬虫将从主页面开始,它将跟踪所有到作者页面的链接,并为每个链接调用parse_author回调函数,同时所有分页链接将会由parse回调函数进行之前的处理。
这里我们传递回调函数给response.follow作为位置参数,用来使代码更加简洁;它是为scrapy.Request工作的。
parse_author回调定义了一个辅助函数,这个函数用于从CSS查询中提取和清理数据,并yield带有作者数据的Python字典。
这个爬虫还演示了,即使同一个作者如果有多条名言,我们也不需要因为多次访问同样的作者页面而烦恼。默认情况下,Scrapy将会过滤掉已经访问过的URL,避免由于编程错误而过多攻击服务器的问题。这种行为可以通过设置DUPEFILTER_CLASS来进行配置。
希望到目前为止,你已经很好地理解了如何使用Scrapy跟踪链接和回调函数的机制。
作为另一个利用以下链接机制的爬行器示例,请查看CrawlSpider类,它是一个通用爬行器,实现了一个小型规则引擎,你可以使用该引擎在其上编写爬行器。
另外,一种常见的模式是使用来自多个页面的数据构建一个项目,这将使用另一种技巧将额外的数据传递给回调。
使用爬虫的参数
在运行爬行器时,可以使用-a选项为爬行器提供命名行参数:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
这些参数将被传递给爬虫的init函数,并且默认情况下会成为爬虫的属性。
在这个示例中,为tag参数提供的值将通过self.tag成为可用的。你可以使用它,使爬行器只获取带有特定标签的名言,并根据参数构建URL:
import scrapy
class QuotesSpider(scrapy.Spider):
name = 'quotes'
def start_requests(self):
url = 'http://quotes.toscrape.com/'
tag = getattr(self, 'tag', None)
if tag is not None:
url = url + 'tag/' + tag
yield scrapy.Request(url, self.parse)
def parse(self, response):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').extract_first(),
'author': quote.css('small.author::text').extract_first(),
}
next_page = response.css('li.next a::attr(href)').extract_first()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
如果你传递tag=humor参数给这个爬虫,你将注意到它只访问了具有humor标签的URL,比如:http://quotes.toscrape.com/tag/humor。
你可以在这里阅读关于爬虫的参数相关知识。
下一步
本教程只介绍了Scrapy的基本知识,但是这里没有提到更多其他特性。看看还有什么?在初识Scrapy一文中,可以快速浏览Scrapy中最重要的特性。