安装Scrapy

推荐virtualenv安装,步骤如下:

  1. 如果没有virtualenv,先安装virtualenv
1
sudo pip3 install virtualenv
  1. 创建一个virtualenv环境
1
virtualenv --no-site-packages scrapy
  1. 进入目录,激活virtualenv环境
1
source bin/activate
  1. 安装Scrapy
1
pip install Scrapy

爬虫基本流程

ur2im

URL

一切都从一个URL开始。您需要从您想要抓取的站点上获取一些示例url。

1
scrapy shell -s USER_AGENT="Mozilla/5.0" http://www.gumtree.com/p/studios-bedsits-rent

要在使用scrapy shell时调试问题,请添加--pdb参数,以便在出现异常时启用交互式调试。

1
scrapy shell --pdb https://gumtree.com

请求和响应

Scrapy shell为我们做了一些工作,我们给它提供了一个URL,它执行了默认的GET请求并获得了返回码200的响应。这意味着该页面上的信息已经加载并可以使用。如果尝试打印response.body的前50个字符,则会得到以下内容:

1
2
>>> response.body[:50]
'<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8"'

物品

下一步是尝试将响应中的数据提取到物品的字段中。因为这个页面的格式是HTML,所以我们使用XPath表达式来实现。

主要字段 XPath表达式
title //*[@itemprop="name”][1]/text()
price //*[@itemprop="price”][1]/text()
description //*[@itemprop="description”][1]/text()
address //*[@itemtype="http://schema.org/Place”][1]/text()
image_urls //*[@itemprop="image”][1]/@src

一个Scrapy项目

首先创建一个名为"properties"的工程:

1
$ scrapy startproject properties

定义物品

让我们使用文件编辑器打开items.py。其中已经有一些模板代码,但是我们将针对我们的用例修改它。我们将重新定义PropertiesItem类,以添加我们在前一个表中总结的字段。

我们还会添加一些辅助字段后面会用到。需要注意的一件重要的事情是,我们声明一个字段并不意味着我们要在每个爬行器上填充它,或者甚至一起使用它。您可以随意添加任何您认为合适的字段—您可以在以后修改它们。

计算字段 Python表达式
images 图像管道将根据image_urls自动填充这个。
location 我们的地理编码管道将在稍后填充此内容。
辅助字段 Python表达式
url response.url
project self.settings.get(‘BOT_NAME’)
spider self.name
server socket.gethostname()
date datetime.datetime.now()

定义完这些字段,我们将items.py修改如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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()

编写爬虫

一个爬虫的代码实现了整个$UR^2IM$过程。使用如下命令生成爬虫:

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

使用如下命令查看爬虫模板列表:

1
scrapy genspider -l 

basic.py内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import scrapy


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

    def parse(self, response):
    	pass

开始编码修改一下爬虫:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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())

使用如下命令运行爬虫:

1
$ scrapy crawl basic

填充物品

首先我们需要导入PropertiesItem类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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

保存到文件

以下命令可以将爬虫爬取到的数据保存到文件:

1
2
3
4
5
6
$ scrapy crawl basic -o items.json
$ scrapy crawl basic -o items.jl
$ scrapy crawl basic -o items.csv
$ scrapy crawl basic -o items.xml
$ scrapy crawl basic -o "ftp://user:pass@ftp.scrapybook.com/items.json "
$ scrapy crawl basic -o "s3://aws_key:aws_secret@scrapybook/items.json"

清理

我们首先使用一个很好的实用工具类ItemLoader来替换所有那些看起来很混乱的extract()和xpath()操作。通过使用它,我们的parse()方法改变如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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提供了许多有趣的方式来组合数据、格式化数据和清理数据。其中MapCompose可以用来组合任意一个或多个Python函数来实现复杂的功能。比如:

处理器 功能
Join() 将多个结果合并成一个
MapCompose(unicode.strip) 删除开头和结尾的空白字符
MapCompose(unicode.strip, unicode.title) 删除开头和结尾的空白字符,并返回标题结果
MapCompose(float) 转换为数字
MapCompose(lambda i: i.replace(',', ‘'), float) 忽略’,‘字符,并转换为数字
MapCompose(lambda i: urlparse.urljoin(response.url, i)) 将相对路径URL转为绝对路径URL

使用MapCompose修改过的爬虫如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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)))

创建合约

合约有点像spider的单元测试。它们能让你很快知道是否发生了什么坏了。合约包含在注释中,就在函数名(docstring)后面,并且以@开头。看下面一个例子:

1
2
3
4
5
6
7
8
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
    """

使用名scrapy check检查合约是否满足:

1
$ scrapy check basic

提取更多URL

到目前为止,我们只是爬取一个URL链接。很多网站是有很多分页的。因此一个典型的爬虫在两个方向爬取:

  • 水平方向——从一个分页到另一个分页
  • 垂直方向——从一个分页到列表页面

将parse函数修改为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def parse(self, response):
    # Get the next index URLs and yield Requests
    next_selector = response.xpath('//*[contains(@class, "next")]//@href')
    for url in next_selector.extract():
        yield Request(urlparse.urljoin(response.url, url))
    # Get item URLs and yield Requests
    item_selector = response.xpath('//*[@itemprop="url"]/@href')
    for url in item_selector.extract():
        yield Request(urlparse.urljoin(response.url, url),
                      callback=self.parse_item)

两个方向爬取使用CrawlSpider

前面的代码太麻烦了,Scrapy使用CrawlSpider可以简单的实现同样的工作。首先使用crawl模板创建爬虫:

1
$ scrapy genspider -t crawl easy web
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class EasySpider(CrawlSpider):
    name = 'easy'
    allowed_domains = ["web"]

    # Start on the first index page
    start_urls = (
        'http://web:9312/properties/index_00000.html',
    )

    # Rules for horizontal and vertical crawling
    rules = (
        Rule(LinkExtractor(restrict_xpaths='//*[contains(@class,"next")]')),
        Rule(LinkExtractor(restrict_xpaths='//*[@itemprop="url"]'),
             callback='parse_item')
    )