爬虫Scrapy笔记(四)、spider

正主登场,前面我们介绍了爬虫开始前的一些外围工作,现在,我们终于开始讨论爬虫了。

class OschinaSpider(scrapy.Spider):
 ......
    def parse(self, response):
        pass

他也很简单,继承 Spider 类,我们只要实现 parse 接口就好了,parse 接受一个 response 参数。
在我们实现以前先理一下逻辑。我们首先需要从找到所有的文章,他的页面文章大致如下图:

其次,我们需要找到页面上具体元素的信息,如下图

我们只要解析好上面两组信息就好了,解析第一组信息是为了找到具体文章,之后把信息交给 scrapy 的 schedule 组件,通过Downloader去请求文章,之后再将结果返回给爬虫,解析第二组信息,就是具体要持久化的信息,这样一来就好办了。
我们来看下代码:


def parse(self, response): # 通过xpath定位文件,找到文章url链接 news_href = response.xpath( '//div[@class="page"]/div[@class="box vertical news"]/a[@class="news-link primary " or @class="news-link primary visited " ]/@href').extract() # 循环获取到的数据,让scrapy重新请求,过滤下,只请求news信息 for news in news_href: if news.startswith("https://www.oschina.net/news"): # 执行yield生成器,结果回调news_detail yield scrapy.Request(news, callback=self.news_detail) def news_detail(self, response): # 生成一个SampleItem容器 item = SampleItem() # 解析具体的字段 # 标题 item['title'] = response.xpath('//div[@class="article-detail"]/h2[@class="header"]/text()').extract() # 作者 item['author'] = response.xpath( '//div[@class="article-detail"]/div[@class="extra ui horizontal list meta-wrap"]/div[@class="item"]/a[@class="__user"]/span/text()').extract() # 收藏 item['collect'] = response.xpath( '//div[@class="article-detail"]/div[@class="extra ui horizontal list meta-wrap"]/div[@class="item collect-btn "]/span/text()').extract() # 评论 item['comment'] = response.xpath( '//div[@class="article-detail"]/div[@class="extra ui horizontal list meta-wrap"]/div[@class="item comment-count"]/a[@class="normal"]/span/text()').extract() # 执行yield生成器,让pipeline处理 yield item

现在来启动我们的爬虫


执行成功,我们来看下执行的结果

成功生成了数据

这样,我们就完成了一只爬虫的基本任务。之后我们就可以基于这些数据做一些分析,应用到相应的业务中去。总的来说,还是很简单的,最主要的是要动手,看着简单,时间操作中还是会遇到各种各样的问题。
做爬虫始终绕不开一个问题,就是反爬,反爬的技术有很多,需要具体问题具体分析。下一节,我们学学最简单的user-agent和代理ip。

爬虫Scrapy笔记(三)、pipelines

pipelines 主要是用于接收一个 item,然后处理这个 item,通过 pipeline 来决定是否需要继续处理还是存储起来,亦或是丢弃等等。
他也很简单,只要实现一个接口方法就可以

  def process_item(self, item, spider):

下面我们来实现这个方法,在之前我们需要做一些预处理,我们想把这些信心存储起来,简单起见,我们就把 item 存储在 sqlite 中。sqlite 是一个简单的数据库,不做过多介绍。
我们需要把爬虫的启动和关闭和数据库的启动和关闭关联起来,防止出现异常。
scrapy 中本身包含如下信号

#signals.py
engine_started = object()
engine_stopped = object()
spider_opened = object()
spider_idle = object()
spider_closed = object()
spider_error = object()
request_scheduled = object()
request_dropped = object()
response_received = object()
response_downloaded = object()
item_scraped = object()
item_dropped = object()

# for backwards compatibility
stats_spider_opened = spider_opened
stats_spider_closing = spider_closed
stats_spider_closed = spider_closed

item_passed = item_scraped

request_received = request_scheduled

这里使用了 dispatcher.connect 函数

class SamplePipeline(object):
       #文件名
    filename = 'oschina.sqlite'

    def __init__(self):
        self.conn = None
        # 添加信号分发器,控制数据库的连接与关闭
        dispatcher.connect(self.started, signals.engine_started)
        dispatcher.connect(self.stopped, signals.engine_stopped)

    # 初始换
    def started(self):
        # 如果路径存在,表示数据库已创建,连接数据库
        if path.exists(self.filename):
            self.conn = sqlite3.connect(self.filename)
            self.conn.text_factory = str
            # 否则表示数据库不存在,创建数据库,并创建相关表
        else:
            self.conn = self.init_table(self.filename)

    # 清理相关工作,提交事务,关闭连接
    def stopped(self):
        if self.conn is not None:
            self.conn.commit()
            self.conn.close()
            self.conn = None

    # 建表
    def init_table(self, filename):
        conn = sqlite3.connect(filename)
        # 这里的表结构和item相对应
        conn.execute("""create table oschina(
                        id integer primary key autoincrement,
                        title text,
                        author text,
                        collect int,
                        comment int
                        )""")
        conn.commit()
        return conn

    def process_item(self, item, spider):
        self.conn.execute(
            'insert or replace into oschina values((select id from oschina a where a.title=?),?,?,?,?)',
            (item['title'][0], item['title'][0], item['author'][0],  int(item['collect'][0]),
             int(item['comment'][0])))
        return item

这样,我们就完成了简单的数据持久化操作。注意一点的是,需要在setting.py中开启pipeline设置。

ITEM_PIPELINES = {
    当有多个Pipeline,可加在后面,后面数据表示执行顺序
    'sample.pipelines.SamplePipeline': 300,
}

接下里就等着正主登场了。下一章节,我们来看下如何驱动一只爬虫。

爬虫Scrapy笔记(二)、items

items,项目,顾名思义这个组件主要用来转储项目的,是一个转储容器,类似于 Java 里 POJO。

class SampleItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    pass

我们自定的这个 SampleItem,集成了 scrapy 框架中的 Item,来看下他的定义

class DictItem(MutableMapping, BaseItem):

    fields = {}

    def __init__(self, *args, **kwargs):
        self._values = {}
        if args or kwargs:  # avoid creating dict for most common case
            for k, v in six.iteritems(dict(*args, **kwargs)):
                self[k] = v

    def __getitem__(self, key):
        return self._values[key]

    def __setitem__(self, key, value):
        if key in self.fields:
            self._values[key] = value
        else:
            raise KeyError("%s does not support field: %s" %
                (self.__class__.__name__, key))

    def __delitem__(self, key):
        del self._values[key]

    def __getattr__(self, name):
        if name in self.fields:
            raise AttributeError("Use item[%r] to get field value" % name)
        raise AttributeError(name)

    def __setattr__(self, name, value):
        if not name.startswith('_'):
            raise AttributeError("Use item[%r] = %r to set field value" %
                (name, value))
        super(DictItem, self).__setattr__(name, value)

    def __len__(self):
        return len(self._values)

    def __iter__(self):
        return iter(self._values)

    __hash__ = BaseItem.__hash__

    def keys(self):
        return self._values.keys()

    def __repr__(self):
        return pformat(dict(self))

    def copy(self):
        return self.__class__(self)


@six.add_metaclass(ItemMeta)
class Item(DictItem):
    pass

Item 继承 DictItem,我们查看源码,姑且这样理解,它就是一个实现了字典的这样一个容器。
好了,我们也定义一下我们的容器,比如我们想知道每一篇文章的收藏情况以及评论的参与度,大致了解一下这篇文章是否受欢迎。我们定义一下我们需要的信息


class SampleItem(scrapy.Item): # define the fields for your item here like: # 文章标题 title = scrapy.Field() # 作者 author = scrapy.Field() # 发布日期 date = scrapy.Field() # 收藏 collect = scrapy.Field() # 评论 comment = scrapy.Field() pass

看来还是很简单的,接下来我们将创建一个 pipeline,来处理持久化我们的数据。

爬虫Scrapy笔记(一)、安装及初始化

因为业务的需要,最近需要研究爬虫实现相关功能。故此,新开一篇记录学习过程。
从网上查询相关的资料,母目前使用的比较多的开源框架可能要算 Scrapy 了。
百度百科是这样介绍的:

- Scrapy 是 Python 开发的一个快速、高层次的屏幕抓取和 web 抓取框架,用于抓取 web 站点并从页面中提取结构化的数据。Scrapy 用途广泛,可以用于数据挖掘、监测和自动化测试。

- Scrapy 吸引人的地方在于它是一个框架,任何人都可以根据需求方便的修改。它也提供了多种类型爬虫的基类,如 BaseSpider、sitemap 爬虫等,最新版本又提供了 web2.0 爬虫的支持。

Scrapy 架构介绍(摘自百度百科)

- Scrapy Engine(引擎):负责 Spider、ItemPipeline、Downloader、Scheduler 中间的通讯,信号、数据传递等。
- Scheduler(调度器):它负责接受引擎发送过来的 Request 请求,并按照一定的方式进行整理排列,入队,当引擎需要时,交还给引擎。
- Downloader(下载器):负责下载 Scrapy Engine(引擎)发送的所有 Requests 请求,并将其获取到的 Responses 交还给 Scrapy Engine(引擎),由引擎交给 Spider 来处理。
- Spider(爬虫):它负责处理所有 Responses,从中分析提取数据,获取 Item 字段需要的数据,并将需要跟进的 URL 提交给引擎,再次进入 Scheduler(调度器)。
- Item Pipeline(管道):它负责处理 Spider 中获取到的 Item,并进行进行后期处理(详细分析、过滤、存储等)的地方。
- Downloader Middlewares(下载中间件):一个可以自定义扩展下载功能的组件。
- Spider Middlewares(Spider 中间件):一个可以自定扩展和操作引擎和 Spider 中间通信的功能组件。

大概的工作流程如下:

- 爬虫引擎启动之后,爬虫根据初始的url经由Engine交给调度器;
- 调度器按照一定的逻辑排序,在通过Engine依赖于Downloader请求相关资源;
- Downloader将获取到的结果返回给爬虫来解析;
- 爬虫解析完成后,将一部分结果通过pipeline做持久化;
- 另一部需要继续爬取的任务再经由由Engine交给调度器;
- 按照上述逻辑重复执行,直到没有可以爬取的数据;
- 爬取停止,退出;

这样我们就有了一个初步的理解,理论部分结束。我们开始动手吧,实际使用加上阅读源码才能了解的更彻底。
参照相关的教程,大致如下:

安装 Scrapy,我本地因为使用的是 conda 环境,他可以自己解决各种依赖,很方便,直接安装就可以了,这里不再赘述。
安装好了之后,检查下版本 【scrapy version】,出现版本号,表示安装完成

接下来创建一个项目,我们命名为 sample,【scrapy startproject sample】,提示创建完成

我们看下创建的项目文件,和上面的组件基本上对应的,具体的功能不再赘述

这里有两个文件,一个是 settings,主要是做设置的,是项目中程序相关的配置,是一个全局配置。譬如我想配置一个常量,可以写在这个文件里。还有一个 scrapy.cfg 这样也是一个配置信息,主要是项目的相关信息。

当然了你也可以不使用命令行,你也可以手动创建,都是可以的,只不过这样麻烦一点而已。话说回来谁喜欢和自己过不去呢。

好了,接下来我们需要新建一只属于我们自己爬虫,本着开源学习的精神,我们以爬取开源中国 https://www.oschina.net/ 为例:

当然,这一步也可手动创建,其实也很简单,就是创建一个集成scrapy的子类

# -*- coding: utf-8 -*-
import scrapy


class OschinaSpider(scrapy.Spider):
    name = "oschina"
    allowed_domains = ["https://www.oschina.net/"]
    start_urls = ['http://https://www.oschina.net//']

    def parse(self, response):
        pass

爬虫的名字叫oschina,爬虫会爬取 https://www.oschina.net/ 域名下url,起始url是 https://www.oschina.net/ 。
这样一只爬虫就建好了,下一步我们将丰满这只爬虫,让它去爬取我们需要的信息。