scrapy框架的使用

前记:爬虫框架部分整理完成,后续慢慢完善,

声明:

   1)仅作为个人学习,如有冒犯,告知速删!

   2)不想误导,如有错误,不吝指教!

创建工程:

  • scrapy startproject name

    • cd proName

      • scrapy genspider spiderName urlName(限制爬虫的爬取范围)

      • 执行:scrapy crawl spiderName

start_urls = []=====>列表中的元素会被scrapy自动进行发送--几个url,请求

setting必备设置:

  • robotstxt_obey = False

  • USER_AGENT = ' '

  • LOG_LEVEL = 'ERROR'

  • LOG_FILE = 'log.txt'----一般不用

scrapy框架中:

  • xpath返回的列表中的列表元素是selector对象,我们需要解析获取的字符串的数据

  • 必须经过一个extract() 操作才可以将该对象汇总存储的字符串的数据获取

.extract()----列表

.extract_first()-----字符串

---问题:xpath返回的列表中的列表元素有多个(selector对象),想要将每个列表元素对应的selector中的字符串取出--------使用 extract()

scrapy数据解析

  • xpath语法

    • 通过返回的response数据可以直接直接进行Xpath解析。

scrapy持久化存储

  • 基于终端指令:

    • 只可以将parse方法的返回值存储到电脑磁盘中

      • scrapy crwal first -o file.csv-------->将当前返回值存储到file文件中csv\json

  • 基于管道:>pipelines.py

    • 编码流程

        1. 数据解析(在爬虫类中)

        2. 在item的类中定义相关的属性(爬虫类)

        3. 将解析的数据存储封装到item类型的对象中;

        4. 将item对象提交给管道 yield

        5. 在管道类中process_item方法负责接收item对象,然后对item进行任意形式的格式持久化存储

        6. 在settings中设置MySQL的配置信息

          1.  # 如下所示
             # mysql 配置  
             MYSQL_DB_NAME = 'scrapy_db'
             MYSQL_HOST = '127.0.0.1'
             MYSQL_USER = 'root'
             MYSQL_PASSWORD = '123456'
  • 细节补充:

    • 管道文件中的一个管道类表示将数据存储到某一个形式的平台中。

    • 如果管道文件中定义多个管道类,爬虫类提交的item的操作会给到优先级最高的管道类,只有优先级最高的管道类才可以接受到item,剩下的管道类是需要从优先级最高的管道中接受item;

    • process_item 方法的实现中的return item的操作表示item传递给下一个即将被执行的管道类

手动请求发送:

yield scrapy.Request(url,callback)-----发送的get请求

对yield总结:

  1). 向管道提交item的时候:yield item

  2). 手动请求发送:yeld scrapy.Request(url,callback)

手动发送post请求:

  yield scrapy.FormRequest(url,formdata,callback):formdata是一个字典表示请求参数

scrapy五大核心组件:

  - 略(网上都有)

scrapy的请求传参:

  1.   - 作用:实现深度爬取
      - 使用场景:使用scrapy爬取的数据没有存在同一张页面的数据
      - 传递item:使用meta的技巧----yield scrapy.Request(url,callback,meta)
      - 接受itemresponse.meta

提升scrapy爬取数据的效率(配置文件中):

- 增加并发:

  CONCURRENT_REQUESTS = 100

- 降低日志级别:

  LOG_LEVEL='ERROR'

- 禁止cookies:

  COOKIES_ENABLE = True

-禁止重试:

  retry_ebable = False

- 减少下载超时:

  DOWNLOAD_TIMEOUT = 3----请求超过3,丢弃,

scrapy的中间件(middlewares):

--概念:下载中间件

  --->处于引擎和下载器

--封装了两个类:

  NameDownloaderMiddleware;NameSpiderMiddleware

--一次请求经过两次中间件:

  ---必须经过引擎:

  • 首先由调度器(scheduler)到下载器(downloader)之间可以进行request的修改与设置;

  • 再由下载器(downloader)到主爬虫(spider)之间可以进行response的修改与设置;

-- 批量拦截所有的请求响应

-- 拦截请求

  -- 篡改请求的头信息(UA伪装)

  -- 篡改请求对应的ip(代理)

-- 拦截响应:

  -- 篡改响应数据,篡改响应对象

下载器中间件中的核心方法(NameDownloaderMiddleware):

  • 位置:处于scrapy的Request和Response之间的处理模块;

  1. process_request---拦截正常请求,并修改与设置

    • 进行ua伪装:resquest.headers["User-Agent"] = "xxx "  

      • 随机更换UA

          1. 1 #middlewares中间件重写,记得开启该中间件
          2. 2 from scrapy import signals
          3. 3 import random
          4. 4 from xbhog.settings import USER_AGENTS_LIST
          5. 5
          6. 6 class UserAgentMiddleware(object):
          7. 7
          8. 8 def process_request(self,request,spider):
          9. 9 #设置随机请求头
          10. 10 ua = random.choice(USER_AGENTS_LIST)
          11. 11 #设置初始URL中的UA
          12. 12 request.headers['User-Agent'] = ua
          1. 1 #settings设置
          2. 2 USER_AGENTS_LIST = [
          3. 3 "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
          4. 4 "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
          5. 5 "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
          6. 6 "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
          7. 7 "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
          8. 8 "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
          9. 9 "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
          10. 10 "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5"
          11. 11 ]
      • 有一个fake-useragent,自行百度下用法;

        • 见仁见智

    • 进行代理设置:resquest.meta["proxy"] = proxy

      • 代理池中随机选择代理ip

      • 代理ip的webapi发送请求获取一个代理ip

      • 添加代理:基本认证与摘要认证

      • source:一文读懂HTTP Basic身份认证https://juejin.im/entry/5ac175baf265da239e4e3999

        1. 1 #settings设置 PROXY_LIST = {"ip_port":"ip:port","user_pass":"user:pass"}
        1. 1 #付费代理
        2. 2 import base64
        3. 3
        4. 4 # 代理隧道验证信息 这个是在那个网站上申请的
        5. 5 proxyServer = 'http://proxy.abuyun.com:9010' # 收费的代理ip服务器地址,这里是abuyun
        6. 6 proxyUser = 用户名
        7. 7 proxyPass = 密码
        8. 8 #Basic后面有一个空格
        9. 9 proxyAuth = "Basic " + base64.b64encode(proxyUser + ":" + proxyPass)
        10. 10
        11. 11 class ProxyMiddleware(object):
        12. 12 def process_request(self, request, spider):
        13. 13 # 设置代理
        14. 14 request.meta["proxy"] = proxyServer
        15. 15 # 设置认证
        16. 16 request.headers["Proxy-Authorization"] = proxyAuth
      • 返回三种方式:

        • 返回None值:没有return也是返回None,该request对象传递给下载器,或通过引擎传递给其他权重低的process_request方法

        • 返回Response对象:不再请求,把response返回给引擎j交给spider解析

        • 返回Request对象:把request对象通过引擎交给调度器,此时将不通过其他权重低的process_request方法。

    • scrapy与selenium中间件的使用:

      • 可以选择性的使用selenium,因为selenium比较慢,我们尽可能的避免使用它,在下载器中间件里面的process_request函数,可以理解所有的请求都要经过该函数,所以我们在修改请求时,可以判断所过的url里面有没有我们需要用selenium的url,一般可以使用url里面的关键字进行判断;

        1. 1 #判断关键字是否在url中
        2. 2 if "xxxxx" in resquest.url:
        3. 3 driver = webdriver.Chorme()
        4. 4 driver.get(url)
        5. 5 time.sleep(3)
        6. 6
        7. 7 data = driver.page_source
        8. 8
        9. 9 driver.close()
        10. 10 #创建相应对象
        11. 11 res = HtmlResponse(url=url,body=data,encoding='utf-8',request=request)
        1. 1 from scrapy.http import HtmlResponse
        2. 2 import time
        3. 3
        4. 4
        5. 5 #设置selenium的中间件
        6. 6 class SelemiumSpiderMiddleware(object):
        7. 7
        8. 8 def process_request(self, request, spider):
        9. 9 spider.driver.get(request.url)
        10. 10 time.sleep(1)
        11. 11 #获得渲染后的网页源代码
        12. 12 page_text = spider.driver.page_source
        13. 13 spider.driver.close()
        14. 14 #创建响应对象
        15. 15 return HtmlResponse(url=request.url, body=page_text, request=request, encoding='utf-8')
  1. process_response--拦截所有的响应

    • 在downloader执行Request下载后,会得到对应的response,在发送到spider之前,可以使用process_response来对数据进行处理。

  1. process_exception--拦截发生异常的请求对象

    • 当downloader或者process_request()方法抛出异常是,该方法会被调用;

spider中间件:(NameSpiderMiddleware):

  • spider Middleware是接入到scrapy的Spider处理机制的钩子框架;

  • 在Downloader生成Response之后,Response会被发送到Spider之前,首先经过SpiderMiddleware处理,当spider处理生成item和Request之后,还会经过SpiderMiddleware处理。

  • 暂时没有用到,只大体学习记录;

--四大核心方法(百度自行了解):

  -process_spider_input
  -process_spider_output
  -process_spider_exception
  -process_start_requests

CrawlSpider:

  1.  - 基于scrapy进行全站数据爬取的一种新的手段
  • CrawlSpider就是spider的一个子类

    • 链接提取器(LinkExtractor):

      • 规则解析器(Rule):

  • 使用流程

    • 新建一个工程

    • cd 工程中

    • 新建一个爬虫文件:scrapy genspider -t crawl spiderName www.xxx.com

  • 样例:

    1. 1 #demo
    2. 2 # -*- coding: utf-8 -*-
    3. 3 import scrapy
    4. 4 from scrapy.linkextractors import LinkExtractor
    5. 5 from scrapy.spiders import CrawlSpider, Rule
    6. 6
    7. 7
    8. 8 class CrawlproSpider(CrawlSpider):
    9. 9 name = 'Crawlpro'
    10. 10 allowed_domains = ['www.xxx.com']
    11. 11 start_urls = ['http://www.xxx.com/']
    12. 12
    13. 13 rules = (
    14. 14 Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
    15. 15 )
    16. 16
    17. 17 def parse_item(self, response):
    18. 18 item = {}
    19. 19 #item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
    20. 20 #item['name'] = response.xpath('//div[@id="name"]').get()
    21. 21 #item['description'] = response.xpath('//div[@id="description"]').get()
    22. 22 return item
    23. 23

分布式:

-- 目的:

  - 让多台爬虫机器同时运行爬虫任务并协同爬取,协同爬取的前提是共享爬取队列;

  - 统一爬取队列,多个调度器、多个下载器----结果是爬取效率翻倍。

-- 维护爬虫队列:

- 性能考虑:基于内存存储的redis

  1. 列表有lpush、lpop、rpush、rpop方法,实现先进先出爬取队列,也可以实现先进后出栈式爬取队列

  2. 集合元素无需并且不重复

  3. 可实现带优先级的调度队列

-- URL地址去重:

  • 使用md5生成的数据指纹来筛选数据,将转换的md5值与之前的传入redis中的数据进行比对;

  • 使用哈希算法生成数据指纹筛选数据,将转换的md5值与之前的传入redis中的数据进行比对;

  • 布隆过滤器

-- 文本内容去重:

  1. 编辑距离算法

  2. simhash算法

-- 防止中断:

  1. 为什么要防止中断:

    1. 在scrapy中,爬虫运行时的Request队列放在内存中,在爬虫运行中中断后,这个空间就被释放,此队列就被销毁,所以一旦爬虫被中断,爬虫再次运行就相当于全新的爬取过程。

  2. 解决方法:

    1. 将队列中的Request保存起来,再次爬取直接读取保存的数据即可

    2. 实现命令:

      1.  scrapy crawl spider -s JOB_DIP=crawls/spider(路径使用JOB_DIP标识)

增量式:

  • 概念:用于检测网站数据更新的情况。

  • 应用的网站类型:

    • 增量式深度爬取

    • 增量式非深度爬取

  • 核心机制:去重;redis-set去重方式(可做持久化存储),python中的set是基于缓存中的

  • 增量式流程:

      1.  与基本的爬虫Scrapy流程相似,只是再spider中加了数据指纹的认证
         pipelines中增加了redis的存储过程
    • rules配置规则

      • 数据库的连接

      1. item数据类型创建完成

      2. pipelines

        • 数据库的调用:spider.方法

        • redis的存储

  • 框架中的item传输问题(parse传到其他函数中):

    • 传输:

      1.  
      1. 1 yield scrapy.Resquest(url,callback=self.parse_detail,meta={'item':item})
    • 下个函数接收:

      1. 1 item = response.meta['item']
      2. 2 item[""] = ...
      3. 3 yield item

scrapy中的post请求:

首先创建好scrapy文件,注释掉allowed_domains以及start_urls;重写初始化请求(start_requests),最后yield返回给解析函数。

  1. 1 class xxx(scrapy.Spider):
  2. 2 name = 'xxxx'
  3. 3 #allowed_domains = ['']
  4. 4 #start_url = ['']
  5. 5
  6. 6 def start_requests(self):
  7. 7 headers = {}
  8. 8 base_url = ''
  9. 9 formdata = {}
  10. 10 yield scrapy.FormRequest(url=base_url,headers=headers,formdata=formdata,callback=self.parse)
  11. 11 #如果使用FormRequest报错,备选方案
  12. 12 scrapy.Request(url=base_url,headers=headers,body=json.dumps(formdata),method= 'POST',callback=self.parse)
  13. 13
  14. 14
  15. 15 def parse(self,response):
  16. 16 pass

扩展:

反爬机制整理:

  • robots

  • UA伪装

  • 验证码

  • 代理

  • cookie

  • 动态变化的请求参数

  • JS加密

  • JS混淆

  • 图片懒加载

  • 动态数据的获取

  • selenium:规避检测

图片懒加载:

  • 网站优化手段

  • 应用到标签的伪属性,数据捕获的时候一定基于伪属性进行

    1. 是一种反爬机制,图片懒加载是一种网页优化技术。图片作为一种网络资源,在被请求时也与普通静态资源一样,将占用网络资源,而一次性将整个页面的所有图片加载完,将大大增加页面的首屏加载时间。为了解决这种问题,通过前后端配合,使图片仅在浏览器当前视窗内出现时才加载该图片,达到减少首屏图片请求数的技术就被称为“图片懒加载”。
    2. 在网页源码中,在img标签中首先会使用一个“伪属性”(通常使用src2original…)去存放真正的图片链接而并非是直接存放在src属性中。当图片出现到页面的可视化区域中,会动态将伪属性替换成src属性,完成图片的加载。
  • imagePileline:专门用于二进制数据下载和持久化存储的管道类(图片下载)

scrapy中技巧:

  1. 当调用item中的类时,没有显示且标红

  2. 解决方法:找到项目根文件--右击--找到Mark Directory as----Source-root后点击,生成源文件

    1. 1 #翻页设置
    2. 2 url = response.xpath('//span[@class="next"]/a/@href').extract_first()
    3. 3 if url !=None:
    4. 4 ''''
    5. 5 在提取数据后,parse()方法查找到下一页的链接,使用urljoin()方法构建完整的绝对URL(因为链接可以是相对的),
    6. 6 并产生一个新的请求到下一个页面,将自己作为回调函数来处理下一页的数据提取,并保持遍历所有页面的抓取。
    7. 7 '''
    8. 8 url = response.urljoin(url)
    9. 9 yield scrapy.Request(
    10. 10 url=url,callback=self.parse
    11. 11 )

selenium-绕过网站监测:

sourcehttps://www.cnblogs.com/presleyren/p/12936553.html

使用 Google 的Chrome Devtools-Protocol(Chrome 开发工具协议)简称CDP。

通过这个命令,我们可以给定一段 JavaScript 代码,让 Chrome 刚刚打开每一个页面,还没有运行网站自带的 JavaScript代码时,就先执行我们给定的这段代码。

那么如何在 Selenium中调用 CDP的命令呢?实际上非常简单,我们使用driver.execute_cdp_cmd。根据 Selenium 的官方文档[2],传入需要调用的 CDP 命令和参数即可;

只需要执行一次,之后只要你不关闭这个driver开启的窗口,无论你打开多少个网址,他都会自动提前在网站自带的所有 js 之前执行这个语句,隐藏window.navigator.webdriver

完美隐藏window.navigator.webdriver。并且,关键语句:

  1. 1 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
  2. 2 "source": """
  3. 3 Object.defineProperty(navigator, 'webdriver', {
  4. 4 get: () => undefined
  5. 5 })
  6. 6 """
  7. 7 })

虽然使用以上代码就可以达到目的了,不过为了实现更好的隐藏效果,大家也可以继续加入两个实验选项:

  1. 1 from selenium import webdriver
  2. 2 options = webdriver.ChromeOptions()
  3. 3 options.add_experimental_option("excludeSwitches", ["enable-automation"])
  4. 4 options.add_experimental_option('useAutomationExtension', False)
  5. 5 driver = webdriver.Chrome(options=options, executable_path='./chromedriver')
  6. 6 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
  7. 7 "source": """
  8. 8 Object.defineProperty(navigator, 'webdriver', {
  9. 9 get: () => undefined
  10. 10 })
  11. 11 """
  12. 12 })
  13. 13 driver.get('http://exercise.kingname.info')

这是close()的说明:

Closes the current window. ​ 关闭当前窗口。

这是quit()的说明:

Quits the driver and closes every associated window. ​ 退出驱动并关闭所有关联的窗口。

  • gb2312与gb2312 urlencode区别

  1. 1 import urllib
  2. 2 country = u'中国'
  3. 3 country.encode('gb2312')
  4. 4 #-------'\xd6\xd0\xb9\xfa'
  5. 5 urllib.quote(country.encode('gb2312'))
  6. 6 #--------'%D6%D0%B9%FA'

scrapy框架使用:分布式、增量式的更多相关文章

  1. 爬虫 crawlSpider 分布式 增量式 提高效率

    crawlSpider 作用:为了方便提取页面整个链接url,不必使用创参寻找url,通过拉链提取器,将start_urls的全部符合规则的URL地址全部取出 使用:创建文件scrapy startp ...

  2. python爬虫---CrawlSpider实现的全站数据的爬取,分布式,增量式,所有的反爬机制

    CrawlSpider实现的全站数据的爬取 新建一个工程 cd 工程 创建爬虫文件:scrapy genspider -t crawl spiderName www.xxx.com 连接提取器Link ...

  3. scrapy框架之分布式操作

    分布式概念 分布式爬虫: 1.概念:多台机器上可以执行同一个爬虫程序,实现网站数据的分布爬取. 2.原生的scrapy是不可以实现分布式爬虫? a)调度器无法共享 b)管道无法共享 3.scrapy- ...

  4. 6 scrapy框架之分布式操作

    分布式爬虫 一.redis简单回顾 1.启动redis: mac/linux:   redis-server redis.conf windows: redis-server.exe redis-wi ...

  5. 爬虫开发14.scrapy框架之分布式操作

    分布式爬虫 一.redis简单回顾 1.启动redis: mac/linux:   redis-server redis.conf windows: redis-server.exe redis-wi ...

  6. 基于scrapy框架的分布式爬虫

    分布式 概念:可以使用多台电脑组件一个分布式机群,让其执行同一组程序,对同一组网络资源进行联合爬取. 原生的scrapy是无法实现分布式 调度器无法被共享 管道无法被共享 基于 scrapy+redi ...

  7. 爬虫---scrapy分布式和增量式

    分布式 概念: 需要搭建一个分布式的机群, 然后在每一台电脑中执行同一组程序, 让其对某一网站的数据进行联合分布爬取. 原生的scrapy框架不能实现分布式的原因 调度器不能被共享, 管道也不能被共享 ...

  8. 爬虫07 /scrapy图片爬取、中间件、selenium在scrapy中的应用、CrawlSpider、分布式、增量式

    爬虫07 /scrapy图片爬取.中间件.selenium在scrapy中的应用.CrawlSpider.分布式.增量式 目录 爬虫07 /scrapy图片爬取.中间件.selenium在scrapy ...

  9. Scrapy 框架 增量式

    增量式: 用来检测网站中数据的更新情况 from scrapy.linkextractors import LinkExtractor from scrapy.spiders import Crawl ...

  10. 基于Scrapy框架的增量式爬虫

    概述 概念:监测 核心技术:去重 基于 redis 的一个去重 适合使用增量式的网站: 基于深度爬取的 对爬取过的页面url进行一个记录(记录表) 基于非深度爬取的 记录表:爬取过的数据对应的数据指纹 ...

随机推荐

  1. C语言基础之理论概述

    C语言介绍 C语言是一种高级程序设计语言,由贝尔实验室的Dennis Ritchie在1972年开发.C语言是结构化编程语言,支持变量.数据类型.运算符.表达式.流程控制语句和函数等基本程序设计元素. ...

  2. java项目实践-webapp-mytomcat-day16

    目录 1. http协议 2. 自定义的web框架 3. 具体实现 4. 启动 1. http协议 CS架构 建立连接"三次握手" 断开连接 "四次挥手" 三次 ...

  3. VUEX 使用学习四 : action

    转载请注明出处: action 用于处理异步任务:action,可以操作任意的异步操作,类似于mutations,但是是替代mutations来进行异步操作的.首先mutations中必须是同步方法, ...

  4. ORA-00947:Not enough values (没有足够的值)

    1.问题 2.解决方式 大概率是关系表实际列数大于你所填的元素个数,请检查是否有疏漏的列即可. 我这里是以为代理键直接忽略不写即可,没有标明具体插入列,但是还是得标明才行 --创建图书目录表TITLE ...

  5. crypto常用工具

    古典密码 维吉尼亚密码(Vigenere): https://github.com/atomcated/Vigenere(加密解密程序,包含自动猜测密钥功能) https://www.guballa. ...

  6. [转帖](1.3)sql server for linux 配置mssql-conf(即SSCM)

    https://blog.51cto.com/ultrasql/2152021 目录 [配置mssql-conf] 启用SQL Server代理 修改SQL Server排序规则 配置客户反馈 修改默 ...

  7. [转帖]SIMD+SSE+AVX

    http://home.ustc.edu.cn/~shaojiemike/posts/simd/   SIMD SIMD全称Single Instruction Multiple Data,单指令多数 ...

  8. 【转帖】JAVA GC日志分析

    https://zhuanlan.zhihu.com/p/613592552 ​ 目录 1. GC分类 针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Par ...

  9. [转帖]Linux:CPU频率调节模式以及降频方法简介

    概述 cpufreq的核心功能,是通过调整CPU的电压和频率,来兼顾系统的性能和功耗.在不需要高性能时,降低电压和频率,以降低功耗:在需要高性能时,提高电压和频率,以提高性能. cpufreq 是一个 ...

  10. bcc工具的简要学习

    摘要 继续补充假期落下的内容. 其实有很多知识需要学习, 自己掌握的还是偏少一些. bcc的全貌 # 注意 bcc 需要较高的内核. 3.10 系列的内核基本不可用. argdist drsnoop ...