Scrapy 2.0入门
Scrapy 入门
在本教程中,我们假设您的系统上已经安装了Scrapy。
如果不是这种情况,请参阅安装指南。
我们将爬取quotes.toscrape.com,该网站列出了著名作家的名言。
本教程将指导您完成以下任务:
- 创建一个新的Scrapy项目
- 编写爬虫以爬网站点并提取数据
- 使用命令行导出抓取的数据
- 更改爬虫以递归地跟随链接
- 使用爬虫参数
Scrapy用Python编写。
如果您是该语言的新手,则可能首先要了解该语言,以充分利用Scrapy。
如果您已经熟悉其他语言,并且想快速学习Python,那么Python教程是一个很好的资源。
如果您不熟悉编程并且想开始使用Python,那么以下书籍可能对您有用:
- Automate the Boring Stuff With Python
- How To Think Like a Computer Scientist
- Learn Python 3 The Hard Way
您还可以查看针对非程序员的Python资源列表,以及Learnpython-subreddit中的建议资源。
创建项目
在开始抓取之前,您将必须设置一个新的Scrapy项目。
进入您要存储代码目录,运行:
scrapy startproject tutorial
这将创建一个tutorial
目录,其中包含以下内容:
tutorial/
scrapy.cfg # deploy configuration file
tutorial/ # project's Python module, you'll import your code from here
__init__.py
items.py # project items definition file
middlewares.py # project middlewares file
pipelines.py # project pipelines file
settings.py # project settings file
spiders/ # a directory where you'll later put your spiders
__init__.py
我们的第一只爬虫
爬虫是您定义的类,Scrapy用于从网站(或一组网站)中获取信息。
他们必须继承Spider
的子类,并定义要发出的初始请求,可以选择如何跟随页面中的链接,以及如何解析下载的页面内容以提取数据。
这是我们第一个Spider的代码。
将其保存在项目中tutorial / spiders
目录下的quotes_spider.py
文件中:
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)
如您所见,我们的Spider从scrapy.Spider
创建子类并定义了一些属性和方法:
-
name
: 识别爬虫。它在一个项目中必须是唯一的,也就是说,您不能为不同的Spider设置相同的名称。 -
start_requests()
: 必须返回一个可迭代的请求(您可以返回一个请求列表或编写一个生成器函数),Spider将从中开始爬行。随后的请求将根据这些初始请求连续生成。 -
parse()
: 一个将被调用以处理针对每个请求下载的响应的方法。
response参数是TextResponse
的一个实例,该实例保存页面内容并具有其他有用的方法来处理它。
parse()
方法通常解析响应,提取抓取的数据作为字典,还查找要遵循的新URL并从中创建新请求(Request)。
如何运行我们的爬虫
要使我们的爬虫工作,请转到项目的顶级目录并运行:
scrapy crawl quotes
此命令运行带有我们刚刚添加的名称quotes
的爬虫,它将发送对quotes.toscrape.com
域的一些请求。您将获得类似于以下的输出:
... (omitted for brevity)
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Spider opened
2016-12-16 21:24:05 [scrapy.extensions.logstats] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-12-16 21:24:05 [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-1.html
2016-12-16 21:24:05 [quotes] DEBUG: Saved file quotes-2.html
2016-12-16 21:24:05 [scrapy.core.engine] INFO: Closing spider (finished)
...
现在,检查当前目录中的文件。您应该注意,已经创建了两个新文件:quotes-1.html和quotes-2.html,其中包含我们URL的内容,正如我们的parse方法所指示的那样。
注意
如果您想知道为什么我们还没有解析HTML,请稍候,我们将尽快解决。
到底发生了什么?
Scrapy调度Spider的start_requests
方法返回的scrapy.Request
对象。
在收到每个响应时,它实例化Response
对象并调用与请求关联的回调方法(在本例中为parse
方法),并将响应作为参数传递。
start_requests方法的快捷方式
无需实现从URL生成scrapy.Request
对象的start_requests()
方法,您只需定义带有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)
即使我们没有明确告诉Scrapy这样做,也会调用parse()
方法来处理对这些URL的每个请求。
发生这种情况是因为parse()
是Scrapy的默认回调方法,对于没有显式分配的回调的请求会调用该方法。
提取数据
学习如何使用Scrapy提取数据的最佳方法是使用Scrapy shell尝试选择器。运行:
scrapy shell 'http://quotes.toscrape.com/page/1/'
注意
请记住,从命令行运行Scrapy shell时,始终将网址括在引号中,否则包含参数(如
&
字符)的网址将不起作用。
在Windows上,请使用双引号代替:
scrapy shell "http://quotes.toscrape.com/page/1/"
您会看到类似以下内容:
[ ... Scrapy log here ... ] 2016-09-19 12:09:27 [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 0x7fa91d888c90> [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 0x7fa91d888c10> [s] spider <DefaultSpider 'default' at 0x7fa91c8af990> [s] Useful shortcuts: [s] shelp() Shell help (print this help) [s] fetch(req_or_url) Fetch request (or URL) and update local objects [s] view(response) View response in a browser
使用命令行,您可以尝试使用带有响应对象的CSS选择元素:
>> response.css('title') [<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
运行
response.css('title')
的结果是一个名为SelectorList
的类似列表的对象,该对象表示围绕XML / HTML元素的Selector
对象的列表,并允许您运行进一步的查询(queries)来细化选择或提取内容数据。
要从上面的标题中提取文本,您可以执行以下操作:
>>> response.css('title::text').getall()
['Quotes to Scrape']
这里有两点需要注意:一是我们在CSS查询中添加了::text
,这意味着我们只想直接在<title>
元素内选择text元素。如果不指定::text
,则会获得完整的title元素,包括其标签(tag):
>>> response.css('title').getall()
['<title>Quotes to Scrape</title>']
另一件事是,调用.getall()
的结果是一个列表:选择器有可能返回多个结果,因此我们将它们全部提取出来。
当您知道只想要第一个结果时,在这种情况下,您可以执行以下操作:
>>> response.css('title::text').get()
'Quotes to Scrape'
或者,您可以编写:
>>> response.css('title::text')[0].get()
'Quotes to Scrape'
然而,直接在SelectorList
实例上使用.get()
可以避免IndexError
,并且在找不到与选择匹配的任何元素时返回None
。
这里有一个教训:对于大多数抓取代码,您希望它能够对由于页面上找不到内容而导致的错误具有弹性,以便即使某些部分未能被抓取,您也至少可以获取一些数据。
除了getall()
和get()
方法之外,您还可以使用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(response)
从Web浏览器的外壳中打开响应页面很有用。您可以使用浏览器的开发人员工具检查HTML并提供一个选择器(请参阅使用浏览器的开发人员工具进行抓取)。
Selector Gadget还是一个不错的工具,可以快速为视觉选择的元素找到CSS选择器,该选择器可在许多浏览器中使用。
XPath: 简介
除了CSS,Scrapy选择器还支持使用XPath表达式:
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').get()
'Quotes to Scrape'
XPath表达式非常强大,并且是Scrapy Selectors的基础。实际上,CSS选择器是在后台转换为XPath的。您可以看到,如果您仔细阅读shell中选择器对象的文本表示形式。
尽管XPath表达式可能不如CSS选择器流行,但它提供了更多功能,因为除了导航结构之外,它还可以查看内容。使用XPath,您可以选择以下内容:选择包含文本“下一页”的链接。这使XPath非常适合于抓取任务,并且即使您已经知道如何构造CSS选择器,我们也鼓励您学习XPath,这将使抓取更加容易。
我们不会在这里介绍XPath,但是您可以在此处阅读有关将XPath与Scrapy Selectors结合使用的更多信息。要了解有关XPath的更多信息,我们建议这个通过示例学习XPath教程,和这个学习“如何在XPath中思考”教程。
提取名言和作者
现在您对选择和提取有所了解,让我们通过编写代码从网页中提取引号来完善爬虫。
http://quotes.toscrape.com中的每个名言均由如下所示的HTML元素表示:
<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名言(quote)的选择器的列表,其中包括:
>>> response.css("div.quote")
[<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
<Selector xpath="descendant-or-self::div[@class and contains(concat(' ', normalize-space(@class), ' '), ' quote ')]" data='<div class="quote" itemscope itemtype...'>,
...]
上面的查询返回的每个选择器都允许我们在其子元素上运行进一步的查询。让我们将第一个选择器分配给变量,以便我们可以直接在特定名言上运行CSS选择器:
>>> quote = response.css("div.quote")[0]
现在,让我们使用刚刚创建的quote对象从该名言中提取文本,作者和标签:
>>> text = quote.css("span.text::text").get()
>>> text
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").get()
>>> author
'Albert Einstein'
鉴于标签是字符串列表,我们可以使用.getall()
方法来获取所有标签:
>>> tags = quote.css("div.tags a.tag::text").getall()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']
在弄清楚如何提取每一位之后,我们现在可以遍历所有名言元素并将它们放到Python字典中:
>>> for quote in response.css("div.quote"):
... text = quote.css("span.text::text").get()
... author = quote.css("small.author::text").get()
... tags = quote.css("div.tags a.tag::text").getall()
... 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', 'tags': ['abilities', 'choices']}
...
在我们的爬虫中提取数据
让我们回到爬虫。到目前为止,它没有特别提取任何数据,只是将整个HTML页面保存到本地文件中。让我们将上面的提取逻辑集成到我们的爬虫中。
让我们回到爬虫。到目前为止,它没有特别提取任何数据,只是将整个HTML页面保存到本地文件中。让我们将上面的提取逻辑集成到我们的Spider中。
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):
for quote in response.css('div.quote'):
yield {
'text': quote.css('span.text::text').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
如果运行此爬虫,它将输出提取的数据和日志:
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'}
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"}
存储抓取的数据
存储抓取数据的最简单方法是使用Feed导出,并使用以下命令:
scrapy crawl quotes -o quotes.json
这将生成一个quotes.json
文件,其中包含所有以JSON序列化的抓取项。
由于历史原因,Scrapy会附加到给定文件,而不是覆盖其内容。如果您两次运行此命令而没有在第二次之前删除该文件,那么最终将得到一个损坏的JSON文件。
您还可以使用其他格式,例如JSON Lines:
scrapy crawl quotes -o quotes.jl
JSON Lines格式很有用,因为它像流一样,您可以轻松地向其添加新记录。当您运行两次时,就不会遇到JSON的相同问题。另外,由于每条记录都是单独的一行,因此您可以处理大文件而不必将所有内容都放入内存中,因此有类似JQ的工具可以在命令行中帮助完成此任务。
在小型项目中(例如本教程中的项目),这应该足够了。但是,如果要对已抓取物件(item)执行更复杂的操作,则可以编写物件管道 Item Pipeline
。创建项目时,已在tutorial / pipelines.py中为您设置了“Item Pipeline”的占位文件。如果您只想存储已抓取物件,则无需实施任何物件管道。
跟踪链接
假设,您不仅需要从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').get()
'<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'
这获得了anchor元素,但是我们需要属性href
。为此,Scrapy支持CSS扩展,可让您选择属性内容,如下所示:
>>> response.css('li.next a::attr(href)').get()
'/page/2/'
还有一个attrib
属性可用(有关更多信息,请参见选择元素属性):
>>> response.css('li.next a').attrib['href']
'/page/2/'
现在让我们看一下我们的Spider,将其修改为以递归方式访问下一页的链接,并从中提取数据:
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').get(),
'author': quote.css('small.author::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
现在,在提取数据之后,parse()
方法将查找到下一页的链接,使用urljoin()
方法构建完整的绝对URL(因为链接可以是相对的),并产生对下一页的新请求,将其自身注册为回调,以处理下一页的数据提取并保持所有页面的爬取。
您在这里看到的是Scrapy的以下链接机制:当您在回调方法中产生请求时,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').get(),
'author': quote.css('span small::text').get(),
'tags': quote.css('div.tags a.tag::text').getall(),
}
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
yield response.follow(next_page, callback=self.parse)
与scrapy.Request
不同,response.follow
直接支持相对URL----无需调用urljoin
。注意response.follow
仅返回一个Request实例;您仍然需要产生此请求。
您还可以将选择器而不是字符串传递给response.follow
。该选择器应提取必要的属性:
for href in response.css('ul.pager a::attr(href)'):
yield response.follow(href, callback=self.parse)
对于<a>
元素,有一个快捷方式:response.follow自动使用其href属性。因此,代码可以进一步缩短:
for a in response.css('ul.pager a'):
yield response.follow(a, callback=self.parse)
要从一个可迭代对象创建多个请求,可以改用response.follow_all
:
anchors = response.css('ul.pager a')
yield from response.follow_all(anchors, callback=self.parse)
或者,将其进一步缩短:
yield from response.follow_all(css='ul.pager a', callback=self.parse)
更多示例和模式
这是说明回叫和后续链接的另一个爬虫,这次是用于抓取作者信息:
import scrapy
class AuthorSpider(scrapy.Spider):
name = 'author'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
author_page_links = response.css('.author + a')
yield from response.follow_all(author_page_links, self.parse_author)
pagination_links = response.css('li.next a')
yield from response.follow_all(pagination_links, self.parse)
def parse_author(self, response):
def extract_with_css(query):
return response.css(query).get(default='').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
回调的分页链接(pagination links), 像前面看到的那样。
在这里,我们将回调传递给response.follow_all
作为位置参数,以使代码更短;它也适用于request请求。
parse_author
回调定义了一个辅助函数,用于从CSS查询中提取和清除数据,并生成包含作者数据的Python字典。
该爬虫演示的另一件有趣的事情是,即使同一位作者的名言很多,我们也不必担心会多次访问同一作者页面。默认情况下,Scrapy过滤掉对已访问URL的重复请求,从而避免了由于编程错误而导致服务器访问过多的问题。可以通过设置DUPEFILTER_CLASS
进行配置。
希望到目前为止,您已经对如何在Scrapy中使用跟踪链接和回调的机制有了很好的了解。
作为另一个利用以下链接机制的爬虫示例,请查看CrawlSpider
类中的通用爬虫,该类实现了一个小的规则引擎,您可以使用该规则引擎在其上编写爬虫。
同样,一种常见的模式是使用技巧将更多数据传递给回调,从而使用来自多个页面的数据来构建项目。
使用爬虫参数
您可以在运行爬虫时使用-a
选项来为爬虫提供命令行参数:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
这些参数会传递给Spider的__init__
方法,并默认成为爬虫属性。
在此示例中,为tag参数提供的值可通过self.tag
获得。您可以使用它使您的Spider只获取带有特定标记的名言,并根据参数构建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').get(),
'author': quote.css('small.author::text').get(),
}
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
yield response.follow(next_page, self.parse)
如果您将tag=humor
参数传递给该爬虫,您会注意到它只会访问来自humor
标签的URL,例如http://quotes.toscrape.com/tag/humor
。
您可以在此处了解有关处理爬虫参数的更多信息。
下一步
本教程仅介绍了Scrapy的基础知识,但此处未提及许多其他功能。检查Scrapy概览中的还有什么?一章,其中快速概述了最重要的主题。
您可以从“基本概念”部分继续,以进一步了解命令行工具,爬虫,选择器以及本教程未涵盖的其他内容,例如对抓取的数据进行建模。如果您喜欢玩示例项目,请查看“示例”部分。