Scrapy教程¶
在本教程中,我们假设Scrapy已经安装在您的系统上。 如果尚未安装,请参阅安装指南。
我们将要抓取quotes.toscrape.com,这是一个列出著名作家名言的网站。
本教程将引导您完成以下任务:
创建一个新的Scrapy项目
编写一个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
我们的第一个爬虫¶
蜘蛛是您定义的类,Scrapy 使用它们从网站(或一组网站)中抓取信息。它们必须继承 Spider 并定义初始请求,还可以选择如何跟随页面中的链接并解析下载的页面内容以提取数据。
这是我们第一个Spider的代码。将其保存在项目中的tutorial/spiders目录下,文件名为quotes_spider.py:
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(): 必须返回一个可迭代的请求(你可以返回一个请求列表或编写一个生成器函数),蜘蛛将从这些请求开始爬取。后续的请求将从这些初始请求中依次生成。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 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 的 start_requests 方法返回的 scrapy.Request 对象。在接收到每个请求的响应后,它会实例化 Response 对象,并调用与请求关联的回调方法(在这种情况下,是 parse 方法),将响应作为参数传递。
start_requests方法的快捷方式¶
与其实现一个start_requests()方法
从URL生成scrapy.Request对象,
你可以简单地定义一个start_urls类属性
包含一个URL列表。然后这个列表将被start_requests()的默认实现使用
来为你的爬虫创建初始请求。
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,你可以尝试使用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,这意味着我们只想选择直接位于元素内的文本元素。如果我们不指定::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)在网页浏览器中打开来自shell的响应页面很有用。你可以使用浏览器的开发者工具来检查HTML并想出一个选择器(参见使用浏览器的开发者工具进行抓取)。
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,您可以选择诸如:包含文本“Next Page”的链接。这使得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 exports,使用以下命令:
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 可以帮助在命令行中完成这一操作。
在小型项目(如本教程中的项目)中,这应该足够了。
但是,如果您想对抓取的项目执行更复杂的操作,
您可以编写一个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>'
这将获取锚元素,但我们想要的是属性 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的链接跟踪机制:当你在回调方法中生成一个请求时,Scrapy会安排发送该请求,并注册一个回调方法在该请求完成时执行。
使用这个,你可以构建复杂的爬虫,根据你定义的规则跟随链接,并根据访问的页面提取不同类型的数据。
在我们的示例中,它创建了一种循环,跟随所有链接到下一页,直到找不到为止——这对于爬取博客、论坛和其他具有分页的站点非常方便。
创建请求的快捷方式¶
作为创建请求对象的快捷方式,您可以使用
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实例;你仍然需要yield这个Request。
你也可以传递一个选择器给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)
要从一个可迭代对象创建多个请求,你可以使用
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 字典。
这个爬虫展示的另一个有趣的事情是,即使有来自同一作者的许多引用,我们也不需要担心多次访问同一作者页面。默认情况下,Scrapy会过滤掉已经访问过的URL的重复请求,避免了由于编程错误而过度访问服务器的问题。这可以在DUPEFILTER_CLASS设置中进行配置。
希望到现在你已经很好地理解了如何使用Scrapy中的链接跟随和回调机制。
作为另一个利用链接跟随机制的示例蜘蛛,请查看CrawlSpider类,这是一个通用的蜘蛛,它实现了一个小型规则引擎,您可以在其上编写您的爬虫。
此外,一个常见的模式是使用来自多个页面的数据构建一个项目, 使用一个技巧将额外数据传递给回调函数。
使用爬虫参数¶
你可以通过在运行爬虫时使用 -a 选项来提供命令行参数给你的爬虫:
scrapy crawl quotes -O quotes-humor.json -a tag=humor
这些参数被传递给Spider的__init__方法,并默认成为spider的属性。
在这个例子中,为 tag 参数提供的值将通过 self.tag 可用。你可以使用这个来让你的爬虫只抓取带有特定标签的引用,基于参数构建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)
如果你向这个爬虫传递tag=humor参数,你会注意到它只会访问来自humor标签的URL,例如https://quotes.toscrape.com/tag/humor。
下一步¶
本教程仅涵盖了Scrapy的基础知识,但还有许多其他功能未在此提及。请查看还有什么?部分,在Scrapy概览章节中快速了解最重要的功能。
你可以从基本概念部分继续了解更多关于命令行工具、爬虫、选择器以及教程中未涵盖的其他内容,例如建模抓取的数据。如果你更喜欢通过示例项目来学习,请查看示例部分。