Scrapy教程¶
在本教程中,我们假设Scrapy已经安装在你的系统上。 如若不然,请参考 安装指南 。
我们打算爬取quotes.toscrape.com,一个列出名人名言的网站。
本篇教程中将带您完成下列任务:
- 创建一个Scrapy项目
- 编写一只spider爬取网站并提取数据
- 使用命令行导出爬取的数据
- 修改spider来递归地跟踪链接
- 使用spider的参数
Scrapy是用Python编写的。如果你是这个语言的新学者,你可能想通过获取该语言是什么样子来开始,以从Scrapy学到最多。
如果你已经熟悉其他语言,并且想要快速学习Python,我们推荐阅读Dive Into Python 3。另外,你可以依照Python 教程。
如果你是编程的新人,并要以Python 开始,你会发现有用的在线图书Learn Python The Hard Way。你还可以看看这个给非程序员的Python资源列表。
创建一个项目 ¶
在开始爬取之前,你必须创建一个新的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
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
目录下的名为dmoz_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()
:必须返回Request的一个可迭代对象(你可以返回一个请求的列表或者编写一个生成器函数),Spider将从它们开始爬取。后续请求将从这些初始的请求连续生成。parse()
:一个方法,将用来处理发出的每个请求所下载的响应。response参数是TextResponse
的一个实例,它保存页面内容并具有进一步处理它的有用方法。parse()
方法通常解析response,提取爬取的数据为字典,找出跟踪的新URL并从它们创建新的请求(Request
)。
如何运行我们的Spider ¶
为了让我们的Spider工作,回到项目的顶层目录并运行︰
scrapy crawl quotes
此命令运行我们刚添加的名为quotes
的Spider,它将发送一些请求给quotes.toscrape.com
域。你将得到类似下面的输出︰
... (omitted for brevity)
2016-09-20 14:48:00 [scrapy] INFO: Spider opened
2016-09-20 14:48:00 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-09-20 14:48:00 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-09-20 14:48:00 [scrapy] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2016-09-20 14:48:00 [scrapy] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-09-20 14:48:01 [quotes] DEBUG: Saved file quotes-1.html
2016-09-20 14:48:01 [scrapy] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2016-09-20 14:48:01 [quotes] DEBUG: Saved file quotes-2.html
2016-09-20 14:48:01 [scrapy] INFO: Closing spider (finished)
...
现在,检查当前目录中的文件。你应该注意到已创建两个新文件︰quotes-1.html和quotes-2.html,内容分别对应我们parse
方法指示的URL。
Note
如果你想知道为什么我们尚未解析HTML,坚持住,我们很快就将涵盖的。
底层刚刚发生了什么?¶
Scrapy调度这个Spider的start_requests
方法返回的scrapy.Request
对象。在接收到每个响应后,它实例化Response
对象,并调用和将该Response作为参数传递给与请求关联的回调方法(在这种情况下是parse
方法)。
start_requests方法的快捷方式¶
你可以用一个URL列表只定义一个start_urls
类属性,而不用实现一个从URL生成scrapyscrapy.Request
对象的start_requests()
方法。然后,start_requests()
的默认实现将使用此列表来创建你的Spider的初始请求︰
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)
parse()
方法将被调用来处理这些URL的每个请求,尽管我们还没有明确告知Scrapy要这样做。它能够发生是因为parse()
是Scrapy的默认回调方法,是为没有显式分配回调方法的请求调用。
提取数据¶
学习如何使用Scrapy提取数据的最佳方法就是使用Scrapy shell来尝试Selector。运行︰
scrapy shell 'http://quotes.toscrape.com/page/1/'
Note
记住,当从命令行运 Scrapy shell时,要始终将URL放入引号中,否则包含参数的URL(即&
字符)将不能工作。
你将看到类似这样︰
[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy] 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
的类似列表的对象,它表示一个Selector
对象的列表,这些对象封装XML/HTML元素并允许你进一步查询细粒度的选择或提取数据。
从上面的标题中提取文本,你可以︰
>>> response.css('title::text').extract()
['Quotes to Scrape']
这里有两件事要注意︰ 一是我们已经添加 ::text
到 CSS 查询,意味着我们只想要选择直接在<title>
元素内的文本元素。如果我们不指定::text
,我们会得到完整的标题元素,包括其标记︰
>>> 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()
可以避免IndexError
并在找不到任何匹配的元素时返回None
。
这里有条经验︰对于大部分爬取代码,你想让它适应在网页上找不到内容而导致的错误,这样即使某些部分爬取失败,你至少可以得到一些数据。
除了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选择器来使用,你可以在shell中使用view(response)
来在网页浏览器中打开资源页。你可以使用你的浏览器开发者工具或扩展有如Firebug(参见关于使用Firebug来爬取和使用Firefox来爬取部分)。
Selector Gadget也是一个用于为视觉上选择元素的快速查找CSS选择器很好的工具,在很多浏览器中都可以工作。
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。如果你仔细查看shell中选择器对象的文本表示形式,你可以看到这点。
虽然也许不像CSS选择器受欢迎,XPath表达式提供更强大的能力,因为除了浏览结构,它也可以查看内容。使用XPath,你能够选择这样的东西︰选择包含文本"Next Page"的链接。这使得XPath非常适于爬取任务,我们鼓励你学会XPath,即使你已经知道如何构建CSS选择器,它将使得爬取容易得多。
这里我们不会大量讲述XPath ,但你可以阅读更多有关Scrapy选择器使用XPath。若要了解更多关于XPath,我们推荐这本教程来通过实例学习XPath和 这本教程来学习"如何在XPath中思考"。
提取名言和作者¶
现在,你知道一点关于选择和提取,让我们通过编写代码从网页中提取名言来完成我们的Spider。
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'
我们获得名言HTML元素选择器的一个列表︰
>>> response.css("div.quote")
上述每个查询所返回的选择器允许我们在它们的子元素上运行进一步查询。让我们分配第一选择器给一个变量,以便我们可以直接在一个特定的名言上运行我们的CSS选择器︰
>>> quote = response.css("div.quote")[0]
现在,让我们使用我们刚刚创建的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']
已经弄清楚了如何提取每一位,我们可以迭代提取所有的名言元素并将它们合并到一个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
>>>
在我们的Spider中提取数据¶
让我们回到我们的Spider。直到现在,它并不提取任何特别的数据,只是将整个HTML页面保存到本地文件。让我们将上面的提取逻辑集成到我们的Spider。
Scrapy的Spider通常会生成许多包含从页面提取的数据的字典。要做到这一点,我们在回调函数中使用Python关键字yield
,正如你可以看到的下面内容︰
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('span small::text').extract_first(),
'tags': quote.css('div.tags a.tag::text').extract(),
}
如果你运行这个Spider,它将输出提取的数据︰
2016-09-19 18:57:19 [scrapy] 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] 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 Lines︰
scrapy crawl quotes -o quotes.jl
JSON Lines格式有用是因为它是类似流,你可以轻松地将新的记录追加给它。当你运行两次的时候,它没有和JSON同样的问题。此外,每个记录是单独的行,你可以处理大文件而无需中内存填充一切,在命令行中有像JQ这样的工具帮助做这件事。
在小型项目中(在本教程中一样),这应该是足够的。然而,如果你想要利用爬取的项目执行更复杂的事情,你可以编写 Item Pipeline。在tutorial/pipelines.py
中,在项目创建的时候已经为Item Pipelines建立一个占位文件。然而你不需要实现任何item pipelines,如果你只想存储爬取的项目。
跟踪链接¶
我们说,你想要爬取网站中的所有页面的名言,而不只是爬取http://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').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/'
让我们看看现在我们的Spider修改为递归地跟踪链接到下一页,从中提取数据︰
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:
next_page = response.urljoin(next_page)
yield scrapy.Request(next_page, callback=self.parse)
现在,在提取数据后,parse
方法查找指向下一个页面的链接,使用urljoin()
方法生成一个完整的绝对URL(因为链接可能是相对的)并产生一个到下一页的新请求,将自己注册为回调以处理下一个页面的数据提取和保持爬取经过所有的页面。
这里你看到的是Scrapy跟踪链接的机制︰当你在回调方法中yield一个Request,Scrapy将调度发送这个Request并注册一个在Request完成时执行的回调方法。
使用这个,你可以构建复杂的爬虫程序,根据你定义的规则跟踪链接,并根据访问的网页提取不同种类的数据。
在我们的示例中,它创建一种循环,跟踪到下一个页面的所有链接,直到找不到——便于爬取博客、论坛和其它具有分页的网站。
更多的例子和模式 ¶
下面是另一Spider,说明回调和跟踪链接,这次是为了爬取作者信息︰
import scrapy
class AuthorSpider(scrapy.Spider):
name = 'author'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
# follow links to author pages
for href in response.css('.author a::attr(href)').extract():
yield scrapy.Request(response.urljoin(href),
callback=self.parse_author)
# follow pagination links
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)
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'),
}
这个Spider从主页面将开始,它将跟踪所有链接到作者页面的页面并对它们调用parse_author
回调函数,以及用parse
回调函数跟踪页码链接,如我们之前看到的。
parse_author
回调函数定义一个辅助函数来提取和清理CSS查询中的数据,并生成带有作者数据的Python 字典。
这个Spider演示的另一个有趣的事情是,即使有许多名言来自同一作者,我们不需要担心多次前往同一作者页。默认情况下,Scrapy过滤掉已经访问过的URL的重复请求,从而避免因为一个编程错误访问服务器次数太多。这可以通过设置DUPEFILTER_CLASS
配置。
希望现在你有很好地理解如何使用Scrapy的跟踪链接和回调机制。
作为另外一个运用跟踪链接机制的示例Spider,请查看CrawlSpider
类,它是一个通用的Spider,实现小的规则引擎,你可以使用它来编写在它上面的爬虫。
另外,一种常见的模式是从多个页面中的数据建立一个项目,使用一个将额外数据传递到回调的把戏。
使用Spider的参数 ¶
当运行你的Spider时,你可以通过使用-a
选项来提供命令行参数给它们︰
scrapy crawl quotes -o quotes-humor.json -a tag=humor
这些参数将传递给Spider的__init__
方法,默认情况下成为Spider的属性。
在此示例中,提供给tag
参数的值,可通过self.tag
访问。基于参数构建URL,你可以使用它来让你的Spider只获取带有一个特定标记的名言:
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('span small a::text').extract_first(),
}
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, self.parse)
如果你传递tag=humor
参数给这个Spider,你会注意到它将只访问来自humor
标记的URL,如http://quotes.toscrape.com/tag/humor
。
下一步 ¶
本教程涵盖了基本的Scrapy,但还有很多其他功能这里没有提到。查看What else?一节,位于Scrapy at a glance一章,以获得最重要信息的快速概览。
你可以继续基本概念部分以更多地了解命令行工具、Spider、 选择器和其它本教程没有涵盖的内容,如建模爬取的数据。如果你喜欢玩示例项目,请查看示例部分。