在本教程中,我们假设您已经安装了Scrapy。如果没有,请参阅安装指南。
我们将要抓取 quotes.toscrape.com,一个列出著名作家的名言(quote)的网站。
本教程将引导您完成以下任务:
- 创建一个新的 Scrapy 项目
- 编写一个爬虫来爬取站点并提取数据
- 使用命令行导出抓取的数据
- 改写爬虫以递归地跟踪链接
- 使用爬虫参数
Scrapy 是用 Python 编写的。如果你没学过 Python,你可能需要了解一下这个语言,以充分利用 Scrapy。
如果您已经熟悉其他语言,并希望快速学习 Python,我们建议您阅读 Dive Into Python 3。或者,您可以学习 Python 教程。
如果您刚开始编程,并希望从 Python 开始,在线电子书《Learn Python The Hard Way》非常有用。您也可以查看非程序员的 Python 资源列表。
创建一个项目
在开始抓取之前,您必须创建一个新的 Scrapy 项目。 进入您要存储代码的目录,然后运行:
scrapy startproject tutorial
这将创建一个包含以下内容的 tutorial 目录:
tutorial/ scrapy.cfg # 项目配置文件 tutorial/ # 项目的 Python 模块,放置您的代码的地方 __init__.py items.py # 项目项(item)定义文件 pipelines.py # 项目管道(piplines)文件 settings.py # 项目设置文件 spiders/ # 一个你以后会放置 spider 的目录 __init__.py
第一个爬虫
Spider 是您定义的类,Scrapy 用它从网站(或一组网站)中抓取信息。 他们必须是 scrapy.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。 它在项目中必须是唯一的,也就是说,您不能为不同的 Spider 设置相同的名称。
- start_requests():必须返回一个 Requests 的迭代(您可以返回一个 requests 列表或者写一个生成器函数),Spider 将从这里开始抓取。 随后的请求将从这些初始请求连续生成。
- parse():用来处理每个请求得到的响应的方法。 响应参数是 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)(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 和 quotes-2.html,其中包含各个URL的内容,就像我们的 parse 方法指示一样。
注意
如果您想知道为什么我们还没有解析 HTML,请继续,我们将尽快介绍。
这个过程中发生了什么?
Spider 的 start_requests 方法返回 scrapy.Request 对象,Scrapy 对其发起请求 。然后将收到的响应实例化为 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 的请求,即使我们没有明确告诉 Scrapy 这样做。 这是因为 parse() 是 Scrapy 的默认回调方法,没有明确分配回调方法的请求默认调用此方法。
提取数据
学习如何使用 Scrapy 提取数据的最佳方式是在 Scrapy shell 中尝试一下选择器。 运行:
scrapy shell 'http://quotes.toscrape.com/page/1/'
注意
在从命令行运行 Scrapy shell 时必须给 url 加上引号,否则包含参数(例如 &符号)的 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 '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') []
运行 response.css('title') 返回的结果是一个 SelectorList 类列表对象,它是一个指向 XML/HTML 元素的 Selector 对象的列表,允许您进行进一步的查询来细分选择或提取数据。
要从上面的 title 中提取文本,您可以执行以下操作:
>>> response.css('title::text').extract() ['Quotes to Scrape']
这里有两件事情要注意:一个是我们在 CSS 查询中添加了 ::text,这意味着我们只想要
>>> response.css('title').extract() ['Quotes to Scrape ']
另一件事是调用 .extract() 返回的结果是一个列表,因为我们在处理 SelectorList。 当你明确你只是想要第一个结果时,你可以这样做:
>>> response.css('title::text').extract_first() 'Quotes to Scrape'
或者你可以这样写:
>>> response.css('title::text')[0].extract() 'Quotes to Scrape'
但是,如果没有找到匹配选择的元素,.extract_first() 返回 None,避免了 IndexError
这里有一个教训:对于大多数爬虫代码,您希望它具有容错性,如果在页面上找不到指定的元素导致无法获取某些项,至少其它的数据可以被抓取。
除了 extract() 和 extract_first() 方法之外,还可以使用 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 选择器语句,您可以在浏览器中打开页面并查看源代码。 您也可以使用浏览器的开发工具或扩展(如 Firebug)(请参阅有关 Using Firebug for scraping 和 Using Firefox for scraping 的部分)。
Selector Gadget 也是一个很好的工具,可以快速找到元素的 CSS 选择器语句,它可以在许多浏览器中运行。
XPath:简要介绍
除了 CSS,Scrapy 选择器还支持使用 XPath 表达式:
>>> response.xpath('//title') [] >>> response.xpath('//title/text()').extract_first() 'Quotes to Scrape'
XPath 表达式非常强大,是 Scrapy 选择器的基础。 实际上,如果你查看相关的源代码就可以发现,CSS 选择器被转换为 XPath。
虽然也许不像 CSS 选择器那么受欢迎,但 XPath 表达式提供更多的功能,因为除了导航结构之外,它还可以查看内容。 使用 XPath,您可以选择以下内容:包含文本“下一页”的链接。 这使得 XPath 非常适合抓取任务,我们鼓励您学习 XPath,即使您已经知道如何使用 CSS 选择器,这会使抓取更容易。
我们不会在这里讲太多关于 XPath 的内容,但您可以阅读 using XPath with Scrapy Selectors 获取更多有关 XPath 的信息。 我们推荐教程 to learn XPath through examples,和教程 “how to think in XPath”。
提取名人和名言
现在你知道了如何选择和提取,让我们来完成我们的爬虫,编写代码从网页中提取名言(quote)。
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 Einsteinsmall> <a href="/author/Albert-Einstein">(about)a> span> <div class="tags"> Tags: <a class="tag" href="/tag/change/page/1/">changea> <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughtsa> <a class="tag" href="/tag/thinking/page/1/">thinkinga> <a class="tag" href="/tag/world/page/1/">worlda> div> div>
让我们打开 scrapy shell 玩一玩,找到提取我们想要的数据的方法:
$ scrapy shell 'http://quotes.toscrape.com'
得到 quote 元素的 selector 列表:
>>> response.css("div.quote")
通过上述查询返回的每个 selector 允许我们对其子元素运行进一步的查询。 让我们将第一个 selector 分配给一个变量,以便我们可以直接在特定的 quote 上运行我们的 CSS 选择器:
>>> quote = response.css("div.quote")[0]
现在,我们使用刚刚创建的 quote 对象,从该 quote 中提取 title,author 和 tags:
>>> title = quote.css("span.text::text").extract_first() >>> title '“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").extract_first() >>> author 'Albert Einstein'
鉴于标签是字符串列表,我们可以使用 .extract() 方法将它们全部提取出来:
>>> tags = quote.css("div.tags a.tag::text").extract() >>> tags ['change', 'deep-thoughts', 'thinking', 'world']
现在已经弄清楚了如何提取每一个信息,接下来遍历所有 quote 元素,并把它们放在一个 Python 字典中:
>>> for quote in response.css("div.quote"): ... text = quote.css("span.text::text").extract_first() ... author = quote.css("small.author::text").extract_first() ... tags = quote.css("div.tags a.tag::text").extract() ... print(dict(text=text, author=author, tags=tags)) {'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'} {'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'} ... a few more of these, omitted for brevity >>>
在爬虫中提取数据
让我们回到我们的爬虫上。 到目前为止,它并没有提取任何数据,只将整个 HTML 页面保存到本地文件。 让我们将上述提取逻辑整合到我们的爬虫中。
Scrapy 爬虫通常生成许多包含提取到的数据的字典。 为此,我们在回调方法中使用 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').extract_first(), 'author': quote.css('small.author::text').extract_first(), 'tags': quote.css('div.tags a.tag::text').extract(), }
如果您运行此爬虫,它将输出提取的数据与日志:
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 序列化的数据。
由于历史原因,Scrapy 追加内容到给定的文件,而不是覆盖其内容。 如果您在第二次之前删除该文件两次运行此命令,那么最终会出现一个破坏的 JSON 文件。您还可以使用其他格式,如 JSON 行(JSON Lines):
scrapy crawl quotes -o quotes.jl
JSON 行格式很有用,因为它像流一样,您可以轻松地将新记录附加到文件。 当运行两次时,它不会发生 JSON 那样的问题。 另外,由于每条记录都是单独的行,所以您在处理大文件时无需将所有内容放到内存中,还有 JQ 等工具可以帮助您在命令行中执行此操作。
在小项目(如本教程中的一个)中,这应该是足够的。 但是,如果要使用已抓取的项目执行更复杂的操作,则可以编写项目管道(Item Pipeline)。 在工程的创建过程中已经为您创建了项目管道的占位符文件 tutorial/pipelines.py, 虽然您只需要存储已抓取的项目,不需要任何项目管道。
跟踪链接
或许你希望获取网站所有页面的 quotes,而不是从 http://quotes.toscrape.com 的前两页抓取。
现在您已经知道如何从页面中提取数据,我们来看看如何跟踪链接。
首先是提取我们想要跟踪的页面的链接。 检查我们的页面,我们可以看到链接到下一个页面的URL在下面的元素中:
<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').extract_first() 'Next '
这得到了超链接元素,但是我们需要其属性 href。 为此,Scrapy 支持 CSS 扩展,您可以选择属性内容,如下所示:
>>> response.css('li.next a::attr(href)').extract_first() '/page/2/'
现在修改我们的爬虫,改为递归地跟踪下一页的链接,从中提取数据:
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').extract_first(), 'author': quote.css('small.author::text').extract_first(), 'tags': quote.css('div.tags a.tag::text').extract(), } next_page = response.css('li.next a::attr(href)').extract_first() if next_page is not None: next_page = response.urljoin(next_page) yield scrapy.Request(next_page, callback=self.parse)
现在,在提取数据之后,parse() 方法查找到下一页的链接,使用 urljoin() 方法构建一个完整的绝对 URL(因为链接可以是相对的),并生成(yield)一个到下一页的新的请求, 其中包括回调方法(parse)。
您在这里看到的是 Scrapy 的链接跟踪机制:当您在一个回调方法中生成(yield)请求(request)时,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').extract_first(), 'author': quote.css('span small::text').extract_first(), 'tags': quote.css('div.tags a.tag::text').extract(), } next_page = response.css('li.next a::attr(href)').extract_first() 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('li.next a::attr(href)'): yield response.follow(href, callback=self.parse)
对于元素,有一个快捷方式:response.follow 自动使用它们的 href 属性。 所以代码可以进一步缩短:
for a in response.css('li.next a'): yield response.follow(a, callback=self.parse)
注意
response.follow(response.css('li.next a')) 无效,因为 response.css 返回的是一个类似列表的对象,其中包含所有结果的选择器,而不是单个选择器。for 循环或者 response.follow(response.css('li.next a')[0]) 则可以正常工作。
更多的例子和模式
这是另外一个爬虫,示例了回调和跟踪链接,这次是为了抓取作者信息:
import scrapy class AuthorSpider(scrapy.Spider): name = 'author' start_urls = ['http://quotes.toscrape.com/'] def parse(self, response): # 链接到作者页面 for href in response.css('.author + a::attr(href)'): yield response.follow(href, self.parse_author) # 链接到下一页 for href in response.css('li.next a::attr(href)'): yield response.follow(href, self.parse) def parse_author(self, response): def extract_with_css(query): return response.css(query).extract_first().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,这样代码更短,也可以传递给 scrapy.Request。
parse_author 回调方法里定义了另外一个函数来根据 CSS 查询语句(query)来提取数据,然后生成包含作者数据的 Python 字典。
这个爬虫演示的另一个有趣的事是,即使同一作者有许多名言,我们也不用担心多次访问同一作者的页面。默认情况下,Scrapy 会将重复的请求过滤出来,避免了由于编程错误而导致的重复服务器的问题。可以通过 DUPEFILTER_CLASS 进行相关的设置。
希望现在您已经了解了 Scrapy 的跟踪链接和回调方法机制。
CrawlSpider 类是一个小规模的通用爬虫引擎,只需要修改其跟踪链接的机制等,就可以在它之上实现你自己的爬虫程序。
另外,一个常见的模式是从多个页面据构建一个包含数据的项(item),有一个将附加数据传递给回调方法的技巧。
使用爬虫参数
在运行爬虫时,可以通过 -a 选项为您的爬虫提供命令行参数:
scrapy crawl quotes -o quotes-humor.json -a tag=humor
默认情况下,这些参数将传递给 Spider 的 __init__ 方法并成为爬虫的属性。
在此示例中,通过 self.tag 获取命令行中参数 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').extract_first(), 'author': quote.css('small.author::text').extract_first(), } next_page = response.css('li.next a::attr(href)').extract_first() 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 中的“还有什么?”部分可以快速了解有哪些重要的内容。
您可以通过目录了解更多有关命令行工具、爬虫、选择器以及本教程未涵盖的其他内容的信息。下一章是示例项目。