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 的代码。将其保存在项目目录
下名为 tutorial/spiders
的文件中quotes_spider.py
from pathlib import Path
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
async def start(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()
:必须是一个异步生成器,用于产生供 spider 开始抓取的请求(以及可选的 item)。后续请求将从这些初始请求中连续生成。parse()
:一个将被调用的方法,用于处理每个请求下载的响应。response 参数是TextResponse
的一个实例,它包含页面内容并具有其他有用的方法来处理它。parse()
方法通常会解析响应,将抓取到的数据提取为字典,并找到要跟踪的新 URL 并从中创建新的请求(Request
)。
如何运行我们的 spider
要运行我们的 spider,请进入项目的顶层目录并运行
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) <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 发送由 start()
spider 方法产生的第一个 scrapy.Request
对象。在收到每个请求的响应后,Scrapy 会调用与请求关联的回调方法(在本例中是
方法),并传入一个 parse
Response
对象。
start
方法的快捷方式
start
您可以不实现一个产生来自 URL 的 Request
对象的 start()
方法,而是定义一个带有 URL 列表的 start_urls
类属性。然后,这个列表将由 start()
的默认实现用来创建您的 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)
即使我们没有明确告诉 Scrapy 这样做,parse()
方法也会被调用来处理这些 URL 的每个请求。发生这种情况是因为 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,您可以使用响应对象尝试使用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 选择器,您可能会发现在 shell 中使用
在 Web 浏览器中打开响应页面很有用。您可以使用浏览器的开发者工具检查 HTML 并找出选择器(参阅使用浏览器的开发者工具进行抓取)。view(response)
Selector Gadget 也是一个很好的工具,可以快速查找视觉选择元素的 CSS 选择器,它在许多浏览器中都能工作。
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 非常适合抓取任务,我们鼓励您即使已经知道如何构建 CSS 选择器,也学习 XPath,这将使抓取变得容易得多。
我们在这里不会过多介绍 XPath,但您可以在此处阅读更多关于将 XPath 与 Scrapy 选择器一起使用的内容。要了解更多关于 XPath 的信息,我们推荐这个通过示例学习 XPath 的教程,以及这个学习“如何用 XPath 思考”的教程。
在我们的 spider 中提取数据
让我们回到我们的 spider。到目前为止,它还没有特别提取任何数据,只是将整个 HTML 页面保存到本地文件。让我们将上面的提取逻辑集成到我们的 spider 中。
Scrapy spider 通常会生成许多包含从页面提取的数据的字典。为此,我们在回调中使用 Python 关键字
,如下所示yield
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(),
}
要运行此 spider,请通过输入以下内容退出 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
这将生成一个
文件,其中包含所有抓取到的 item,以JSON格式序列化。quotes.json
命令行选项
会覆盖任何现有文件;请改用 -O
将新内容追加到现有文件中。但是,追加到 JSON 文件会使文件内容无效 JSON。追加文件时,请考虑使用不同的序列化格式,例如JSON Lines-o
scrapy crawl quotes -o quotes.jsonl
JSON Lines 格式很有用,因为它像流一样,因此您可以轻松地向其中追加新记录。当您运行两次时,它不会像 JSON 那样出现问题。此外,由于每个记录都是单独一行,您可以处理大文件而无需将所有内容都放入内存,有一些工具如JQ可以在命令行中帮助完成此操作。
在小型项目(例如本教程中的项目)中,这应该足够了。但是,如果您想对抓取到的 item 执行更复杂的操作,您可以编写一个Item Pipeline。创建项目时,已经在
中为您设置了一个 Item Pipelines 的占位文件。尽管如果您只想存储抓取到的 item,则无需实现任何 Item Pipeline。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>'
这获取了 anchor 元素,但我们想要属性
。为此,Scrapy 支持一个 CSS 扩展,允许您选择属性内容,如下所示href
>>> 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 = [
"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 跟踪链接的机制:当您在回调方法中 yield 一个 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 不同,
直接支持相对 URL - 无需调用 urljoin。请注意,response.follow
仅返回一个 Request 实例;您仍然需要 yield 这个 Request。response.follow
您还可以将选择器而不是字符串传递给
;此选择器应提取必要的属性response.follow
for href in response.css("ul.pager a::attr(href)"):
yield response.follow(href, callback=self.parse)
对于
元素有一个快捷方式:<a>
会自动使用它们的 href 属性。因此代码可以进一步缩短response.follow
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)
更多示例和模式
这里是另一个演示回调和跟踪链接的 spider,这次用于抓取作者信息
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"),
}
这个 spider 将从主页开始,它将跟踪所有指向作者页面的链接,并为每个链接调用
回调,同时也会像我们之前看到的那样,使用 parse_author
回调跟踪分页链接。parse
这里我们将回调作为位置参数传递给 response.follow_all
,以使代码更短;这也适用于 Request
。
回调定义了一个辅助函数,用于从 CSS 查询中提取和清理数据,并生成包含作者数据的 Python 字典。parse_author
这个 spider 演示的另一个有趣的事情是,即使同一作者有许多引言,我们也不必担心多次访问同一作者页面。默认情况下,Scrapy 会过滤掉已访问过的 URL 的重复请求,从而避免因编程错误而过度访问服务器的问题。这可以在 DUPEFILTER_CLASS
设置中配置。
希望到目前为止,您已经对如何使用 Scrapy 的跟踪链接和回调机制有了很好的理解。
作为另一个利用跟踪链接机制的 spider 示例,请查看 CrawlSpider
类,它是一个通用的 spider,实现了一个小型规则引擎,您可以在其之上编写爬虫。
此外,一种常见模式是使用传递额外数据给回调函数的方法,从多个页面构建一个 item。
使用 spider 参数
您可以在运行 spider 时使用
选项为它们提供命令行参数-a
scrapy crawl quotes -O quotes-humor.json -a tag=humor
这些参数默认会传递给 Spider 的
方法,并成为 spider 的属性。__init__
在此示例中,为
参数提供的值将通过 tag
获取。您可以使用此方法让您的 spider 仅抓取具有特定标签的引言,并根据参数构建 URLself.tag
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes"
async def start(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
标签的 URL,例如 humor
。https://quotes.toscrape.com/tag/humor
下一步
本教程仅涵盖了 Scrapy 的基础知识,但还有许多此处未提及的其他功能。请查看还有哪些?部分在Scrapy 概览章节中,快速了解最重要的功能。
您可以从基本概念部分继续学习,了解更多关于命令行工具、spiders、选择器以及本教程未涵盖的其他内容,例如抓取数据的建模。如果您喜欢使用示例项目,请查看示例部分。