Scrapy 教程¶
在本教程中,我们假定scrapy已经安装在您的系统上。如果不是这样的话,看 安装指南 .
我们将抓取' quotes.toscrape.com ' _,这是一个列出著名作家名言的网站。
本教程将指导您完成以下任务:
创建新的Scrapy项目
写一篇 spider 对网站进行爬网并提取数据
使用命令行导出抓取的数据
将spider改为递归跟踪链接
使用蜘蛛参数
Scrapy是用 Python 写的。如果你对这门语言不熟悉,你可能想从了解这门语言是什么开始,从 Scrapy 语言中得到最大的收获。
如果您已经熟悉其他语言,并且希望快速学习Python,那么 Python Tutorial 是一种很好的资源。
如果您是编程新手,并且想从python开始,那么下面的书可能对您有用:
创建项目¶
在开始抓取之前,你必须建立一个新的零碎项目。输入要在其中存储代码并运行的目录:
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 定义要发出的初始请求,可以选择如何跟踪页面中的链接,以及如何解析下载的页面内容以提取数据。
这是我们第一只蜘蛛的代码。将其保存在名为的文件中 quotes_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 = f'quotes-{page}.html'
with open(filename, 'wb') as f:
f.write(response.body)
self.log(f'Saved file{filename}')
如你所见,我们的蜘蛛子类 scrapy.Spider 并定义了一些属性和方法:
name :标识蜘蛛。它在一个项目中必须是唯一的,也就是说,不能为不同的蜘蛛设置相同的名称。
start_requests() :必须返回一个ITable of requests(您可以返回一个请求列表或编写一个生成器函数),蜘蛛将从中开始爬行。随后的请求将从这些初始请求中依次生成。
parse() :将调用的方法,用于处理为每个请求下载的响应。响应参数是的实例 TextResponse 它保存页面内容,并有进一步有用的方法来处理它。
这个 parse() 方法通常解析响应,将抓取的数据提取为dict,并查找新的URL以跟踪和创建新的请求。( Request 从他们那里。
如何运行我们的蜘蛛¶
要使蜘蛛正常工作,请转到项目的顶级目录并运行:
scrapy crawl quotes
此命令运行名为的spider 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) (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) (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 和 引用-2.HTML, 将各个URL的内容作为 parse 方法指示。
注解
如果您想知道为什么我们还没有解析HTML,请稍等,我们很快就会讨论这个问题。
引擎盖下面发生了什么?¶
Scrapy安排了 scrapy.Request 返回的对象 start_requests 蜘蛛的方法。在接收到每个响应时,它实例化 Response 对象并调用与请求关联的回调方法(在本例中,为 parse 方法)将响应作为参数传递。
启动请求方法的快捷方式¶
而不是执行 start_requests() 生成的方法 scrapy.Request 来自URL的对象,您只需定义 start_urls 具有URL列表的类属性。然后,此列表将由 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 = f'quotes-{page}.html'
with open(filename, 'wb') as f:
f.write(response.body)
这个 parse() 方法将被调用来处理这些URL的每个请求,即使我们没有明确地告诉Scrapy这样做。这是因为 parse() 是Scrapy的默认回调方法,对没有显式分配回调的请求调用该方法。
提取数据¶
学习如何使用scrappy提取数据的最佳方法是使用 Scrapy shell . 运行:
scrapy shell 'http://quotes.toscrape.com/page/1/'
注解
否则,在运行Scrapy命令时,请记住要在命令行中包含url。 & 字符)不起作用。
在Windows上,使用双引号:
scrapy shell "http://quotes.toscrape.com/page/1/"
您将看到类似的内容:
[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) (referer: None)
[s] Available Scrapy objects:
[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s] crawler
[s] item {}
[s] request
[s] response <200 http://quotes.toscrape.com/page/1/>
[s] settings
[s] spider
[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
使用shell,可以尝试使用 CSS 对于响应对象:
>>>response.css('title')
[]
运行``response.css('title')``的结果是一个类似于列表的对象:class:~scrapy.selector.SelectorList,它表示一个列表:class:`~scrapy.selector.Selector,这些对象环绕XML/HTML元素,并允许您运行进一步的查询,以细化所选内容或提取数据。
要从上述标题中提取文本,可以执行以下操作:
>>>response.css('title::text').getall()
['Quotes to Scrape']
这里有两件事需要注意:一是我们已经添加了 ::text 对于CSS查询,意味着我们只想直接选择内部的文本元素
元素。如果我们不指定 ::text ,我们将获得完整的title元素,包括其标记:>>>response.css('title').getall()
['
Quotes to Scrape']另一件事是呼叫的结果 .getall() 是一个列表:选择器可能返回多个结果,因此我们提取所有结果。当您知道您只想要第一个结果时,如本例所示,您可以:
>>>response.css('title::text').get()
'Quotes to Scrape'
作为替代,你可以写下:
>>>response.css('title::text')[0].get()
'Quotes to Scrape'
然而,使用 .get() 直接在A上 SelectorList 实例避免了 IndexError 回报 None 当它找不到任何与所选内容匹配的元素时。
这里有一个教训:对于大多数抓取代码,您希望它能够对由于在页面上找不到的东西而导致的错误具有弹性,这样即使某些部分无法抓取,您至少可以 some 数据。
>>>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选择器,您可能会发现在Web浏览器的shell中使用 view(response) . 您可以使用浏览器的开发人员工具检查HTML并找到一个选择器(请参见 使用浏览器的开发人员工具进行抓取 )
Selector Gadget 也是一个很好的工具,可以快速找到视觉上选中的元素的CSS选择器,它可以在许多浏览器中使用。
XPath: 简介¶
此外 CSS ,scrapy选择器也支持使用 XPath 表达:
>>>response.xpath('//title')
[]
>>>response.xpath('//title/text()').get()
'Quotes to Scrape'
XPath表达式是非常强大的,是抓取选择器的基础。实际上,CSS选择器在引擎盖下转换为xpath。如果仔细阅读shell中选择器对象的文本表示形式,可以看到这一点。
虽然可能不像CSS选择器那么流行,但xpath表达式提供了更多的功能,因为除了导航结构之外,它还可以查看内容。使用xpath,您可以选择如下内容:*选择包含文本“下一页”*的链接。这使得xpath非常适合于抓取任务,并且我们鼓励您学习xpath,即使您已经知道如何构造css选择器,它也会使抓取更加容易。
我们在这里不会涉及很多XPath,但你可以阅读更多关于:ref:在这里使用带有Scrapy选择器的XPath 。 要了解有关XPath的更多信息,我们建议`本教程通过示例学习XPath `_,以及`本教程学习“如何在XPath中思考 “`_。
提取引用和作者¶
既然您对选择和提取有了一些了解,那么让我们通过编写代码从网页中提取引号来完成蜘蛛程序。
Http://quotes.toscrape.com中的每个引号都由如下所示的HTML元素表示:
“The world as we have created it is a process of our
thinking. It cannot be changed without changing our thinking.”
by Albert Einstein
(about)
让我们打开Scrapy Shell并播放一点以了解如何提取所需数据:
$ scrapy shell 'http://quotes.toscrape.com'
我们得到了一个quote HTML元素的选择器列表,其中包括:
>>>response.css("div.quote")
[,
,
...]
上面查询返回的每个选择器都允许我们对其子元素运行进一步的查询。让我们将第一个选择器分配给一个变量,这样我们就可以直接在特定的引号上运行CSS选择器:
>>>quote = response.css("div.quote")[0]
现在,让我们提取 text , author 以及 tags 从引用中使用 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']
找到了如何提取每个位之后,我们现在可以迭代所有的quotes元素,并将它们放在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页面保存到一个本地文件中。让我们把上面的提取逻辑集成到蜘蛛中。
剪贴蜘蛛通常会生成许多字典,其中包含从页面中提取的数据。为此,我们使用 yield 回调中的python关键字,如下所示:
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(),
}
如果运行这个spider,它将用日志输出提取的数据:
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 exports ,使用以下命令::
scrapy crawl quotes -O quotes.json
这将生成一个``quotes.json``文件,其中包含所有已删除的项目,在`JSON`_中序列化。
这个 -O 命令行开关覆盖任何现有文件;使用 -o 而是将新内容附加到任何现有文件中。但是,附加到JSON文件会使文件内容无效JSON。附加到文件时,请考虑使用不同的序列化格式,例如 JSON Lines ::
scrapy crawl quotes -o quotes.jl
这个 JSON Lines 格式很有用,因为它类似于流,您可以很容易地向它附加新记录。当您运行两次时,它不存在相同的JSON问题。另外,由于每个记录都是单独的一行,因此您可以处理大文件,而不必将所有内容都放入内存中,因此有如下工具: JQ 以帮助在命令行中执行此操作。
在小项目中(如本教程中的项目),这就足够了。但是,如果您想对爬取的项目执行更复杂的操作,可以编写一个 Item Pipeline . 项目创建时已为您设置了项目管道的占位符文件,位于 tutorial/pipelines.py . 但是,如果只想存储爬取的项目,则不需要实现任何项目管道。
以下链接¶
比如说,你不需要从http://quotes.toscrape.com的前两页抓取内容,而是需要从网站上所有页面的引用。
既然您知道了如何从页面中提取数据,那么让我们看看如何从页面中跟踪链接。
第一件事是提取到我们要跟踪的页面的链接。检查我们的页面,我们可以看到有一个链接指向下一个带有以下标记的页面:
Next →
我们可以尝试在外壳中提取:
>>>response.css('li.next a').get()
'Next →'
这将获取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() 方法(因为链接可以是相对的),并生成对下一页的新请求,将自身注册为回调,以处理下一页的数据提取,并保持爬行在所有页中进行。
这里您看到的是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 只返回一个请求实例;您仍然需要生成这个请求。
也可以将选择器传递给 response.follow 而不是字符串;此选择器应提取必要的属性:
for href in response.css('ul.pager a::attr(href)'):
yield response.follow(href, callback=self.parse)
为了 元素有一个快捷方式: response.follow 自动使用其href属性。因此代码可以进一步缩短:
for a in response.css('ul.pager a'):
yield response.follow(a, callback=self.parse)
要从iterable创建多个请求,可以使用 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)
更多示例和模式¶
下面是另一个spider,它演示回调和以下链接,这次是为了抓取作者信息:
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 像我们以前看到的那样回拨。
这里,我们把回电传递给 response.follow_all 作为使代码更短的位置参数;它也适用于 Request .
这个 parse_author 回调定义了一个助手函数,用于从CSS查询中提取和清理数据,并用作者数据生成python dict。
这个蜘蛛展示的另一个有趣的事情是,即使同一作者引用了很多话,我们也不需要担心多次访问同一作者页面。默认情况下,scrappy过滤掉对已经访问过的URL的重复请求,避免了由于编程错误而太多地访问服务器的问题。这可以通过设置进行配置 DUPEFILTER_CLASS .
希望到目前为止,您已经很好地了解了如何使用scrappy跟踪链接和回调的机制。
作为另一个利用以下链接机制的蜘蛛示例,请查看 CrawlSpider 类,该类用于实现一个小规则引擎,您可以使用该引擎在上面编写爬虫程序。
另外,一个常见的模式是使用:ref:`trick将其他数据传递给回调`来构建包含来自多个页面的数据的项目。
使用蜘蛛参数¶
通过使用 -a 运行它们时的选项:
scrapy crawl quotes -O quotes-humor.json -a tag=humor
这些论点被传给蜘蛛 __init__ 方法并默认成为spider属性。
在本例中,为 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').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 标记,如 http://quotes.toscrape.com/tag/humor .
你可以:参考:在这里学习更多关于处理蜘蛛参数的信息。
下一步¶
本教程只介绍 Scrapy 的基础知识,但这里没有提到很多其他特性。检查:ref:`topics-whatelse`部分:ref:`intro-overview`一章,快速概述最重要的部分。
您可以继续阅读以下部分:ref:`section-basics`以了解有关命令行工具,蜘蛛,选择器以及本教程尚未涵盖的其他内容的更多信息,例如对已删除数据进行建模。 如果您更喜欢使用示例项目,请查看:ref:`intro-examples`部分。