在本教程中,咱们假设您已经安装了Scrapy。若是没有,请参阅安装指南。css
咱们将要抓取 quotes.toscrape.com,一个列出著名做家的名言(quote)的网站。html
本教程将引导您完成如下任务:python
Scrapy 是用 Python 编写的。若是你没学过 Python,你可能须要了解一下这个语言,以充分利用 Scrapy。程序员
若是您已经熟悉其余语言,并但愿快速学习 Python,咱们建议您阅读 Dive Into Python 3。或者,您能够学习 Python 教程。正则表达式
若是您刚开始编程,并但愿从 Python 开始,在线电子书《Learn Python The Hard Way》很是有用。您也能够查看非程序员的 Python 资源列表。shell
在开始抓取以前,您必须建立一个新的 Scrapy 项目。 进入您要存储代码的目录,而后运行:编程
scrapy startproject tutorial
这将建立一个包含如下内容的 tutorial 目录:json
tutorial/ scrapy.cfg # 项目配置文件 tutorial/ # 项目的 Python 模块,放置您的代码的地方 __init__.py items.py # 项目项(item)定义文件 pipelines.py # 项目管道(piplines)文件 settings.py # 项目设置文件 spiders/ # 一个你之后会放置 spider 的目录 __init__.py
Spider 是您定义的类,Scrapy 用它从网站(或一组网站)中抓取信息。 他们必须是 scrapy.Spider 的子类并定义初始请求,和如何获取要继续抓取的页面的连接,以及如何解析下载的页面来提取数据。api
这是咱们第一个爬虫的代码。 将其保存在项目中的 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 的子类并定义了一些属性和方法:
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 http://quotes.toscrape.com/robots.txt> (referer: None) 2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None) 2016-12-16 21:24:05 [scrapy.core.engine] DEBUG: Crawled (200) <GET http://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,请继续,咱们将尽快介绍。
Spider 的 start_requests 方法返回 scrapy.Request 对象,Scrapy 对其发起请求 。而后将收到的响应实例化为 Response 对象,以响应为参数调用请求对象中定义的回调方法(在这里为 parse 方法)。
用于代替实现一个从 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) <GET http://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 http://quotes.toscrape.com/page/1/> [s] response <200 http://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 xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
运行 response.css('title') 返回的结果是一个 SelectorList 类列表对象,它是一个指向 XML/HTML 元素的 Selector 对象的列表,容许您进行进一步的查询来细分选择或提取数据。
要从上面的 title 中提取文本,您能够执行如下操做:
>>> response.css('title::text').extract() ['Quotes to Scrape']
这里有两件事情要注意:一个是咱们在 CSS 查询中添加了 ::text,这意味着咱们只想要 <title> 元素中的文本。 若是咱们不指定 ::text,咱们将获得完整的 title 元素,包括其标签:
>>> response.css('title').extract() ['<title>Quotes to Scrape</title>']
另外一件事是调用 .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 选择器语句,它能够在许多浏览器中运行。
除了 CSS,Scrapy 选择器还支持使用 XPath 表达式:
>>> response.xpath('//title') [<Selector xpath='//title' data='<title>Quotes to Scrape</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 Einstein</small> <a href="/author/Albert-Einstein">(about)</a> </span> <div class="tags"> Tags: <a class="tag" href="/tag/change/page/1/">change</a> <a class="tag" href="/tag/deep-thoughts/page/1/">deep-thoughts</a> <a class="tag" href="/tag/thinking/page/1/">thinking</a> <a class="tag" href="/tag/world/page/1/">world</a> </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() '<a href="/page/2/">Next <span aria-hidden="true">→</span></a>'
这获得了超连接元素,可是咱们须要其属性 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)
对于<a>元素,有一个快捷方式: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 中的“还有什么?”部分能够快速了解有哪些重要的内容。
您能够经过目录了解更多有关命令行工具、爬虫、选择器以及本教程未涵盖的其余内容的信息。下一章是示例项目。