Scrapy框架

一、构成

Scrapy 使用了 Twisted异步网络库来处理网络通讯。整体架构大致如下:

  • 引擎(Scrapy)
    用来处理整个系统的数据流处理, 触发事务(框架核心)
  • 调度器(Scheduler)
    用来接受引擎发过来的请求, 压入队列中, 并在引擎再次请求的时候返回. 可以想像成一个URL(抓取网页的网址或者说是链接)的优先队列, 由它来决定下一个要抓取的网址是什么, 同时去除重复的网址
  • 下载器(Downloader)
    用于下载网页内容, 并将网页内容返回给蜘蛛(Scrapy下载器是建立在twisted这个高效的异步模型上的)
  • 爬虫(Spiders)
    爬虫是主要干活的, 用于从特定的网页中提取自己需要的信息, 即所谓的实体(Item)。用户也可以从中提取出链接,让Scrapy继续抓取下一个页面
  • 项目管道(Pipeline)
    负责处理爬虫从网页中抽取的实体,主要的功能是持久化实体、验证实体的有效性、清除不需要的信息。当页面被爬虫解析后,将被发送到项目管道,并经过几个特定的次序处理数据。
  • 下载器中间件(Downloader Middlewares)
    位于Scrapy引擎和下载器之间的框架,主要是处理Scrapy引擎与下载器之间的请求及响应。
  • 爬虫中间件(Spider Middlewares)
    介于Scrapy引擎和爬虫之间的框架,主要工作是处理蜘蛛的响应输入和请求输出。
  • 调度中间件(Scheduler Middewares)
    介于Scrapy引擎和调度之间的中间件,从Scrapy引擎发送到调度的请求和响应。

Scrapy运行流程大概如下:

  1. 引擎从调度器中取出一个链接(URL)用于接下来的抓取。
  2. 引擎把URL封装成一个请求(Request)传给下载器。
  3. 下载器把资源下载下来,并封装成应答包(Response)。
  4. 爬虫解析应答包。
  5. 解析出实体(Item),则交给实体管道进行进一步的处理。
  6. 解析出的是链接(URL),则把URL交给调度器等待抓取。

二、选择器

from scrapy.selector import Selector

Selector(response=response).xpath('//a')  # 去所有的子代找a标签  "//"代表去所有子代找所有a标签

Selector(response=response).xpath('//a[@id="i1"]')  # 找到所有id=i1的a标签

Selector(response=response).xpath('//a[@href="link.html"][@id="i1"]')  # 多个条件属性的写法

Selector(response=response).xpath('//a[starts-with(@href, "link")]')  # 找到href以"link"开头的标签

Selector(response=response).xpath('//a[re:test(@id, "i\d+")]')  # 通过这则表达式进行匹配

Selector(response=response).xpath('//a[re:test(@id, "i\d+")]/text()') # 获取文本值

Selector(response=response).xpath('//a[re:test(@id, "i\d+")]/@href')  # 获取href属性

Selector(response=response).xpath('//a').extract()  # 加上extract就转换为文本

Selector(response=response).xpath('//a').extract_first()  # 第一个转换为文本

三、创建爬虫工程

scrapy相关命令:

scrapy startproject 项目名称   - 在当前目录中创建中创建一个项目文件,项目名称是唯一的
 
scrapy genspider [-t template] <name> <domain>   - 创建爬虫应用,生成一个py文件

scrapy list        - 展示爬虫应用列表
 
scrapy crawl 爬虫应用名称      - 运行单独爬虫应用,加上 --nolog参数不会打印过多的信息

scrapy check 项目名称     - 检查代码有没有问题

scrapy runspider 文件名称.py    - 运行程序和crawl一样

scrapy genspider -l   - 展示模板

四、各个文件说明

spiders.py

import scrapy
from scrapy.http import Request
from scrapy import Selector
from ..items import MyProjectItem

class SinaSpider(scrapy.Spider):
    name = 'sina'  # 这个名字必须写
    allowed_domains = ['quotes.toscrape.com']   # 可允许爬取范围
    start_urls = ['https://news.sina.com.cn/']  # 起始的url

    def parse(self, response):
        item = MyProjectItem()   # 声明item对象,用于格式化数据
        targets = Selector(response=response).xpath("//div[@id='ad_entry_b2']")
        for target in targets:
            item["href"] = target.xpath(".//a/@href").extract() 
            item["text"] = target.xpath(".//a/text()").extract()
            yield item   # 这句的作用就是把item放到pipline中进行处理
        yield Request(url="", callback=self.parse)  # 将url放到调度器中并没有真正的去下载  必须写

当执行爬虫的时候会默认找parse方法,如果不想让其执行parse方法就自定义,重写方法:

# 源码内容(在spiders里)
def start_requests(self):
    for url in self.start_urls:
        yield Request(url, dont_filter=True)
---------------------------------------------------------
# 进行重写
def start_requests(): 
    return Request(callback="")  # 通过重写start_requests指定回调函数

pipline.py(scrapy的数据持久化操作)

class MyProjectPipeline(object):  # 做持久化

    def process_item(self, item, spider):
        return item   # 只有写上这句话才会交给下一个pipline,否则不会交给下一个pipline
                      # 但是一般抛出一个异常来终止传递操作   
                      # from scrapy.exceptions import DropItem    raise DropItem()

    def open_spider(self, spider):
        # 爬虫开始时调用
        pass

    def close_spider(self, spider):
        # 爬虫关闭时调用
        pass

    @classmethod
    def from_crawler(cls, crawler):   # 创建对象、获取配置
        conn = crawler.settings.get("DB")   # crawler.settings就是指settings文件  get就是获取变量
        return cls(conn)   # 在这里拿到配置信息就能初始化对象

    def __init__(self,conn):
        self.conn = conn

settings.py(scrapy的配置文件),部分配置信息。

BOT_NAME = 'my_project'   # 蜘蛛名字 会放到USER_AGENT里面

# 爬虫目录  路径
SPIDER_MODULES = ['my_project.spiders']
NEWSPIDER_MODULE = 'my_project.spiders'

USER_AGENT = 'my_project (+http://www.yourdomain.com)'   # 请求头信息

ROBOTSTXT_OBEY = False   # 是否遵循协议

CONCURRENT_REQUESTS = 32  # 并发请求数最大32,默认是16

DOWNLOAD_DELAY = 3   # 下载延迟
CONCURRENT_REQUESTS_PER_DOMAIN = 16  # 针对每一个域名最多请求书
CONCURRENT_REQUESTS_PER_IP = 16   # 针对每一个IP最多请求书

COOKIES_ENABLED = False   #  是否自动爬取cookie
COOKIES_DEBUG = False   #  是否打印

TELNETCONSOLE_ENABLED = True   #进入交互用est命令 用于监听爬虫程序  端口默认6023

# 默认请求头
DEFAULT_REQUEST_HEADERS = {
  'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.26 Safari/537.36 Core/1.63.5383.400 QQBrowser/10.0.1313.400',
  'Accept-Language': 'en',
}

# 爬虫中间件
SPIDER_MIDDLEWARES = {
    'my_project.middlewares.MyProjectSpiderMiddleware': 543,
}

# 下载中间件
DOWNLOADER_MIDDLEWARES = {
    'my_project.middlewares.MyProjectDownloaderMiddleware': 543,
}

# scarpy自定义扩展
EXTENSIONS = {
   'scrapy.extensions.telnet.TelnetConsole': None,
   'my_project.MyExtension.Extend': 300,   #进行注册
}

# 定义pipline,会根据权重进行排序
ITEM_PIPELINES = {
   'my_project.pipelines.MyProjectPipeline': 300,
   'my_project.pipelines.MyProjectPipeline2': 200,
}

# 限速
AUTOTHROTTLE_ENABLED = True    # 是否启用限速

AUTOTHROTTLE_START_DELAY = 5   # 第一个请求延迟几秒

AUTOTHROTTLE_MAX_DELAY = 60   # 最大延迟几秒

AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0   # 上下波动

AUTOTHROTTLE_DEBUG = False

# 做缓存
HTTPCACHE_ENABLED = True           # 是否启用缓存
HTTPCACHE_EXPIRATION_SECS = 0      # 缓存超时时间
HTTPCACHE_DIR = 'httpcache'        # 缓存保存路径
HTTPCACHE_IGNORE_HTTP_CODES = []   # 缓存忽略的Http状态码 以后就忽略这些请求
HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'   # 缓存存储的插件
HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"    # 缓存策略

DEPTH_LIMIT = 2  # 控制访问深度  递归层数
DUPEFILTER_CLASS = "my_project.repeatFilter.Filter"   # 自定制去重的类
DEPTH_PRIORITY = 0  # 只能是0和1  是广度优先还是深度优先  1是广度优先

item.py(scrapy的数据格式化)

import scrapy

class MyProjectItem(scrapy.Item):
    # 在这里定义字段  做格式化。
    # 需要在spider里导入
    text = scrapy.Field()
    href = scrapy.Field()

middleware.py(scrapy的中间件)

下载中间件:

class MyProjectDownloaderMiddleware(object):

    @classmethod
    def from_crawler(cls, crawler):
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s

    # 爬虫进来执行这个方法 
    def process_request(self, request, spider):
        # 对于返回的参数
        # 返回一个None、后续的中间件去下载
        # 返回Response对象、停止process_request的函数执行,去执行process_response
        # 返回Request对象、停止中间件的执行,将Request重新调度
        # 抛出一个异常raise IgnoreRequest、停止执行process_request去执行process_exception

    # 爬虫离开执行这个方法
    def process_response(self, request, response, spider):
        # 必须返回response
        # 返回response、去执行其他中间件的process_response
        # 返回request对象停止中间件的执行,将Request重新调度
        # 抛出一个异常raise IgnoreRequest:停止执行process_request去执行request.err
        return response

    def process_exception(self, request, exception, spider):
        # 返回一个None、后续的中间件去处理异常
        # 返回response、停止后续的process_exception方法
        # 返回request对象停止中间件的执行,将Request重新调度

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

然后需要在settings文件中进行配置:

DOWNLOADER_MIDDLEWARES = {
   'my_project.middlewares.MyProjectDownloaderMiddleware': 543,
}

※:如果自己定义的中间件都不去进行下载,scrapy内部会有一个内置的中间件的操作。当执行完下载中间件以后就会交给spider中间件。

其他的内置下载中间件:

默认下载中间件
    {
        'scrapy.contrib.downloadermiddleware.robotstxt.RobotsTxtMiddleware': 100,
        'scrapy.contrib.downloadermiddleware.httpauth.HttpAuthMiddleware': 300,
        'scrapy.contrib.downloadermiddleware.downloadtimeout.DownloadTimeoutMiddleware': 350,
        'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': 400,
        'scrapy.contrib.downloadermiddleware.retry.RetryMiddleware': 500,
        'scrapy.contrib.downloadermiddleware.defaultheaders.DefaultHeadersMiddleware': 550,
        'scrapy.contrib.downloadermiddleware.redirect.MetaRefreshMiddleware': 580,
        'scrapy.contrib.downloadermiddleware.httpcompression.HttpCompressionMiddleware': 590,
        'scrapy.contrib.downloadermiddleware.redirect.RedirectMiddleware': 600,
        'scrapy.contrib.downloadermiddleware.cookies.CookiesMiddleware': 700,
        'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': 750,
        'scrapy.contrib.downloadermiddleware.chunked.ChunkedTransferMiddleware': 830,
        'scrapy.contrib.downloadermiddleware.stats.DownloaderStats': 850,
        'scrapy.contrib.downloadermiddleware.httpcache.HttpCacheMiddleware': 900,
    }

爬虫中间件:

class MyProjectSpiderMiddleware(object):

    @classmethod
    def from_crawler(cls, crawler):
        s = cls()
        crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
        return s
    
    # 当执行完下载中间件就执行,然后执行parse函数
    def process_spider_input(self, response, spider):
        return None
    
    # 当执行完parse函数以后执行该方法
    def process_spider_output(self, response, result, spider):
        # result就是yeild的内容
        # 必须返回包含request或item的可迭代对象
        for i in result:
            yield i
    
    # 处理异常的方法     
    def process_spider_exception(self, response, exception, spider):
        # 当返回的是None就会交给其他的中间件进行处理
        # 当返回Response或item就会交给Request或者pipline
        pass

    # 当爬虫刚开始运行时就会执行此方法
    def process_start_requests(self, start_requests, spider):
        for r in start_requests:
            yield r

    def spider_opened(self, spider):
        spider.logger.info('Spider opened: %s' % spider.name)

然后在settings中进行定义:

SPIDER_MIDDLEWARES = {
   'my_project.middlewares.MyProjectSpiderMiddleware': 543,
}

其他的爬虫中间件:

{
    'scrapy.contrib.spidermiddleware.httperror.HttpErrorMiddleware': 50,
    'scrapy.contrib.spidermiddleware.offsite.OffsiteMiddleware': 500,
    'scrapy.contrib.spidermiddleware.referer.RefererMiddleware': 700,
    'scrapy.contrib.spidermiddleware.urllength.UrlLengthMiddleware': 800,
    'scrapy.contrib.spidermiddleware.depth.DepthMiddleware': 900,
}

五、url的去重

scrapy有自己的一个去重的方法,但是我们也可以进行自定义。

class Filter(object):
    def __init__(self):
        # 可以在初始化操作中满足不同的需求,例如:数据库和缓存文件等操作
        pass

    # 这些函数只能叫这个名
    @classmethod
    def from_settings(cls, settings):   # 在这里导入配置文件  创建对象
        print("--------------")
        return cls()  # 在这里直接调用初始化方法,很好的解耦合

    def request_seen(self, request):  # 在这里判断是否去重
        return False

    def open(self):  # can return deferred   开始的时候执行
        pass

    def close(self, reason):  # can return a deferred   关闭的时候执行
        pass

    def log(self, request, spider):  # log that a request has been filtered
        pass
------------------------------------------------------
同时需要在配置文件中自定义去重的类

DUPEFILTER_CLASS = "my_project.repeatFilter.Filter"   # 路径

scrapy的指纹去重:

在 from scrapy.dupefilters import RFPDupeFilter

六、scrapy的调度器

Request(
    url=request,  # 请求的url
    callback=self.parse,    # 回调函数
    body=None,    # 请求体  这里body不能传字典格式的数据,需要字符串形式的数据
    headers={},    # 请求头
    method={},     # 请求方式
    cookies=,      # cookie    
)

七、scrapy的信号

首先需要在配置文件中注册

EXTENSIONS = {
   'scrapy.extensions.telnet.TelnetConsole': None,
   'my_project.MyExtension.Extend': 300,   # 进行注册
}

然后自定义一个类:

from scrapy import signals   # 导入信号


class Extend():
    def __init__(self,crawler):
        pass

    @classmethod
    def from_crawler(cls, crawler):
        obj = cls(crawler)
        crawler.signals.connect(obj.start, signals.engine_started)   # 注册一个信号(函数,信号)进行绑定
        return obj

    def start(self):   # 触发信号执行此函数
        print("engine_started")

scrapy中所有的信号:

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()
stats_spider_opened = spider_opened
stats_spider_closing = spider_closed
stats_spider_closed = spider_closed
item_passed = item_scraped
request_received = request_scheduled

八、scrapy交互模式

当配置文件 TELNETCONSOLE_ENABLED = True 时,可以在终端执行 telnet 127.0.0.1 6023(port),进入交互模式,所有的命令都在:

具体信息在scrapy.extensions.telnet.TelnetConsole路径下
------------------------------------------------------------

class TelnetConsole(protocol.ServerFactory):
    def _get_telnet_vars(self):
        # Note: if you add entries here also update topics/telnetconsole.rst
        telnet_vars = {
            'engine': self.crawler.engine,
            'spider': self.crawler.engine.spider,
            'slot': self.crawler.engine.slot,
            'crawler': self.crawler,
            'extensions': self.crawler.extensions,
            'stats': self.crawler.stats,
            'settings': self.crawler.settings,
            'est': lambda: print_engine_status(self.crawler.engine),
            'p': pprint.pprint,
            'prefs': print_live_refs,
            'hpy': hpy,
            'help': "This is Scrapy telnet console. For more info see: " \
                "https://doc.scrapy.org/en/latest/topics/telnetconsole.html",
        }
        self.crawler.signals.send_catch_log(update_telnet_vars, telnet_vars=telnet_vars)
        return telnet_vars

九、缓存

首先需要配置缓存存储的插件,也可以进行自定义:

def retrieve_response(self, spider, request):
    """Return response if present in cache, or None otherwise."""
    # 首先进行查看当前内容是否在缓存中

def store_response(self, spider, request, response):
    """Store the given response in the cache."""
    # 进行写入操作

在scrapy中有两种缓存策略,第一种是全部缓存

通过配置HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
------------------------------------------------------------
源码:
class DummyPolicy(object):

    def __init__(self, settings):
        self.ignore_schemes = settings.getlist('HTTPCACHE_IGNORE_SCHEMES')  # 从配置文件获取信息
        self.ignore_http_codes = [int(x) for x in settings.getlist('HTTPCACHE_IGNORE_HTTP_CODES')]

    def should_cache_request(self, request):
        # 如果不在要求忽略中
        return urlparse_cached(request).scheme not in self.ignore_schemes

    def should_cache_response(self, response, request):
        # 如果不在响应码中
        return response.status not in self.ignore_http_codes

第二种策略:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略

将配置设置为  HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"
------------------------------------------------------
def should_cache_response(self, response, request):
    cc = self._parse_cachecontrol(response)
    if b'no-store' in cc:
        return False
    elif response.status == 304:   # 如果是304直接返回
        return False
    elif self.always_store:
        return True
    elif b'max-age' in cc or b'Expires' in response.headers:  # 判断cookie
        return True
    elif response.status in (300, 301, 308):    # 判断是不是这些状态码
        return True
    elif response.status in (200, 203, 401):
        return b'Last-Modified' in response.headers or b'ETag' in response.headers
    else:
        return False

当然,也可以进行自定义的缓存策略。

源码在这里:from scrapy.contrib.throttle import AutoThrottle
-----------------------------------------------------------
算法的主要思想:
1. 获取最小延迟 DOWNLOAD_DELAY
2. 获取最大延迟 AUTOTHROTTLE_MAX_DELAY
3. 设置初始下载延迟 AUTOTHROTTLE_START_DELAY
4. 当请求下载完成后,获取其"连接"时间 latency,即:请求连接到接受到响应头之间的时间
5. 用于计算的... AUTOTHROTTLE_TARGET_CONCURRENCY
-----------------------------------------------------------
源码:
target_delay = latency / self.target_concurrency
new_delay = (slot.delay + target_delay) / 2.0 # 表示上一次的延迟时间  slot.delay是上一次的延迟
new_delay = max(target_delay, new_delay)
new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
slot.delay = new_delay  # 进行赋值

十一、代理

scrapy的默认代理是

from scrapy.contrib.downloadermiddleware.httpproxy import HttpProxyMiddleware
----------------------------------------------------------
源码:
class HttpProxyMiddleware(object):

    def __init__(self, auth_encoding='latin-1'):
        self.auth_encoding = auth_encoding
        self.proxies = {}
        for type, url in getproxies().items():   # 相当于os.environ()
            self.proxies[type] = self._get_proxy(url, type)

重写process_request()方法

class xxx:
    def process_request(self, request, spider):
        # 需要注意这两个都必须是字节,headers必须经过base64加密
        request.meta['proxy'] = proxy      #  将代理进行赋值给request
        request.headers['Proxy-Authorization'] = b'Basic ' + creds  # 这里必须以Basic开头

------------------------------------------------
然后在settings里面注册

DOWNLOADER_MIDDLEWARES = {
    'my_project.middlewares.xxx': 543,   # 进行注册
}

十二、cookie

在scrapy中的cookie验证需要导入

from scrapy.http.cookies import CookieJar   # 做cookie验证

cookie_jar = CookieJar()  # 直接进行实例化一个cookie容器
cookie_jar.extract_cookies(response, response.request)   # 需要两个参数response和request
cookie_jar._cookies   # 拿到所有的cookie

也可以在settings配置文件中进行设置自动爬取所有的cookie。

十三、证书

在scrapy中进行证书的配置时,scrapy的原生配置

# 这两个是关于证书的配置 默认不可见  这样仅仅支持买来的证数
DOWNLOADER_HTTPCLIENTFACTORY = "scrapy.core.downloader.webclient.ScrapyHTTPClientFactory"
DOWNLOADER_CLIENTCONTEXTFACTORY = "scrapy.core.downloader.contextfactory.ScrapyClientContextFactory"

当然还可以自定义证书:

class MySSLFactory(ScrapyClientContextFactory):  # 继承这个类重写它的方法
    def getCertificateOptions(self):
        from OpenSSL import crypto  # 需要导入这个类
        v1 = crypto.load_privatekey(crypto.FILETYPE_PEM, 
open('/Users/wupeiqi/client.key.unsecure', mode='r').read())
        v2 = crypto.load_certificate(crypto.FILETYPE_PEM, open('/Users/wupeiqi/client.pem', mode='r').read())
        return CertificateOptions(
            privateKey=v1,  # pKey对象
            certificate=v2,  # X509对象
            verify=False,
            method=getattr(self, 'method', getattr(self, '_ssl_method', None))
        )

源码流程:(源码在 scrapy.core.downloader.contextfactory.ScrapyClientContextFactory 中)

 

十四、自定义scrapy的命令

在spiders同级创建任意目录,如:commands,在其中创建文件,此处文件名就是自定义的命令。

from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings


class Command(ScrapyCommand):

    requires_project = True

    def syntax(self):
        return '[options]'

    # 简短的文字说明
    def short_desc(self):
        return 'Runs all of the spiders'

    def run(self, args, opts):
        # 找到所有的爬虫
        spider_list = self.crawler_process.spiders.list()
        for name in spider_list:
            # name就是爬虫的名字
            self.crawler_process.crawl(name, **opts.__dict__)
        self.crawler_process.start()

然后在setts里面注册一下:

COMMANDS_MODULE = '项目名称.目录名称'

就可以执行命令:

scrapy  文件名 [参数]

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值