爬虫之Scrapy架构

Scrapy架构介绍

Scrapy是适用于Python的一个快速、高层次的屏幕抓取和web抓取框架,其最初是为了页面抓取 (更确切来说, 网络抓取 )所设计的,使用它可以以快速、简单、可扩展的方式从网站中提取所需的数据。但目前Scrapy的用途十分广泛,可用于如数据挖掘、监测和自动化测试等领域,也可以应用在获取API所返回的数据(例如 Amazon Associates Web Services ) 或者通用的网络爬虫

image

引擎(EGINE)

引擎负责控制系统所有组件之间的数据流,并在某些动作发生时触发事件。

调度器(SCHEDULER)

用来接受引擎发过来的请求,压入队列中,并在引擎再次请求的时候返回。可以想像成一个URL的优先级队列,由它来决定下一个要抓取的网址是什么,同时去除重复的网址。

下载器(DOWLOADER)

用于下载网页内容,并将网页内容返回给EGINE,下载器是建立在twisted这个高效的异步模型上的。

爬虫(SPIDERS)

SPIDERS是开发人员自定义的类,用来解析responses,并且提取items,或者发送新的请求。

项目管道(ITEM PIPLINES)

在items被提取后负责处理它们,主要包括清理、验证、持久化(比如存到数据库)等操作。

下载器中间件(Downloader Middlewares)

位于Scrapy引擎和下载器之间,主要用来处理从EGINE传到DOWLOADER的请求request,已经从DOWNLOADER传到EGINE的响应response。

爬虫中间件(Spider Middlewares)

位于EGINE和SPIDERS之间,主要工作是处理SPIDERS的输入(即responses)和输出(即requests)。

Scrapy下载

安装:

pip install scrapy

如果安装失败,就需要走下面的流程:

1.安装wheel,之后通过wheel文件安装软件,wheel文件官网

pip install wheel
pip install lxml
pip install pyopenssl

3.下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/

4.下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted

5.twisted的wheel文件通过pip安装

pip install 下载目录/Twisted-17.9.0-cp36-cp36m-win_amd64.whl

6.安装scrapy

pip install scrapy

Scrapy基本使用

创建项目

通过cmd窗口:在当前目录下创建项目

scrapy startproject 项目名

爬虫创建

在项目目录下,打开cmd窗口:

scrapy genspider 爬虫名 爬虫地址

比如创建爬博客园的爬虫:

scrapy genspider cnblogs cnblogs.com

此时会在项目spiders文件夹下创建cnblogs.py

import scrapy

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['cnblogs.com']  # 允许爬取的地址
    start_urls = ['http://cnblogs.com/']  # 起始爬取的地址
    def parse(self, response):  # response就是爬取结果
        # 爬取完之后的代码,解析代码放在这里
        print(response.text)

爬虫启动方式一

--nolog代表不要日志输出

scrapy crawl 爬虫名
scrapy crawl 爬虫名 --nolog

比如启动刚刚创建的博客园爬虫:

scrapy crawl cnblogs --nolog

爬虫启动方式二

项目路径下新建py文件:

from scrapy.cmdline import execute
execute(['scrapy','crawl','爬虫名','--nolog'])

启动爬虫运行这个py文件即可。

Scrapy目录结构

项目名
 ├── 项目同名文件夹
 ├    ├── spiders -- 文件夹,专门存放爬虫文件
 ├    ├    ├── 爬虫1.py -- 其中一个爬虫,重点写代码的地方
 ├    ├    └── 爬虫2.py -- 其中一个爬虫,重点写代码的地方
 ├    ├── items.py -- 类比djagno的models,表模型--->类
 ├    ├── middlewares.py -- 爬虫中间件和下载中间件都在里面
 ├    ├── pipelines.py -- 管道,做持久化需要在这写代码
 ├    └── settings.py -- 配置文件
 └── scrapy.cfg -- 上线配置,开发阶段不用

Scrapy解析数据

创建爬虫时会自动在spiders下创建对应的爬虫名py文件,里面一个类,类中有一个解析数据用的方法parse(self, response):

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['cnblogs.com']  # 允许爬取的地址
    start_urls = ['http://cnblogs.com/']  # 起始爬取的地址

    def parse(self, response):  # response就是爬取结果
        # 爬取完之后的代码,解析代码放在这里
        print(type(response))

response对象的方法:

方法 作用
response.css('css选择器') 根据css选择器选择标签
response.css('css选择器::text') 获取标签文本内容
response.css('css选择器::attr(class)') 获取标签class属性
response.xpath('xpath') 根据xpath选择标签
response.xpath('xpath/text()') 获取标签文本内容
response.xpath('xpath/@class') 获取标签class属性
取个数
response.css().extract_first() 根据css选择器选择标签,取一个
response.css().extract() 根据css选择器选择标签,取全部

举例:获取博客园首页博客标题

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['cnblogs.com']  # 允许爬取的地址
    start_urls = ['http://cnblogs.com/']  # 起始爬取的地址

    def parse(self, response):  
        print(response.css('.post-item-title::text').extract())

settings相关配置

基础配置

# 是否遵循爬虫协议
ROBOTSTXT_OBEY = False
# 输出日志级别
LOG_LEVEL='ERROR'
# 客户端类型
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'
# 默认请求头
DEFAULT_REQUEST_HEADERS = {
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en',
}
# 爬虫中间件
SPIDER_MIDDLEWARES = {
    '项目名.middlewares.项目名SpiderMiddleware': 543,
}
# 下载中间件
DOWNLOADER_MIDDLEWARES = {
    '项目名.middlewares.项目名DownloaderMiddleware': 543,
}
# 持久化配置
ITEM_PIPELINES = {
    '项目名.pipelines.项目名Pipeline': 300,
}

增加爬虫的爬取效率

1.增加并发:

默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改:并发设置成了为100

CONCURRENT_REQUESTS = 100

2.降低日志级别:

在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:

LOG_LEVEL = 'ERROR'

3.禁止cookie:

如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:

COOKIES_ENABLED = False

4 禁止重试:

对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:

RETRY_ENABLED = False

5.减少下载超时:

如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:

DOWNLOAD_TIMEOUT = 10  # 超时时间为10s

去重规则(布隆过滤器)

scrapy中 实现了去重,爬过的网址不会再爬。

原理:每次根据爬取的地址对象request生成一个指纹(唯一),判断是否在集合中,如果在集合中,就不爬取了,如果不在,就爬取并且把生成的指纹(唯一)放到集合中。

源码剖析

在scrapy自己的配置文件中配置了一个过滤器:

DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'

RFPDupeFilter类中有一个方法request_seen:集合去重

def request_seen(self, request: Request) -> bool:
    # 生成指纹
    fp = self.request_fingerprint(request)
    # self.fingerprints是一个集合
    # 判断指纹是否存在
    if fp in self.fingerprints:
        # 返回True就说明重复了,不爬取这个地址了
        return True
    self.fingerprints.add(fp)
    if self.file:
        self.file.write(fp + '\n')
    return False

布隆过滤器

极小内存校验是否重复

https://zhuanlan.zhihu.com/p/94668361

可以自己写一个类,替换掉内置的去重:

class MyRFPDupeFilter(RFPDupeFilter):
    fingerprints=布隆过滤器

持久化方案(数据保存)

方式一:保存到文件

解析函数parse,需要返回 [{}, {}] 的格式:

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['cnblogs.com']  # 允许爬取的地址
    start_urls = ['http://cnblogs.com/']  # 起始爬取的地址

    def parse(self, response):  # response就是爬取结果
        # 爬取完之后的代码,解析代码放在这里
        return [{'title': v} for v in response.css('.post-item-title::text').extract()]

然后终端命令:

scrapy crawl 爬虫名 -o 文件名(json,pkl,csv等结尾)

方式二:pipline模式

1.在items.py中写一个类,继承scrapy.Item

2.在类中写属性:

import scrapy

class CnblogsItem(scrapy.Item):
    title = scrapy.Field()

3.在爬虫中导入类,实例化得到对象,把要保存的数据放到对象中

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['cnblogs.com']  # 允许爬取的地址
    start_urls = ['http://cnblogs.com/']  # 起始爬取的地址

    def parse(self, response):  # response就是爬取结果
        from ..items import CnblogsItem

        item = CnblogsItem()
        for title in response.css('.post-item-title::text').extract():
            item['title'] = title
            # 变成生成器
            yield item

4.修改配置文件,指定pipline,数字表示优先级,越小越大

ITEM_PIPELINES = {
    '项目名.pipelines.CrawlCnblogsPipeline': 300,
}

5.在pipelines.py中写一个pipline,和配置文件中的名字对应

class CrawlCnblogsPipeline:
    # 数据初始化,打开文件,打开数据库链接
    def open_spider(self, spider):
        # spider: 即爬虫对象
        # 打开文件
        self.f = open('cnblogs.txt', 'w', encoding='utf-8')

    # 真正存储的地方,一定不要忘了return item,交给后续的pipline继续使用
    def process_item(self, item, spider):
        # 存储
        self.f.write('标题:' + item['title'] + '\n')
        return item

    # 销毁资源,关闭文件,关闭数据库链接
    def close_spider(self, spider):
        # spider: 即爬虫对象
        # 关闭文件
        self.f.close()

request和response传递参数

传递参数需要使用Request方法。

from scrapy.http import Request
Request(url=爬虫, callback=调用哪个爬虫函数, meta=参数)

举例:博客园爬取首页标题和文章详情

爬虫代码:

import scrapy
from scrapy.http import Request

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['cnblogs.com']  # 允许爬取的地址
    start_urls = ['http://cnblogs.com/']  # 起始爬取的地址

    def parse(self, response):  # response就是爬取结果
        from ..items import CnblogsItem

        for title in response.css('.post-item-title'):
            # 要写在里面
            item = CnblogsItem()
            # 标题
            item['title'] = title.css('::text').extract_first()
            # 文章详情地址
            url = title.css('::attr(href)').extract_first()
            # 爬取文章详情地址,把item对象传过去修改
            yield Request(url=url, callback=self.parser_detail, meta={'item': item})

    def parser_detail(self, response):
        item = response.meta.get('item')
        # 文章内容
        content = response.css('#post_detail').extract_first()
        item['content'] = content
        print('一篇文章完成')
        yield item

pipelines.py:(记得添加到配置文件中)

class CrawlCnblogsPipeline:
    # 数据初始化,打开文件,打开数据库链接
    def open_spider(self, spider):
        # spider: 即爬虫对象
        # 打开文件
        self.f = open('cnblogs.txt', 'w', encoding='utf-8')

    # 真正存储的地方,一定不要忘了return item,交给后续的pipline继续使用
    def process_item(self, item, spider):
        # 存储
        self.f.write('标题:' + item['title'] + '\n')
        self.f.write('内容:' + item['content'] + '\n')
        return item

    # 销毁资源,关闭文件,关闭数据库链接
    def close_spider(self, spider):
        # spider: 即爬虫对象
        # 关闭文件
        self.f.close()

items.py:

import scrapy

class CnblogsItem(scrapy.Item):
    title = scrapy.Field()  # 标题
    content = scrapy.Field()  # 文章内容

网页解析下一页继续爬取

import scrapy
from scrapy.http import Request

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'  # 爬虫名字
    allowed_domains = ['cnblogs.com']  # 允许爬取的地址
    start_urls = ['http://cnblogs.com/']  # 起始爬取的地址

    def parse(self, response):  # response就是爬取结果
        from ..items import CnblogsItem

        item = CnblogsItem()
        for title in response.css('.post-item-title::text').extract():
            item['title'] = title
            yield item
        # 下一页的标签的href属性获取
        next_url = response.css('div.pager>a:last-child::attr(href)').extract_first()
        next_url = 'https://www.cnblogs.com' + next_url
        print('准备爬取 ' + next_url)
        # 把下一页的爬取继续交给当前self.parse方法
        yield Request(url=next_url, callback=self.parse)

爬虫和下载中间件

配置文件中:

# 爬虫中间件
SPIDER_MIDDLEWARES = {
    '项目名.middlewares.项目名SpiderMiddleware': 543,
}
# 下载中间件
DOWNLOADER_MIDDLEWARES = {
    '项目名.middlewares.项目名DownloaderMiddleware': 543,
}

用的比较多的是下载中间件,里面的两个方法:

class DownloaderMiddleware:
    # 请求来的时候
    def process_request(self, request, spider):
        # - return None: 继续执行下一个中间件的process_request
        # - return a Response object :直接返回给engin,去解析
        # - return a Request object :给engin,再次被放到调度器中
        # - raise IgnoreRequest: 执行 process_exception()方法
        return None
    # 响应走的时候
    def process_response(self, request, response, spider):
        # - return a Response :继续走下一个中间件的process_response,给engin,进爬虫解析
        # - return a Request :给engin,进入调度器,等待下一次爬取
        # - raise IgnoreRequest:抛异常
        return response

加代理,cookie,header,集成selenium

代理、cookie、header都是在下载中间件中添加

代理

def process_request(self, request, spider):
        print('下载中间件:',request)
        # 代理
        request.meta['proxy'] = 'http://221.6.215.202:9091'
        return None

cookie

def process_request(self, request, spider):
        print('下载中间件:',request)
        # cookie
        request.cookies= {}
        # 或者
        request.cookies['name']='value'
        return None

请求头:

def process_request(self, request, spider):
        print('下载中间件:',request)
        # 请求头
        request.headers['Auth']='asdfasdfasdfasdf'
        request.headers['USER-AGENT']='ssss'
        return None

随机生成user-agent

pip install fake_useragent
from fake_useragent import UserAgent

ua = UserAgent()
print(ua.ie)  # 随机打印ie浏览器任意版本
print(ua.firefox)  # 随机打印firefox浏览器任意版本
print(ua.chrome)  # 随机打印chrome浏览器任意版本
print(ua.random)  # 随机打印任意厂家的浏览器

集成selenium

在爬虫类中集成:

import scrapy
from selenium import webdriver

class CnblogsSpider(scrapy.Spider):
    name = 'cnblogs'
    allowed_domains = ['cnblogs.com'] 
    start_urls = ['http://cnblogs.com/'] 
    # 添加属性
    driver = webdriver.Chrome()
    def parse(self, response): 
        print(response)
    # 关闭触发
    def close(self, reason):
        self.driver.close()

下载中间件中:

def process_request(self, request, spider):
    """spider就是爬虫类对象"""
    from scrapy.http import HtmlResponse
    spider.driver.get(url=request.url)
    response = HtmlResponse(url=request.url, body=spider.driver.page_source.encode('utf-8'), request=request)
    return response

scrapy-redis实现分布式爬虫

第一步:安装scrapy-redis

pip install scrapy-redis

第二步:改造爬虫类

from scrapy_redis.spiders import RedisSpider
class CnblogSpider(RedisSpider):
    name = 'cnblog_redis'
    allowed_domains = ['cnblogs.com']
    # 写一个key:redis列表的key,起始爬取的地址
    redis_key = 'myspider:start_urls'

第三步:配置文件配置

# 分布式爬虫配置
# 去重规则使用redis
REDIS_HOST = 'localhost'  # 主机名
REDIS_PORT = 6379  # 端口
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 持久化:文件,mysql,redis
ITEM_PIPELINES = {
    'scrapy_redis.pipelines.RedisPipeline': 400,
}

第四步:在多台机器上启动scrapy项目

第五步:把起始爬取的地址放到redis的列表中

lpush myspider:start_urls http://www.cnblogs.com/
posted @ 2022-08-03 20:22  Yume_Minami  阅读(111)  评论(0编辑  收藏  举报