一个Scrapy项目

到目前为止,我们只是在通过 scrapy shell “小打小闹”。现在,既然已经拥有了用于开始第一个 Scrapy 项目的所有必要组成部分,那么让我们按下 Ctrl + D 退出 scrapy shell 吧。需要注意的是,你现在输入的所有内容都将丢失。显然,我们并不希望在每次爬取某些东西的时候都要输入代码,因此一定要谨记 scrapy shell 只是一个可以帮助我们调试页面、XPath 表达式和 Scrapy 对象的工具。不要花费大量时间在这里编写复杂代码,因为一旦你退出,这些代码就都会丢失。为了编写真实的 Scrapy 代码,我们将使用项目。下面创建一个 Scrapy 项目,并将其命名为 "properties", 因为我们正在抓取的数据是房产。

$ scrapy startproject properties
$ cd properties
$ tree
.
├── properties
│       ├── __init__.py
│       ├── items.py
│       ├── pipelines.py
│       ├── settings.py
│       └── spiders
│              └── __init__.py
└── scrapy.cfg
2 directories, 6 files

提醒一下,你可以从 GitHub 中获得本书的全部源代码。要下载该代码,可以使用如下命令:

git clone https://github.com/scalingexcellence/scrapybook

本章的代码在 ch03 目录中,其中该示例的代码在 ch03/properties 目录中。

我们可以看到这个 Scrapy 项目的目录结构。命令 scrapy startproject properties 创建了一个以项目名命名的目录,其中包含 3 个我们感兴趣的文件,分别是 items.py、 pipelines.py 和 settings.py。这里还有一个名为 spiders 的子目录,目前为止该目录是空的。在本章中,我们将主要在 items.py 文件和 spiders 目录中工作。在后续的章节里,还将对设置、管道和 scrapy.cfg 文件有更多探索。

声明item

我们使用一个文件编辑器打开 items.py 文件。现在该文件中已经包含了一些模板代码,不过还需要针对用例对其进行修改。我们将重定义 PropertiesItem 类,添加表 3.2 中总结出来的字段。

我们还会添加几个字段,我们的应用在后续会用到这些字段(这样之后就不需要再修改这个文件了)。本书后续的内容会深入解释它们。需要重点注意的一个事情是,我们声明一个字段并不意味着我们将在每个爬虫中都填充该字段,或是全部使用它。你可以随意添加任何你感觉合适的字段,因为你可以在之后更正它们。

Table 1. 表3.2
可计算字段 Python表达式

images

图像管道将会基于 image_urls 自动填充该字段。可以在后续的章节中了解更多相关内容

location

我们的地理编码管道将会在后面填充该字段。可以在后续的章节中了解更多相关的内容

我们还会添加一些管理字段(见表3.3)。这些字段不是特定于某个应用程序的,而是我个人感兴趣的字段,可能会在未来帮助我调试爬虫。你可以在项目中选择其中的一些字段, 当然也可以不选择。如果你仔细观察这些字段,就会明白它们可以让我清楚何地(server、 url)、何时(date)、如何(spider)执行的抓取。它们还可以自动完成一些任务,比如使 item 失效、规划新的抓取迭代或是删除来自有问题的爬虫的 item。如果你还不能理解所有的表达式,尤其是 server 的表达式,也不用担心。当我们进入到后面的章节时,这些都会变得越来越清楚。

Table 2. 表3.3
管理字段 Python表达式

url

response.url

示例值:'http://web…​/property_000000. html'

project

self.settings.get('BOT_NAME')

示例值:'properties'

spider

self.name

示例值:'basic'

server

socket.gethostname()

示例值:'scrapyserver1'

date

datetime.datetime.now()

示例值:datetime.datetime(2015, 6, 25…​)

给出字段列表之后,再去修改并自定义 scrapy startproject 为我们创建的 PropertiesItem 类,就会变得很容易。在文本编辑器中,修改 properties/items.py 文件,使其包含如下内容:

from scrapy.item import Item, Field

class PropertiesItem(Item):
# Primary fields
title = Field()
price = Field()
description = Field()
address = Field()
image_urls = Field()

# Calculated fields
images = Field()
location = Field()

# Housekeeping fields
url = Field()
project = Field()
spider = Field()
server = Field()
date = Field()

由于这实际上是我们在文件中编写的第一个 Python 代码,因此需要重点指出的是,Python 使用缩进作为其语法的一部分。在每个字段的起始部分,会有精确的 4 个空格或 1 个制表符,这一点非常重要。如果你在其中一行使用了 4 个空格,而在另一行使用了 3 个空格,就会出现语法错误。如果你在其中一行使用了 4 个空格,而在另一行使用了制表符,同样也会产生语法错误。这些空格在 PropertiesItem 类下,将字段声明组织到了一起。其他语言一般使用大括号({})或特殊的关键词(如 begin-end)来组织代码,而 Python 使用空格。

编写爬虫

我们已经在半路上了。现在,我们需要编写爬虫。通常,我们会为每个网站或网站的一部分(如果网站非常大的话)创建一个爬虫。爬虫代码实现了完整的 UR2IM 流程,我们很快就可以看到。

什么时候使用爬虫,什么时候使用项目呢?项目是由 Item 和 若干爬虫组成的。如果有很多网站,并且需要从中抽取相同类型的 Item,比如:房产,那么所有这些网站都可以使用同一个项目,并且为每个源/网站使用一个爬虫。反之,如果要处理图书及房产这两种不同的源时,则应该使用不同的项目。

当然,可以在文本编辑器中从头开始创建一个爬虫,不过为了减少一些输入,更好的方法是使用 scrapy genspider 命令,如下所示。

$ scrapy genspider basic web
Created spider 'basic' using template 'basic' in module:
properties.spiders.basic

现在,如果再次运行 tree 命令,就会注意到与之前相比唯一的不同是在 properties/spiders 目录中增加了一个新文件 basic.py。前面的命令所做的工作就是创建了一个名为 "basic" 的 “默认” 爬虫,并且该爬虫被限制为只能爬取 web 域名下的 URL。如果需要的话,可以很容易地移除这个限制,不过目前来说没有问题。爬虫使用 "basic" 模板创建。你可以通过输入 scrapy genspider -l 来查看其他可用的模板,然后在执行 scrapy genspider 时,通过 -t 参数,使用任意其他模板创建爬虫。在本章稍后的部分,我们将会看到一个示例。

Scrapy 有许多子目录。我们一般假设你位于包含 scrapy.cfg 文件的目录中。这是项目的 “顶级” 目录。现在,每当我们引用 Python “包” 和 “模块” 时,它们就是以映射目录结构的方式设置的。

比如,输出提到了 properties.spiders.basic,就是指 properties/spiders 目录中的 basic.py 文件。我们早前定义的 PropertiesItem 类是在 properties.items 模块中,该模块对应的就是 properties 目录中的 items.py 文件。

如果查看 properties/spiders/basic.py 文件,可以看到如下代码。

import scrapy

class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    start_urls = (
        'http://www.web/',
    )

    def parse(self, response):
        pass

import 语句能够让我们使用 Scrapy 框架中已有的类。下面是扩展自 scrapy.Spider 的 BasicSpider 类的定义。通过 “扩展” 的方式,尽管我们实际上没有写任何代码,但是该类已经 “继承” 了 Scrapy 框架中的 Spider 类的相当一部分功能。这样,就可以只额外编写少量的代码行,而获得一个完整运行的爬虫了。然后,我们可以看到一些爬虫的参数,比如它的名字以及我们允许其爬取的域名。最后是空函数 parse() 的定义,该函数包含了两个参数,分别是 self 和 response 对象。通过使用 self 引用,我们就可以使用爬虫中感兴趣的功能了。而另一个对象 response,我们应该很熟悉,它就是我们在 scrapy shell 中使用过的 response 对象。

这是你的代码——你的爬虫。不要害怕修改它,你不会真的把事情搞砸的。即使在最坏的情况下,你还可以使用 rmproperties/spiders/basic.py* 删除文件,然后再重新生成。尽情发挥吧!

好了,让我们开始改造吧。首先,要使用在 scrapy shell 中使用过的那个 URL,对应地设置到 start_urls 参数中。然后,将使用爬虫预定义的方法 log(),输出在基本字段表中总结的所有内容。修改后,properties/spiders/basic.py 的代码如下所示。

import scrapy

class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    start_urls = (
        'http://web:9312/properties/property_000000.html',
    )

    def parse(self, response):
        self.log("title: %s" % response.xpath(
        '//*[@itemprop="name"][1]/text()').extract())
        self.log("price: %s" % response.xpath(
        '//*[@itemprop="price"][1]/text()').re('[.0-9]+'))
        self.log("description: %s" % response.xpath(
        '//*[@itemprop="description"][1]/text()').extract())
        self.log("address: %s" % response.xpath(
        '//*[@itemtype="http://schema.org/'
        'Place"][1]/text()').extract())
        self.log("image_urls: %s" % response.xpath(
        '//*[@itemprop="image"][1]/@src').extract())

我将会不时地修改格式,以便在屏幕和纸张中都能很好地显示。这并不意味着它有什么特殊的含义。

等了这么久,终于到了运行爬虫的时候了。我们可以使用命令 scrapy crawl 以及爬虫的名称来运行爬虫。

$ scrapy crawl basic
INFO: Scrapy 1.0.3 started (bot: properties)
...
INFO: Spider opened
DEBUG: Crawled (200) <GET http://...000.html>
DEBUG: title: [u'set unique family well']
DEBUG: price: [u'334.39']
DEBUG: description: [u'website...']
DEBUG: address: [u'Angel, London']
DEBUG: image_urls: [u'../images/i01.jpg']
INFO: Closing spider (finished)
...

非常好!不要被大量的日志行吓倒。我们将会在后续的章节中更详细地研究其中的一部分, 不过对于现在而言,只需要注意到所有使用 XPath 表达式收集到的数据确实能够通过这个简单的爬虫代码抽取出来就可以了。

让我们再来试验一下另一个命令:scrapy parse。它允许我们使用 “最合适” 的爬虫来解析参数中给定的任意 URL。我不喜欢抱有侥幸心理,所以我们使用它结合 --spider 参数来设置爬虫。

$ scrapy parse --spider=basic http://web:9312/properties/property_000001.html

你会看到输出和之前是相似的,只不过现在是另一套房产。

scrapy parse 同样也是一个相当方便的调试工具。在任何情况下,如果你想 “认真” 抓取的话,应当使用主命令 scrapy crawl。

填充item

我们将会对前面的代码进行少量修改,以填充 PropertiesItem 。你将会看到,尽管修改非常轻微,但是会 “解锁” 大量的新功能。

首先,需要引入 PropertiesItem 类。如前所述,它在 properties 目录的 items.py 文件中,也就是 properties.items 模块中。我们回到 properties/spiders/basic.py 文件,使用如下命令引入该模块。

from properties.items import PropertiesItem

然后需要进行实例化,并返回一个对象。这非常简单。在 parse() 方法中,可以通过添加 item = PropertiesItem() 语句创建一个新的 item,然后可以按如下方式为其字段分配表达式。

item['title'] = response.xpath('//*[@itemprop="name"][1]/text()').extract()

最后,使用 return item 返回 item。最新版的 properties/spiders/basic.py 代码如下所示。

import scrapy
from properties.items import PropertiesItem

class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]
    start_urls = (
        'http://web:9312/properties/property_000000.html',
    )

    def parse(self, response):
        item = PropertiesItem()
        item['title'] = response.xpath(
        '//*[@itemprop="name"][1]/text()').extract()
        item['price'] = response.xpath(
        '//*[@itemprop="price"][1]/text()').re('[.0-9]+')
        item['description'] = response.xpath(
        '//*[@itemprop="description"][1]/text()').extract()
        item['address'] = response.xpath(
        '//*[@itemtype="http://schema.org/'
        'Place"][1]/text()').extract()
        item['image_urls'] = response.xpath(
        '//*[@itemprop="image"][1]/@src').extract()
        return item

现在,如果你再像之前那样运行 scrapy crawl basic,就会发现一个非常小但很重要的区别。我们不再在日志中记录抓取值(所以没有包含字段值的 DEBUG: 行了),而是看到如下的输出行。

DEBUG: Scraped from <200 http://...000.html>
{'address': [u'Angel, London'],
'description': [u'website ... offered'],
'image_urls': [u'../images/i01.jpg'],
'price': [u'334.39'],
'title': [u'set unique family well']}

这是从本页面抓取得到的 PropertiesItem。非常好,因为 Scrapy 是围绕着 Items 的概念构建的,也就是说你现在可以使用后续章节中介绍的管道,对其进行过滤和丰富了,并且可以通过 “Feed exports” 将其以不同的格式导出存储到不同的地方。

保存文件

请尝试如下爬取示例。

$ scrapy crawl basic -o items.json
$ cat items.json
[{"price": ["334.39"], "address": ["Angel, London"], "description":
["website court ... offered"], "image_urls": ["../images/i01.jpg"],"title": ["set unique family well"]}]


$ scrapy crawl basic -o items.jl
$ cat items.jl
{"price": ["334.39"], "address": ["Angel, London"], "description":
["website court ... offered"], "image_urls": ["../images/i01.jpg"],"title": ["set unique family well"]}

$ scrapy crawl basic -o items.csv
$ cat items.csv
description,title,url,price,spider,image_urls...
"...offered",set unique family well,,334.39,,../images/i01.jpg
$ scrapy crawl basic -o items.xml
$ cat items.xml
<?xml version="1.0" encoding="utf-8"?>
<items><item><price><value>334.39</value></price>...</item></items>

我们不需要编写任何额外的代码,就可以保存为这些不同的格式。Scrapy 在幕后识别你想要输出的文件扩展名,并以适当的格式输出到文件中。前面的格式覆盖了一些最常见的用例。CSV 和 XML 文件非常流行,因为类似微软 Excel 的电子表格程序可以直接打开它们。JSON 文件在网上非常流行,原因是它们富有表现力而且与 JavaScript 的关系相当密切。JSON 与 JSON 行(JSON Line)格式的轻微不同是,.json 文件是在一个大数组中存储 JSON 对象的。这就意味着如果你有一个 1GB 的文件,你可能不得不在使用典型的解析器解析之前,将其全部存入内存当中。而 .jl 文件则是每行包含一个 JSON 对象,所以它们可以被更高效地读取。

将你生成的文件保存到文件系统之外的地方也很容易。比如,通过使用如下命令,Scrapy 可以自动将文件上传到 FTP 或 S3 存储桶中。

$ scrapy crawl basic -o "ftp://user:pass@ftp.scrapybook.com/items.json "
$ scrapy crawl basic -o "s3://aws_key:aws_secret@scrapybook/items.json"

需要注意的是,除非凭证和 URL 都更新为与有效的主机 /S3 提供商相匹配,否则该示例无法工作。

我的 MySQL 驱动在哪里?起初,我也对 Scrapy 缺少针对 MySQL 或其他数据库的内置支持感到惊讶。而实际上,没有什么是内置的,这与 Scrapy 的思考方式是完全违背的。Scrapy 的目标是快速和可扩展。它使用了很少的 CPU,以及尽可能高的入站带宽。从性能的角度来看,将数据插入到大部分关系型数据库将会是一场灾难。当需要将 item 插入到数据库时,必须将其先存储到文件当中,然后再使用批量加载机制导入它们。在第 9 章中,我们将会看到多种高效的方式,用来将独立的 item 导入到数据库中。

这里需要注意的另一件事是,如果你现在尝试使用 scrapy parse,它会向你显示已经抓取的 item,以及你的爬取生成的新请求(本例中没有) 。

$ scrapy parse --spider=basic http://web:9312/properties/property_000001.html
INFO: Scrapy 1.0.3 started (bot: properties)
...
INFO: Spider closed (finished)
>>> STATUS DEPTH LEVEL 1 <<<
# Scraped Items ------------------------------------------------
[{'address': [u'Plaistow, London'],
'description': [u'features'],
'image_urls': [u'../images/i02.jpg'],
'price': [u'388.03'],
'title': [u'belsize marylebone...deal']}]
# Requests ------------------------------------------------
[]

在调试给出意料之外的结果的 URL 时,你会更加感激 scrapy parse。

清理——item装载器与管理字段

恭喜,你在创建基础爬虫方面做得不错!下面让我们做得更专业一些吧。

首先,我们使用一个强大的工具类——ItemLoader,以替代那些杂乱的 extract() 和 xpath() 操作。通过使用该类,我们的 parse() 方法会按如下进行代码变更。

def parse(self, response):
    l = ItemLoader(item=PropertiesItem(), response=response)
    l.add_xpath('title', '//*[@itemprop="name"][1]/text()')
    l.add_xpath('price', './/*[@itemprop="price"][1]/text()', re='[,.0-9]+')
    l.add_xpath('description', '//*[@itemprop="description"][1]/text()')
    l.add_xpath('address', '//*[@itemtype="http://schema.org/Place"][1]/text()')
    l.add_xpath('image_urls', '//*[@itemprop="image"][1]/@src')
    return l.load_item()

好多了,是不是?不过,这种写法并不只是在视觉上更加舒适,它还非常明确地声明了我们意图去做的事情,而不会将其与实现细节混淆起来。这就使得代码具有更好的可维护性以及自描述性。

ItemLoader 提供了许多有趣的结合数据及对数据进行格式化和清洗的方式。请注意,此类功能的开发非常活跃,因此请查阅 Scrapy 优秀的官方文档来发现使用它们的更高效的方式,文档地址为 http://doc.scrapy.org/en/latest/topics/loaders.html。 Itemloaders 通过不同的处理类传递 XPath/CSS 表达式的值。处理器是一个快速而又简单的函数。处理器的一个例子是 Join()。假设你已经使用类似 //p 的 XPath 表达式选取了很多个段落,该处理器可以将这些段落结合成一个条目。另一个非常有意思的处理器是 MapCompose()。通过该处理器,你可以使用任意 Python 函数或 Python 函数链,以实现复杂的功能。比如,MapCompose(float) 可以将字符串数据转换为数值,而 MapCompose(Unicode.strip, Unicode.title) 可以删除多余的空白符,并将字符串格式化为每个单词均为首字母大写的样式。让我们看一些处理器的例子,如表3.4所示。

Table 3. 表3.4
处理器 功能

Join()

把多个结果连接在一起

MapCompose(unicode.strip)

去除首尾的空白符

MapCompose(unicode.strip, unicode.title)

与MapCompose(unicode.strip)相同,不过还会使结果按照标题样式进行格式

MapCompose(float)

将字符串转为数值

MapCompose(lambda i: i.replace(',', ''), float)

将字符串转为数值,并忽略可能存在的','字符

MapCompose(lambda i: urlparse.urljoin (response.url, i))

以response.url为基础,将URL相对路径转换为URL绝对路径

你可以使用任何 Python 表达式作为处理器。可以看到,我们可以很容易地将它们一个接一个地连接起来,比如,我们前面给出的去除首尾空白符以及标题化的例子。unicode.strip() 和 unicode.title() 在某种意义上来说比较简单,它们只有一个参数,并且也只有一个返回结果。我们可以在 MapCompose 处理器中直接使用它们。而另一些函数,像 replace() 或 urljoin(),就会稍微有点复杂,它们需要多个参数。对于这种情况,我们可以使用 Python 的 “lambda表达式”。 lambda 表达式是一种简洁的函数。比如下面这个简洁的 lambda 表达式。

myFunction = lambda i: i.replace(',', '')

可以代替:

def myFunction(i):
    return i.replace(',', '')

通过使用 lambda,我们将类似 replace() 和 urljoin() 这样的函数包装在只有一个参数及一个返回结果的函数中。为了能够更好地理解表 3.4 中的处理器,下面看几个使用处理器的例子。使用 scrapy shell 打开任意 URL,然后尝试如下操作。

>>> from scrapy.loader.processors import MapCompose, Join
>>> Join()(['hi','John'])
u'hi John'
>>> MapCompose(unicode.strip)([u' I',u' am\n'])
[u'I', u'am']
>>> MapCompose(unicode.strip, unicode.title)([u'nIce cODe'])
[u'Nice Code']
>>> MapCompose(float)(['3.14'])
[3.14]
>>> MapCompose(lambda i: i.replace(',', ''), float)(['1,400.23'])
[1400.23]
>>> import urlparse
>>> mc = MapCompose(lambda i: urlparse.urljoin('http://my.com/test/abc',i))
>>> mc(['example.html#check'])
['http://my.com/test/example.html#check']
>>> mc(['http://absolute/url#help'])
['http://absolute/url#help']

这里要解决的关键问题是,处理器只是一些简单小巧的功能,用来对我们的 XPath/CSS 结果进行后置处理。现在,在爬虫中使用几个这样的处理器,并按照我们想要的方式输出。

def parse(self, response):
    l.add_xpath('title', '//*[@itemprop="name"][1]/text()',MapCompose(unicode.strip, unicode.title))
    l.add_xpath('price', './/*[@itemprop="price"][1]/text()',MapCompose(lambda i: i.replace(',', ''), float), re='[,.0-9]+')
    l.add_xpath('description', '//*[@itemprop="description"][1]/text()', MapCompose(unicode.strip), Join())
    l.add_xpath('address', '//*[@itemtype="http://schema.org/Place"][1]/text()', MapCompose(unicode.strip))
    l.add_xpath('image_urls', '//*[@itemprop="image"][1]/@src',MapCompose(lambda i: urlparse.urljoin(response.url, i)))

完整列表将会在本章后续部分给出。当你使用我们目前开发的代码运行 scrapy crawl basic 时,可以得到更加整洁的输出值。

'price': [334.39],
'title': [u'Set Unique Family Well']

最后, 我们可以通过使用 add_value() 方法,添加 Python 计算得出的单个值(而不是 XPath/CSS 表达式)。我们可以用该方法设置 “管理字段”,比如 URL、爬虫名称、时间戳等。我们还可以直接使用管理字段表中总结出来的表达式,如下所示。

l.add_value('url', response.url)
l.add_value('project', self.settings.get('BOT_NAME'))
l.add_value('spider', self.name)
l.add_value('server', socket.gethostname())
l.add_value('date', datetime.datetime.now())

为了能够使用其中的某些函数,请记得引入 datetime 和 socket 模块。

好了!我们现在已经得到了非常不错的 Item。此刻,你的第一感觉可能是所做的这些都很复杂,你可能想要知道这些工作是不是值得付出努力。答案当然是值得的——这是因为,这就是你为了从页面抽取数据并将其存储到 Item 中几乎所有需要知道的东西。如果你从零开始编写,或者使用其他语言,该代码通常都会非常难看,而且很快就会变得不可维护。而使用 Scrapy 时,只需要仅仅 25 行代码。该代码十分简洁,用于表明意图,而不是实现细节。你清楚地知道每一行代码都在做什么,并且它可以很容易地修改、复用及维护。

你可能产生的另一个感觉是所有的处理器以及 ItemLoader 并不值得去努力。如果你是一个经验丰富的 Python 开发者,可能会觉得有些不舒服,因为你必须去学习新的类,来实现通常使用字符串操作、lambda 表达式以及列表推导式就可以完成的操作。不过,这只是 ItemLoader 及其功能的简要概述。如果你更加深入地了解它,就不会再回头了。ItemLoader 和处理器是基于编写并支持了成千上万个爬虫的人们的抓取需求而开发的工具包。如果你准备开发多个爬虫的话,就非常值得去学习使用它们。

创建contract

contract 有点像为爬虫设计的单元测试。它可以让你快速知道哪里有运行异常。例如,假设你在几个星期之前编写了一个抓取程序,其中包含几个爬虫,今天想要检查一下这些爬虫是否仍然能够正常工作,就可以使用这种方式。contract 包含在紧挨着函数名的注释(即文档字符串)中,并且以 @ 开头。下面来看几个 contract 的例子。

def parse(self, response):
    """ This function parses a property page.
    @url http://web:9312/properties/property_000000.html
    @returns items 1
    @scrapes title price description address image_urls
    @scrapes url project spider server date
    """

上述代码的含义是,检查该 URL,并找到我列出的字段中有值的一个 Item。现在,当你运行 scrapy check 时, 就会去检查 contract 是否能够满足。

$ scrapy check basic
----------------------------------------------------------------
Ran 3 contracts in 1.640s
OK

如果将 url 字段留空(通过注释掉该行来设置),你会得到一个失败描述。

FAIL: [basic] parse (@scrapes post-hook)
------------------------------------------------------------------
ContractFail: 'url' field is missing

contract 失败的原因可能是爬虫代码无法运行,或者是你要检查的 URL 的 XPath 表达式已经过时了。虽然结果并不详尽,但它是抵御坏代码的第一道灵巧的防线。

综合上面的内容,下面给出我们的第一个基础爬虫的代码。

from scrapy.loader.processors import MapCompose, Join
from scrapy.loader import ItemLoader
from properties.items import PropertiesItem
import datetime
import urlparse
import socket
import scrapy
class BasicSpider(scrapy.Spider):
    name = "basic"
    allowed_domains = ["web"]

    # Start on a property page
    start_urls = (
        'http://web:9312/properties/property_000000.html',
    )

    def parse(self, response):
        """ This function parses a property page.
        @url http://web:9312/properties/property_000000.html
        @returns items 1
        @scrapes title price description address image_urls
        @scrapes url project spider server date
        """
        # Create the loader using the response
        l = ItemLoader(item=PropertiesItem(), response=response)

        # Load fields using XPath expressions
        l.add_xpath('title', '//*[@itemprop="name"][1]/text()', MapCompose(unicode.strip, unicode.title))
        l.add_xpath('price', './/*[@itemprop="price"][1]/text()', MapCompose(lambda i: i.replace(',', ''),float),re='[,.0-9]+')
        l.add_xpath('description', '//*[@itemprop="description"][1]/text()', MapCompose(unicode.strip), Join())
        l.add_xpath('address','//*[@itemtype="http://schema.org/Place"][1]/text()', MapCompose(unicode.strip))
        l.add_xpath('image_urls', '//​ *[@itemprop="image"][1]/@src', MapCompose(lambda i: urlparse.urljoin(response.url, i)))

        # Housekeeping fields
        l.add_value('url', response.url)
        l.add_value('project', self.settings.get('BOT_NAME'))
        l.add_value('spider', self.name)
        l.add_value('server', socket.gethostname())
        l.add_value('date', datetime.datetime.now())

        return l.load_item()