Scrapy Learning笔记(四)- Scrapy双向爬取


摘要:介绍了使用Scrapy进行双向爬取(对付分类信息网站)的方法。

所谓的双向爬取是指以下这种情况,我要对某个生活分类信息的网站进行数据爬取,譬如要爬取租房信息栏目,我在该栏目的索引页看到如下页面,此时我要爬取该索引页中的每个条目的详细信息(纵向爬取),然后在分页器里跳转到下一页(横向爬取),再爬取第二页中的每个条目的详细信息,如此循环,直至最后一个条目。

这样来定义双向爬取:

  • 水平方向 – 从一个索引页到另一个索引页

  • 纯直方向 – 从一个索引页到条目详情页

在本节中,

提取索引页到下一个索引页的xpath为:'//*[contains(@class,"next")]//@href'

提取索引页到条目详情页的xpath为:'//*[@itemprop="url"]/@href'

manual.py文件的源代码地址:

https://github.com/Kylinlin/scrapybook/blob/master/ch03%2Fproperties%2Fproperties%2Fspiders%2Fmanual.py

把之前的basic.py文件复制为manual.py文件,并做以下修改:

  • 导入Request:from scrapy.http import Request

  • 修改spider的名字为manual

  • 更改starturls为'http://web:9312/properties/index00000.html'

  • 将原料的parse函数改名为parse_item,并新建一个parse函数,代码如下:

#本函数用于提取索引页中每个条目详情页的超链接,以及下一个索引页的超链接
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))#Request()函数没有赋值给callback,就会默认回调函数就是parse函数,所以这个语句等价于
yield Request(urlparse.urljoin(response.url, url), callback=parse)

        # 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)

如果直接运行manual,就会爬取全部的页面,而现在只是测试阶段,可以告诉spider在爬取一个特定数量的item之后就停止,通过参数:-s CLOSESPIDER_ITEMCOUNT=10

运行命令:$ scrapy crawl manual -s CLOSESPIDER_ITEMCOUNT=10

它的输出如下:

spider的运行流程是这样的:首先对start_url中的url发起一个request,然后下载器返回一个response(该response包含了网页的源代码和其他信息),接着spider自动将response作为parse函数的参数并调用。

parse函数的运行流程是这样的:

1. 首先从该response中提取class属性中包含有next字符的标签(就是分页器里的“下一页”)的超链接,在第一次运行时是:'index_00001.html'。

2. 在第一个for循环里首先构建一个完整的url地址(’http://web:9312/scrapybook/properties/index_00001.html'),把该url作为参数构建一个Request对象,并把该对象放入到一个队列中(此时该对象是队列的第一个元素)。

3. 继续在该respone中提取属性itemprop等于url字符的标签(每一个条目对应的详情页)的超链接(譬如:'property_000000.html')。

4. 在第二个for循环里对提取到的url逐个构建完整的url地址(譬如:’http://web:9312/scrapybook/properties/ property_000000.html’),并使用该url作为参数构建一个Request对象,按顺序将对象放入到之前的队列中。

5. 此时的队列是这样的

Request(http://…index_00001.html)

Request(http://…property_000000.html)

Request(http://…property_000029.html)

6. 当把最后一个条目详情页的超链接(property_000029.html)放入队列后,调度器就开始处理这个队列,由后到前把队列的最后一个元素提取出来放入下载器中下载并把response传入到回调函数(parse_item)中处理,直至到了第一个元素(index_00001.html),因为没有指定回调函数,默认的回调函数是parse函数本身,此时就进入了步骤1,这次提取到的超链接是:'index_00002.html',然后就这样循环下去。

这个parse函数的执行过程类似于这样:

next_requests = []
for url in...
    next_requests.append(Request(...))
for url in...
    next_requests.append(Request(...))
return next_requests

可以看到使用后进先出队列的最大好处是在处理一个索引页时马上就开始处理该索引页里的条目列表,而不用维持一个超长的队列,这样可以节省内存,有没有觉得上面的parse函数写得有些让人难以理解呢,其实可以换一种更加简单的方式,对付这种双向爬取的情况,可以使用crawl的模板。

首先在命令行里按照crawl的模板生成一个名为easy的spider

$ scrapy genspider -t crawl easy web

打开该文件

...
class EasySpider(CrawlSpider):
    name = 'easy'
    allowed_domains = ['web']
    start_urls = ['http://www.web/']
    rules = (
        Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    )
    def parse_item(self, response):
        ...

可以看到自动生成了上面的那些代码,注意这个spider是继承了CrawlSpider类,而CrawlSpider类已经默认提供了parse函数的实现,所以我们并不需要再写parse函数,只需要配置rules变量即可

rules = (
    Rule(LinkExtractor(restrict_xpaths='//*[contains(@class,"next")]')),
    Rule(LinkExtractor(restrict_xpaths='//*[@itemprop="url"]'),
         callback='parse_item')
)

运行命令:$ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=90

这个方法有以下不同之处:

  • 这两个xpath与之前使用的不同之处在于没有了a和href这两个约束字符,因为LinkExtrator是专门用来提取超链接的,所以会自动地提取标签中的a和href的值,当然可以通过修改LinkExtrator函数里的参数tags和attrs来提取其他标签或属性里的超链接。

  • 还要注意的是这里的callback的值是字符串,而不是函数的引用。

  • Rule()函数里设置了callback的值,spider就默认不会跟踪目标页里的其他超链接(意思是说,不会对这个已经爬取过的网页使用xpaths来提取信息,爬虫到这个页面就终止了)。如果设置了callback的值,也可以通过设置参数follow的值为True来进行跟踪,也可以在callback指定的函数里return/yield这些超链接。

在上面的纵向爬取过程中,在索引页的每一个条目的详情页都分别发送了一个请求,如果你对爬取效率要求很高的话,那就得换一个思路了。很多时候在索引页中对每一个条目都做了简介,虽然信息并没有详情页那么全,但如果你追求很高的爬取效率,那么就不能逐个访问条目的详情页,而是直接从索引页中获取条目的信息。所以,你要平衡好效率与信息质量之间的矛盾。

再次观察索引页,其实可以发现每个条目的节点都使用了itemptype=”http://schema.org/Product”来标记,于是直接从这些节点中获取条目信息。

使用scrapy shell工具来再次分析索引页:

scrapy shell http://web:9312/properties/index_00000.html

上图中的每一个Selector都指向了一个条目,这些Selector也是可以用xpath来解析的,现在就要循环解析着30个Selector,从中提取条目的各种信息

fast.py源文件地址:

https://github.com/Kylinlin/scrapybook/blob/master/ch05%2Fproperties%2Fproperties%2Fspiders%2Ffast.py

将manual.py文件复制并重命名为fast.py,做以下修改:

  • 将spider名称修改为fast

  • 修改parse函数,如下

def parse(self, response):
# Get the next index URLs and yield Requests,这部分并没改变
next_sel = response.xpath('//*[contains(@class,"next")]//@href')
for url in next_sel.extract():
    yield Request(urlparse.urljoin(response.url, url))

# Iterate through products and create PropertiesItems,改变的是这里
selectors = response.xpath(
    '//*[@itemtype="http://schema.org/Product"]')
for selector in selectors: # 对selector进行循环
        yield self.parse_item(selector, response)
  • 修改parse_item函数如下
#有几点变化:
#1、xpath表达式里全部用了一个点号开头,因为这是在selector里面提取信息,所以这个一个相对路径的xpath表达式,这个点号代表了selector
#2、ItemLoader函数里用了selector变量,而不是response变量

def parse_item(self, selector, response):
    # Create the loader using the selector
    l = ItemLoader(item=PropertiesItem(), selector=selector)

    # 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))
    make_url = lambda i: urlparse.urljoin(response.url, i)
    l.add_xpath('image_urls', './/*[@itemprop="image"][1]/@src',
                    MapCompose(make_url))

    # Housekeeping fields
    l.add_xpath('url', './/*[@itemprop="url"][1]/@href',
                    MapCompose(make_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()

运行spider:scrapy crawl fast –s CLOSESPIDER_PAGECOUNT=10

可以看到,爬取了300个item,却只发送了10个request(因为在命令里指定了只爬取10个页面),效率提高了很多