Scrapy 教程¶
在本教程中,我们假设 Scrapy 已经安装在您的系统上。如果不是这种情况,请参阅安装指南。
我们将抓取quotes.toscrape.com,这是一个列出著名作家语录的网站。
本教程将引导您完成以下任务
创建一个新的 Scrapy 项目
编写一个Spider 来爬取网站并提取数据
使用命令行导出抓取的数据
更改 Spider 以递归方式跟随链接
使用 Spider 参数
Scrapy 使用Python编写。您对 Python 了解得越多,您就能从 Scrapy 中获得更多收益。
如果您已经熟悉其他语言并希望快速学习 Python,则Python 教程是一个很好的资源。
如果您是编程新手并希望从 Python 开始,以下书籍可能对您有所帮助
您还可以查看此 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
我们的第一个 Spider¶
Spider 是您定义的类,Scrapy 使用它们从网站(或一组网站)中抓取信息。它们必须是 Spider
的子类,并定义要发出的初始请求,以及可选地,如何在页面中跟随链接以及解析下载的页面内容以提取数据。
这是我们第一个 Spider 的代码。将其保存在名为 quotes_spider.py
的文件中,该文件位于项目中的 tutorial/spiders
目录下
from pathlib import Path
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
urls = [
"https://quotes.toscrape.com/page/1/",
"https://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"
Path(filename).write_bytes(response.body)
self.log(f"Saved file {filename}")
如您所见,我们的 Spider 是 scrapy.Spider
的子类,并定义了一些属性和方法
name
:标识 Spider。它在项目中必须是唯一的,也就是说,您不能为不同的 Spider 设置相同的名称。start_requests()
:必须返回一个 Request 可迭代对象(您可以返回一个请求列表或编写一个生成器函数),Spider 将从此处开始爬取。后续请求将从这些初始请求中依次生成。parse()
:一个将在为每个发出的请求下载响应时调用的方法。response 参数是TextResponse
的实例,它保存页面内容并具有更多有用的方法来处理它。parse()
方法通常解析响应,将抓取的数据提取为字典,并查找要跟随的新 URL,并从中创建新的请求(Request
)。
如何运行我们的 Spider¶
要让我们的 Spider 运行起来,请转到项目的顶级目录并运行
scrapy crawl quotes
此命令运行我们刚刚添加的名为 quotes
的 Spider,它将向 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 https://quotes.toscrape.com/robots.txt> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://quotes.toscrape.com/page/1/> (referer: None)
2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://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 的 scrapy.Request
对象返回的 start_requests
方法。在为每个请求收到响应后,它会实例化 Response
对象并调用与请求关联的回调方法(在本例中为 parse
方法),并将响应作为参数传递。
start_requests 方法的快捷方式¶
而不是实现一个 start_requests()
方法,该方法从 URL 生成 scrapy.Request
对象,您可以只定义一个 start_urls
类属性,其中包含 URL 列表。然后,此列表将由 start_requests()
的默认实现用于为您的 Spider 创建初始请求。
from pathlib import Path
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
]
def parse(self, response):
page = response.url.split("/")[-2]
filename = f"quotes-{page}.html"
Path(filename).write_bytes(response.body)
parse()
方法将被调用以处理这些 URL 的每个请求,即使我们没有明确告诉 Scrapy 这样做。这是因为 parse()
是 Scrapy 的默认回调方法,它用于没有显式分配回调的请求。
提取数据¶
学习如何使用 Scrapy 提取数据的最佳方法是使用Scrapy shell尝试选择器。运行
scrapy shell 'https://quotes.toscrape.com/page/1/'
注意
请记住,在从命令行运行 Scrapy shell 时始终将 URL 括在引号中,否则包含参数(即 &
字符)的 URL 将无法工作。
在 Windows 上,请改用双引号
scrapy shell "https://quotes.toscrape.com/page/1/"
您将看到类似以下内容
[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy.core.engine] DEBUG: Crawled (200) <GET https://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 https://quotes.toscrape.com/page/1/>
[s] response <200 https://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
使用 shell,您可以尝试使用 response 对象的 CSS 选择元素
>>> response.css("title")
[<Selector query='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
运行 response.css('title')
的结果是一个称为 SelectorList
的类似列表的对象,它表示一个 Selector
对象列表,这些对象围绕 XML/HTML 元素,并允许您运行进一步的查询以细化选择或提取数据。
要提取上面标题的文本,您可以执行以下操作
>>> response.css("title::text").getall()
['Quotes to Scrape']
这里需要注意两点:一是我们在 CSS 查询中添加了 ::text
,表示我们只想选择 <title>
元素内部的文本元素。如果我们不指定 ::text
,我们将获得完整的标题元素,包括其标签
>>> 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
实例上访问索引将在没有结果时引发 IndexError
异常
>>> response.css("noelement")[0].get()
Traceback (most recent call last):
...
IndexError: list index out of range
您可能希望直接在SelectorList
实例上使用.get()
,如果没有任何结果,它将返回None
。
>>> response.css("noelement").get()
这里有一个教训:对于大多数抓取代码,您希望它能够抵御因页面上找不到内容而导致的错误,这样即使某些部分抓取失败,您至少也可以获取一些数据。
除了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 浏览器中打开 shell 中的响应页面很有用。您可以使用浏览器的开发者工具检查 HTML 并想出一个选择器(参见使用浏览器的开发者工具进行抓取)。
Selector Gadget 也是一个很好的工具,可以快速找到视觉选择元素的 CSS 选择器,它可以在许多浏览器中使用。
XPath:简要介绍¶
除了CSS 之外,Scrapy 选择器还支持使用XPath 表达式。
>>> response.xpath("//title")
[<Selector query='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath("//title/text()").get()
'Quotes to Scrape'
XPath 表达式非常强大,是 Scrapy 选择器的基础。事实上,CSS 选择器在后台会转换为 XPath。如果您仔细阅读 shell 中选择器对象的文本表示形式,就可以看到这一点。
虽然可能不如 CSS 选择器流行,但 XPath 表达式提供了更强大的功能,因为它除了导航结构之外,还可以查看内容。使用 XPath,您可以选择诸如:包含文本“下一页”的链接之类的东西。这使得 XPath 非常适合抓取任务,我们鼓励您学习 XPath,即使您已经知道如何构建 CSS 选择器,它也会使抓取变得更容易。
我们在这里不会过多介绍 XPath,但您可以在此处阅读有关使用 Scrapy 选择器与 XPath 的更多信息。要了解有关 XPath 的更多信息,我们推荐此教程通过示例学习 XPath,以及此教程学习“如何用 XPath 思考”。
在我们的蜘蛛中提取数据¶
让我们回到我们的蜘蛛。到目前为止,它还没有提取任何特定数据,只是将整个 HTML 页面保存到本地文件。让我们将上面的提取逻辑集成到我们的蜘蛛中。
Scrapy 蜘蛛通常会生成许多包含从页面提取的数据的字典。为此,我们使用回调中的yield
Python 关键字,如下所示。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://quotes.toscrape.com/page/1/",
"https://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(),
}
要运行此蜘蛛,请通过输入以下命令退出 scrapy shell:
quit()
然后,运行:
scrapy crawl quotes
现在,它应该使用日志输出提取的数据。
2016-09-19 18:57:19 [scrapy.core.scraper] DEBUG: Scraped from <200 https://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 https://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 格式序列化。
-O
命令行开关会覆盖任何现有文件;请改用-o
将新内容追加到任何现有文件。但是,将内容追加到 JSON 文件会使文件内容成为无效的 JSON。在追加到文件时,请考虑使用不同的序列化格式,例如JSON Lines。
scrapy crawl quotes -o quotes.jsonl
JSON Lines 格式很有用,因为它类似于流,因此您可以轻松地向其中追加新记录。当您运行两次时,它不会像 JSON 那样出现相同的问题。此外,由于每个记录都是一行,因此您可以处理大型文件,而无需将所有内容都放入内存中,有一些工具(如JQ)可以在命令行中帮助执行此操作。
在小型项目(例如本教程中的项目)中,这应该就足够了。但是,如果您想对抓取的项目执行更复杂的操作,则可以编写一个项目管道。在创建项目时,已为您设置了一个项目管道的占位符文件,位于tutorial/pipelines.py
中。但是,如果您只想存储抓取的项目,则无需实现任何项目管道。
跟随链接¶
假设您不仅要抓取https://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>'
这将获取锚元素,但我们想要href
属性。为此,Scrapy 支持一个 CSS 扩展,它允许您选择属性内容,如下所示:
>>> response.css("li.next a::attr(href)").get()
'/page/2/'
还有一个可用的attrib
属性(有关详细信息,请参见选择元素属性)。
>>> response.css("li.next a").attrib["href"]
'/page/2/'
现在让我们看看我们的蜘蛛,它经过修改,可以递归地跟随指向下一页的链接,并从中提取数据。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://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 跟随链接的机制:当您在回调方法中生成 Request 时,Scrapy 将安排发送该请求并注册一个回调方法,在该请求完成后执行该回调方法。
使用此方法,您可以构建复杂的爬虫,根据您定义的规则跟随链接,并根据访问的页面提取不同类型的数据。
在我们的示例中,它创建了一种循环,跟随所有指向下一页的链接,直到找不到链接为止——这对于抓取博客、论坛和其他具有分页功能的网站非常方便。
创建 Request 的快捷方式¶
作为创建 Request 对象的快捷方式,您可以使用response.follow
。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
start_urls = [
"https://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 实例;您仍然必须生成此 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 = ["https://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 字典。
此 Spider 另一个有趣的地方在于,即使同一作者有多个语录,我们也不需要担心多次访问同一个作者页面。默认情况下,Scrapy 会过滤掉对已访问 URL 的重复请求,避免因编程错误而过度访问服务器。这可以通过 DUPEFILTER_CLASS
设置进行配置。
希望到目前为止,您已经很好地理解了如何在 Scrapy 中使用链接和回调函数的机制。
作为另一个利用链接和回调函数机制的 Spider 示例,请查看 CrawlSpider
类,它是一个通用的 Spider,实现了小型规则引擎,您可以利用它来构建自己的爬虫。
此外,一种常见的模式是使用来自多个页面的数据构建一个 Item,利用将额外数据传递给回调函数的技巧。
使用 Spider 参数¶
您可以使用 -a
选项在运行 Spider 时向其提供命令行参数。
scrapy crawl quotes -O quotes-humor.json -a tag=humor
这些参数默认情况下会传递到 Spider 的 __init__
方法,并成为 Spider 属性。
在此示例中,为 tag
参数提供的值可以通过 self.tag
访问。您可以使用它来使您的 Spider 仅抓取具有特定标签的语录,并根据参数构建 URL。
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
def start_requests(self):
url = "https://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)
如果您向此 Spider 传递 tag=humor
参数,您会注意到它只会访问 humor
标签下的 URL,例如 https://quotes.toscrape.com/tag/humor
。
后续步骤¶
本教程仅涵盖了 Scrapy 的基础知识,但还有许多其他功能在此处未提及。请查看 其他内容? 部分,该部分位于 Scrapy 概览 章节中,可以快速了解最重要的功能。
您可以从 基本概念 部分继续学习,以了解更多关于命令行工具、Spider、选择器和其他教程未涵盖的内容,例如对抓取的数据进行建模。如果您更喜欢使用示例项目进行操作,请查看 示例 部分。