Scrapy 教程
本文翻译自scrapy的最新官方教程,觉得有帮助的朋友可以小小打赏一下,谢谢。
首先,用户需要安装Scrapy,可以参见安装指导。在本教程中,我们将爬取网站dmoz,并包含以下这些任务:
- 创建一个全新的Scrapy项目
- 定义用户想爬取的数据类别
- 编写一个爬虫分析一个网页并提取所需数据
- 编写流程来存储所提取的数据
创建一个项目
首先,我们在目标路径下输入并执行以下代码
scrapy startproject tutorial
这会创建一个tutorial
目录,其中含有以下内容
tutorial/
scrapy.cfg # 配置文件
tutorial/ # 项目的Python模组
__init__.py
items.py # items文件,数据格式定义
pipelines.py # pipelines文件,定义流程处理
settings.py # settings文件,项目设置
spiders/ # 爬虫路径,用户根据需要定义自己的爬虫
__init__.py
...
定义我们的数据(Item)
Items是存储的容器,随着爬虫工作而加载,调用格式上和Python字典类似。Scrapy中也可以使用单纯的Python字典,但是Items提供了额外的对于填充未声明字段的保护机制,避免用户输入错误引起的错误。
我们通过创建一个scrapy.Item
类,并定义其属性为scrapy.Field
来声明Scapy中的数据类型。这和对应关系映射(ORM)机制类似。
我们首先对我们希望从dmoz.org获取到的数据进行建模刻画。具体而言,我们希望获取网页的名称,连接以及描述信息,因此我们对这三个属性进行字段定义。因此我们如下编写tutorial
目录下的items.py
:
import scrapy
class DmozItem(scrapy.Item):
title = scrapy.Field()
link = scrapy.Field()
desc = scrapy.Field()
这种定义初看比较复杂,但是定义这样一个类会让用户可以进一步使用其他Scrapy中便利的部件和助手。
第一个爬虫
爬虫(spiders)是用户定义的一系列类,Scrapy根据这些类在某个定义域内抓取用户感兴趣的信息。在爬虫中,用户定义初始的需要下载的连接,怎么进一步扩展爬虫,如何解析当前页面并提取之间定义的Items
为了创建一个爬虫,用户首先需要定义一个scrapy.Spider
的子类,并且定义某些属性:
-
name
:是爬虫的识别符,在当前项目中必须是唯一,不能为不同的爬虫设置相同的名称。 -
start_urls
:一组起始链接集合,爬虫将从这些链接开始。接下来的链接会从这些起始链接中提取。 -
parse
:爬虫的一个方法,会以下载起始链接得到的Response
作为参数调用。其中Response
作为唯一参数传递进去。这个方法负责解析响应数据并提取爬取的数据以及更多的链接。 更进一步,parse
方法负责处理相应并返回数据和接下来的链接。
所以我们首先如下定义第一个爬虫,存在tutorial/spiders
目录下,命名为dmoz_spider.py
:
import scrapy
class DmozSpider(scrapy.Spider):
name = 'dmoz'
allowed_domains = ["dmoz.org"]
start_urls = [
"http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
"http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
]
def parse(self, response):
filename = response.url.split("/")[-2] + '.html'
with open(filename, 'wb') as f:
f.write(response.body)
抓取
Scrapy中我们通过如下命令执行爬虫任务
scrapy crawl dmoz
注意上述命令在项目的最高层目录执行,而dmoz
就是上面定义的爬虫唯一标识符。第一次我们会得到类似下面的结果输出:
2014-01-23 18:13:07-0400 [scrapy] INFO: Scrapy started (bot: tutorial)
2014-01-23 18:13:07-0400 [scrapy] INFO: Optional features available: ...
2014-01-23 18:13:07-0400 [scrapy] INFO: Overridden settings: {}
2014-01-23 18:13:07-0400 [scrapy] INFO: Enabled extensions: ...
2014-01-23 18:13:07-0400 [scrapy] INFO: Enabled downloader middlewares: ...
2014-01-23 18:13:07-0400 [scrapy] INFO: Enabled spider middlewares: ...
2014-01-23 18:13:07-0400 [scrapy] INFO: Enabled item pipelines: ...
2014-01-23 18:13:07-0400 [scrapy] INFO: Spider opened
2014-01-23 18:13:08-0400 [scrapy] DEBUG: Crawled (200) <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/> (referer: None)
2014-01-23 18:13:09-0400 [scrapy] DEBUG: Crawled (200) <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Books/> (referer: None)
2014-01-23 18:13:09-0400 [scrapy] INFO: Closing spider (finished)
由于我们之前的parse
中将响应的body
写成文件并存储在最高层目录。
对当前爬虫的分析
Scrapy首先针对每一个在start_urls
中的链接创建scrapy.Request
对象,并将parse
方法设置为爬虫的回调函数。这些请求被Scrapy安排时间规划并先后执行,返回scrapy.http.Response
对象然后通过parse
方法传输给爬虫对象。
提取数据
选择器简介
这里有不同的方法从网页中提取数据。Scrapy使用基于XPATH或者CSS的机制实现了自己的选择器Scrapy Selectors。这里简单介绍一些XPath表达式以及其含义
-
/html/head/title
: 选择<head>
元素中的<title>
元素 -
/html/head/title/text()
: 选择上面<title>
元素中的文本 -
//td
: 选择所有的<td>
元素 -
//div[@class="mine"]
: 选择所有包含属性class="min"
的div
元素
为了解析CSS和XPath表达式,Scrapy提供选择器Selector
类以及方便的快捷式避免每次都重复实例化选择器。总体说来,选择器可以被看作是表示文档结构节点的对象。所以,第一个实例化的选择器是和根节点即整个文档关联在一块的。
Scrapy中的Selector
含有四个常用的基本方法
-
xpath()
: 返回一个选择器列表,每一个元素代表根据xpath表达式选择的节点。 -
css()
: 返回一个选择器列表,每一个元素代表根据css表达式选择的节点。 -
extract
: 返回选择数据的unicode字符串 -
re()
: 返回作用上正则表达式的unicode字符串列表
在壳(shell)中尝试选择器
在项目的根目录执行命令
scrapy shell "http://www.dmoz.org/Computers/Programming/Languages/Python/Books/"
此时IPython环境看起来如下
[ ... Scrapy log here ... ]
2014-01-23 17:11:42-0400 [scrapy] DEBUG: Crawled (200) <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Books/> (referer: None)
[s] Available Scrapy objects:
[s] crawler <scrapy.crawler.Crawler object at 0x3636b50>
[s] item {}
[s] request <GET http://www.dmoz.org/Computers/Programming/Languages/Python/Books/>
[s] response <200 http://www.dmoz.org/Computers/Programming/Languages/Python/Books/>
[s] settings <scrapy.settings.Settings object at 0x3fadc50>
[s] spider <Spider 'default' at 0x3cebf50>
[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
In [1]:
在shell环境加载后,我们可以通过局部变量response
获取响应数据,所以如果用户在命令行中输入response.body
,就会看到响应的主体,类似地,你可以通过输入response.headers
访问其头部数据。
此时,response
变量含有选择器selector
属性,为Selector
类的一个实例,随着这个具体的reponse
实例化。因此用户可以通过调用response.selector.xpath()
或者response.selector.css()
来获取需要的部分,也可以通过快捷表达式response.xpath()
和response.css()
来调用。测试我们刚刚介绍的选择器,有
In [1]: response.xpath('//title')
Out[1]: [<Selector xpath='//title' data=u'<title>Open Directory - Computers: Progr'>]
In [2]: response.xpath('//title').extract()
Out[2]: [u'<title>Open Directory - Computers: Programming: Languages: Python: Books</title>']
In [3]: response.xpath('//title/text()')
Out[3]: [<Selector xpath='//title/text()' data=u'Open Directory - Computers: Programming:'>]
In [4]: response.xpath('//title/text()').extract()
Out[4]: [u'Open Directory - Computers: Programming: Languages: Python: Books']
In [5]: response.xpath('//title/text()').re('(\w+):')
Out[5]: [u'Computers', u'Programming', u'Languages', u'Python']
可以看到,直接选择出来为Scrapy中的选择器对象,需要通过extract()
或者re()
方法提取。
提取数据
有了之前的基础,我们现在从这些页面中尝试提取出真实有用的信息。注意到response.body
是网页的源代码,为HTML代码,通常很难对其进行直接分析,用户可以使用一些可视化方法来辅助,比如Firebug。通过观察源代码可以发现,需要的网页信息其实都存储在一个<ul>
元素内,所以整体流程是通过选择<li>
元素列表来获取需要信息:
response.xpath('//ul/li')
以及其中对网页的描述
response.xpath('//ul/li/text()').extract()
网页的标题为
response.xpath('//ul/li/a/text()').extract()
以及这些网页的链接地址
response.xpath('//ul/li/a/@href').extract()
正如我们提到的,每一个.xpath()
返回一个选择器列表,所以我们可以通过对选择器调用.xpath()
方法去进一步获取需要数据,例如我们将之前的几种选择器融合在一块,有
for sel in response.xpath('//ul/li'):
title = sel.xpath('a/text()').extract()
link = sel.xpath('a/@href').extract()
desc = sel.xpath('text()').extract()
print title, link, desc
我们将这部分放到之前的parse()
中,有
import scrapy
class DmozSpider(scrapy.Spider):
name = "dmoz"
allowed_domains = ["dmoz.org"]
start_urls = [
"http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
"http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
]
def parse(self, response):
for sel in response.xpath('//ul/li'):
title = sel.xpath('a/text()').extract()
link = sel.xpath('a/@href').extract()
desc = sel.xpath('text()').extract()
print title, link, desc
此时我们将不再得到简单的HTML文件,而是需要的数据格式
使用我们的Item
Item
对象是Scrapy定制的Python字典,所以我们可以简单使用标准的字典语法来获取我们想要的值
>>> item = DmozItem()
>>> item['title'] = 'Example title'
>>> item['title']
'Example title'
和普通字典没有什么区别。为了返回当前我们爬到的数据,最终的爬虫看上去如下:
import scrapy
from tutorial.items import DmozItem
class DmozSpider(scrapy.Spider):
name = "dmoz"
allowed_domains = ["dmoz.org"]
start_urls = [
"http://www.dmoz.org/Computers/Programming/Languages/Python/Books/",
"http://www.dmoz.org/Computers/Programming/Languages/Python/Resources/"
]
def parse(self, response):
for sel in response.xpath('//ul/li'):
item = DmozItem()
item['title'] = sel.xpath('a/text()').extract()
item['link'] = sel.xpath('a/@href').extract()
item['desc'] = sel.xpath('text()').extract()
yield item
使用这个爬虫去爬取dmoz.org可以得到DmozItem
对象
[scrapy] DEBUG: Scraped from <200 http://www.dmoz.org/Computers/Programming/Languages/Python/Books/>
{'desc': [u' - By David Mertz; Addison Wesley. Book in progress, full text, ASCII format. Asks for feedback. [author website, Gnosis Software, Inc.\n],
'link': [u'http://gnosis.cx/TPiP/'],
'title': [u'Text Processing in Python']}
[scrapy] DEBUG: Scraped from <200 http://www.dmoz.org/Computers/Programming/Languages/Python/Books/>
{'desc': [u' - By Sean McGrath; Prentice Hall PTR, 2000, ISBN 0130211192, has CD-ROM. Methods to build XML applications fast, Python tutorial, DOM and SAX, new Pyxie open source XML processing library. [Prentice Hall PTR]\n'],
'link': [u'http://www.informit.com/store/product.aspx?isbn=0130211192'],
'title': [u'XML Processing with Python']}
紧接着的链接
假设除开仅仅爬取Books和Resources页面,我们还想获取所有Python路径下的页面。现在用户知道如何从一个页面提取数据,所以现在问题是如何从当前页面抓取用户感兴趣页面的链接,紧接着在这些页面再次提取感兴趣的数据。将爬虫代码做一些小小的修正:
import scrapy
from tutorial.items import DmozItem
class DmozSpider(scrapy.Spider):
name = "dmoz"
allowed_domains = ["dmoz.org"]
start_urls = [
"http://www.dmoz.org/Computers/Programming/Languages/Python/",
]
def parse(self, response):
for href in response.css("ul.directory.dir-col > li > a::attr('href')"):
url = response.urljoin(href.extract())
yield scrapy.Request(url, callback=self.parse_dir_contents)
def parse_dir_contents(self, response):
for sel in response.xpath('//ul/li'):
item = DmozItem()
item['title'] = sel.xpath('a/text()').extract()
item['link'] = sel.xpath('a/@href').extract()
item['desc'] = sel.xpath('text()').extract()
yield item
现在parse()
方法仅仅提取了链接数据,通过response.urljoin
方法建立绝对路径并且产生新的请求,并注册回调函数parse_dir_contents()
来爬取需要的数据。这里Scrapy的机制是这样的,当产生新的请求时,Scrapy会调度进程发送请求而回调函数会在请求完成后执行。在这样的机制下,用户可以设计很复杂的爬虫机制,根据规则得到下一步的链接,以及根据当前页面规则提取不同的数据。一个常用的模式是先使用回调函数提取数据,寻找下一个链接然后产生新的请求
def parse_articles_follow_next_page(self, response):
for article in response.xpath("//article"):
item = ArticleItem()
... extract article data here
yield item
next_page = response.css("ul.navigation > li.next-page > a::attr('href')")
if next_page:
url = response.urljoin(next_page[0].extract())
yield scrapy.Request(url, self.parse_articles_follow_next_page)
存储爬取的数据
最简单的方法是调用下面的命令
scrapy crawl dmoz -o items.json
这会产生一个包含所有数据的文件items.json
,使用JSON序列化。在小项目中,这种方法一般是足够的,如果想设计更复杂的系统,用户可以编写一个Item Pipeline,即在tutorial/pipelines.py
中实现自己的存储方法。