zoukankan      html  css  js  c++  java
  • Scrapy选择器和持久化

    介绍

    Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 其可以应用在数据挖掘,信息处理或存储历史数据等一系列的程序中。理解scrapy可以参考django,django框架是用帮助我们快速开发web程序的,而scrapy框架就是用来帮助我们快速抓取网页信息的。

    安装

    #Windows平台
        1、pip3 install wheel #  pip默认只是去网络去找包,要想pip支持本地wheel文件安装,需要安装wheel包。安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
        3、pip3 install lxml
        4、pip3 install pyopenssl
        5、下载并安装pywin32:https://sourceforge.net/projects/pywin32/files/pywin32/
        6、下载twisted的wheel文件:http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
        7、执行pip3 install 下载目录Twisted-17.9.0-cp36-cp36m-win_amd64.whl
        8、pip3 install scrapy
      
    #Linux平台
        1、pip3 install scrapy
    

    整体架构

    在Scrapy的数据流是由执行引擎控制,具体流程如下:

    1. spiders产生request请求,将请求交给引擎
    2. 引擎把请求交给调度器,调度器具有url去重功能(因为request对象里有url),调度器会对需要执行的请求按照优先级排列放到一个地方
    3. 调取器把请求给引擎,引擎把调度好的请求发送给download,通过中间件发送(这个中间件至少有 两个方法,一个请求的,一个返回的)
    4. 一旦完成下载就返回一个response,通过下载器中间件,返回给引擎,引擎把response 对象传给下载器中间件,最后到达引擎
    5. 引擎从下载器中收到response对象,经过爬虫中间件传给了spiders(spiders里面做两件事,1、产生request请求,2、为request请求绑定一个回调函数),spiders只负责解析爬取的任务。不做存储(可以做,但是不建议做,一来是为了分工明确,二来是为了解决重复打开文件的尴尬)
    6. 解析完成之后返回一个解析之后的结果items对象及(跟进的)新的Request给引擎
    7. 引擎将(Spider返回的)爬取到的Item给Item Pipeline,存入数据库,持久化。如果yield的是一个request对象,就传给调度器再次去下载
    8. 重复上述过程

    Scrapy主要包括了以下组件:

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

    创建项目

    scrapy创建项目也是和django创建项目类似

    1. scrapy startproject 项目名称
       - 在当前目录中创建中创建一个项目文件(类似于Django)
     
    2. scrapy genspider [-t template] <name> <domain>
       - 创建爬虫应用,需要cd到项目目录
       如:
          scrapy gensipider -t basic oldboy oldboy.com
          scrapy gensipider -t xmlfeed autohome autohome.com.cn
       PS:
          查看所有命令:scrapy gensipider -l
          查看模板命令:scrapy gensipider -d 模板名称
     
    3. scrapy list
       - 展示爬虫应用列表
     
    4. scrapy crawl 爬虫应用名称  
       - 运行单独爬虫应用,加上--nolog参数就不会打印提示信息
    

    目录结构

     myproject/
       scrapy.cfg    # 项目部署的配置文件
        myproject/
           __init__.py
           items.py          # 用于结构化数据,类似于django中的model
           pipelines.py    # 数据处理,一般做数据持久化
           middlewares.py   # 中间件
           settings.py    # 爬虫程序使用的配置文件
           spiders/
               __init__.py
               jd.py       # 文件名一般和爬虫名一样,但是不一样也没关系,因为scrapy crawl jd,使用的是爬虫的name属性
    

    如果在windows中国之行爬虫出现编码问题,在爬虫程序的最上面加上如下代码

    import sys,os
    sys.stdout=io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030')
    

    起步

    # -*- coding: utf-8 -*-
    import scrapy
    from scrapy.http import Request
    
    class ChoutiSpider(scrapy.Spider):
        # 爬虫名称,不能更改
        name = "chouti"
        # 爬去过程可能遇到一些a标签,这个选项用来控制爬虫只爬取chouti.com这个网站的连接,不往外爬
        allowed_domains = ["chouti.com"]
    
        # 初始url
        start_urls = ['http://chouti.com/']
    
        def start_requests(self):
            # 爬虫刚开启会执行这个,这个函数要么是一个生成器,要么返回一个可迭代对象,因为源码是把生成器或者可迭代对象通过
            # iter() 方法转化为迭代器,然后next取值
            # for url in self.start_urls:
            #     # 默认callback调用的是parse
            #     yield Request(url)
            url_list = []
            for url in self.start_urls:
                url_list.append(Request(url))
            return url_list
    
        def parse(self, response):
            # response 是爬取得到的对象HtmlResponse,里面不仅封装了响应头,也封装了响应体response.body
            items = response.xpath('//*[@id="content-list"]/div[@class="item"]')
            # 每次
            f = open('chouti.txt', 'a+')
            for i in items:
                """
                extract 得到列表, extract_first 得到单个值,extract 或 extract_first 得到的里面的内容都是字符串
                而不是可以使用xpath方法的selector对象
                """
                link = i.xpath('.//a/@href').extract_first()
                f.write(link + '
    ')
            f.close()
    
            page_links = response.xpath('//*[@id="dig_lcpage"]//a/@href').extract()
            for i in page_links:
                # 发送下一个请求
                yield Request(url='https://dig.chouti.com' + i, callback=self.parse)
    

    选择器

    在scrapy中可以使用beautifulsoup,但是如果你在scrapy里面使用beautifulsoup就有点非主流了,因为scrapy可以使用xpath语法,关键是在浏览器有一个copy xpath的功能以及xpath组件(crtl + shift + x)可以快速帮我们寻找的我们想要寻找的内容

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    from scrapy.http import HtmlResponse, Response
    from scrapy.selector import Selector
    html = """<!DOCTYPE html>
    <html>
        <head lang="en">
            <meta charset="UTF-8">
            <title></title>
        </head>
        <body>
            <ul>
                <li class="item-"><a id='i1' href="link.html">first item</a></li>
                <li class="item-0"><a id='i2' href="llink.html">first item</a></li>
                <li class="item-1"><a href="llink2.html">second item<span>vv</span></a></li>
            </ul>
            <div><a href="llink2.html">second item</a></div>
        </body>
    </html>
    """
    response = HtmlResponse(url='http://xxx.com', body=html, encoding='utf8')
    # // 表示子子孙孙, / 表示儿子
    hxs = response.xpath('//a')
    print(hxs)
    hxs = response.xpath('//a[2]')
    print(hxs)
    hxs = response.xpath('//a[@id]')
    print(hxs)
    hxs = response.xpath('//a[@id="i1"]')
    print(hxs)
    # 并且的条件
    hxs = response.xpath('//a[@href="link.html"][@id="i1"]')
    print(hxs)
    # 包含,比较常用
    hxs = response.xpath('//a[contains(@href, "link")]')
    print(hxs)
    # 没有end-with
    hxs = response.xpath('//a[starts-with(@href, "link")]')
    print(hxs)
    # 正则表达式,其中re:test 是固定写法
    hxs = response.xpath('//a[re:test(@id, "id+")]')
    print(hxs)
    hxs = response.xpath('//a[re:test(@id, "id+")]/text()').extract()
    print(hxs)
    hxs = response.xpath('//a[re:test(@id, "id+")]/@href').extract()
    print(hxs)
    # 抽取li标签下的所有a的href属性组成一个列表,列表里放着字符串
    hxs = response.xpath('/html/body/ul/li/a/@href').extract()
    print(hxs)
    # 抽取li标签下的第一个a的href属性组成一个列表,列表里放着字符串
    hxs = response.xpath('//body/ul/li/a/@href').extract_first()
    print(hxs)
    
    ul_list = response.xpath('//body/ul/li')
    for item in ul_list:
        # 如果这里用item.xpath('/a/span'),那么寻找范围是response,而不是item
        v = item.xpath('./a/span')
        # 或
        # v = item.xpath('a/span')
        # 或
        # v = item.xpath('*/a/span')
        print(v)
    

    cookie的处理

    cookie的处理有两种方式,第一种是自己在程序中获取cookie并每次显示地带上cookie

    from scrapy.http.cookies import CookieJar
            cookie_jar = CookieJar()
            cookie_jar.extract_cookies(response, response.request)
            # 去对象中将cookie解析到字典
            for k, v in cookie_jar._cookies.items():
                for i, j in v.items():
                    for m, n in j.items():
                        self.cookie_dict[m] = n.value
    发送请求时携带cookies,需要注意的一点是body传入的值是‘’k1=1&k2=2‘’这样的格式
    yield Request(
                url='https://xxx',
                method='post',
                body='xxoo',
                cookies=self.cookie_dict,
                headers = {'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'},
                callback = self.check_login
            )
    
    from urllib.parse import urlencode
    
    dic = {
        'k1':1,
        'k2':2
    }
    print(urlencode(dic))
    

    第二种方式是在每个请求加上这样一句meta={'cookiejar': True},这样发请求的时候会自动解析cookie并携带cookie

    pipeline

    我们在parse爬取的内容,可以直接在里面打开文件写入文件并关闭文件。但是这样会存在两个问题:

    1. 程序的耦合性较高,因为爬虫代码和存储数据的代码放到一起,这样一旦程序大了就会很乱,我们希望的是爬虫代码就仅仅是爬虫代码,数据持久化代码就只写数据保存相关的逻辑
    2. 一旦在程序设计到翻页,想要再次发请求并且回调函数还是parse的时候,就会重复打开文件,这肯定会浪费资源
      那么我们能否做到在爬虫启动到结束只打开一次文件呢?答案是肯定的,借助item和pipeline就能完成。

    cnblogs.py

    from nj.items import NjItem
    
    class CnblogSpider(scrapy.Spider):
        # 爬虫名称,不能更改
        name = "cnblog"
        allowed_domains = ["cnblogs.com"]
    
        # 初始url
        start_urls = ['https://www.cnblogs.com/longyunfeigu/']
        def parse(self, response):
            links = response.xpath("//a[@class='postTitle2']/@href").extract()
            for link in links:
                yield NjItem(href=link)
    

    items.py 相当于django的model

    import scrapy
    
    class NjItem(scrapy.Item):
        # define the fields for your item here like:
        # name = scrapy.Field()
        href = scrapy.Field()
    

    pipelines.py

    # -*- coding: utf-8 -*-
    
    # Define your item pipelines here
    #
    # Don't forget to add your pipeline to the ITEM_PIPELINES setting
    # See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
    from scrapy.exceptions import DropItem
    
    """
    源码内容:
        1. 判断当前FilePipeline类中是否有from_crawler
            有:
                obj = FilePipeline.from_crawler(....)
            否:
                obj = FilePipeline()
        2. obj.open_spider()
        
        3. obj.process_item()/obj.process_item()/obj.process_item()/obj.process_item()/obj.process_item()
        
        4. obj.close_spider()
    """
    
    """
    这里的两个Pipeline不管parse函数是否yield,都会实例化相应的对象并调用open_spider方法,
    然后这两个对象就坐在这抽烟等着了,等待着parse返回item对象,然后一层层调用process_item处理,最终爬虫结束的时候接关闭close_spider
    
    raise 一个DropItem() 对象的结果
    
    File.from_crawler
    DB.from_crawler
    File.open_spider
    Db.open_spider
    File
    Db.close_spider
    File.close_spider
    """
    
    class FilePipeline(object):
        def __init__(self, path):
            # 把这个类的对象后续方法可能会用到的属性定义在__init__里面,这样看代码的人一眼就能看到这个对象具有的属性
            # 否则调用open方法self才有f属性,别人可能有疑惑,明明__init__没有f,现在怎么就有了呢,还得一个个去方法找哪一个给self绑定f属性,这也是编程规范
            self.f = None
            self.path = path
    
        @classmethod
        def from_crawler(cls, crawler):
            """
            初始化时候,用于创建pipeline对象,我们在这里取settings的配置,注意我们写的settings不是爬虫所有的配置
            类似django的settings也不是项目的完整配置
            :param crawler:
            :return:
            """
            print('File.from_crawler')
            path = crawler.settings.get('FILE_PATH')
            return cls(path)
    
        def open_spider(self, spider):
            """
            爬虫开始执行时,调用
            :param spider:
            :return:
            """
            # if spider.name == 'chouti':
            print('File.open_spider')
            self.f = open(self.path, 'a+')
    
        def process_item(self, item, spider):
            # f = open('xx.log','a+')
            # f.write(item['href']+'
    ')
            # f.close()
            print('File')
            self.f.write(item.get('href','') + '
    ')
            # 返回item或者返回None, 写一个pipeline都的process_item都会被调用,调用传入的参数就是在这返回的值
            # 如果返回None,那么下一个pipeline都的process_item的item参数就是None
            # 要想pipeline都的process_item不再执行,可以使用raise DropItem() 异常实例
            return item
            # raise DropItem()
    
        def close_spider(self, spider):
            """
            爬虫关闭时,被调用
            :param spider:
            :return:
            """
            print('File.close_spider')
            self.f.close()
    
    class DbPipeline(object):
        def __init__(self,path):
            self.f = None
            self.path = path
    
        @classmethod
        def from_crawler(cls, crawler):
            """
            初始化时候,用于创建pipeline对象
            :param crawler:
            :return:
            """
            print('DB.from_crawler')
            path = crawler.settings.get('DB_PATH')
            return cls(path)
    
        def open_spider(self,spider):
            """
            爬虫开始执行时,调用
            :param spider:
            :return:
            """
            print('Db.open_spider')
            self.f = open(self.path,'a+')
    
        def process_item(self, item, spider):
            # f = open('xx.log','a+')
            # f.write(item['href']+'
    ')
            # f.close()
            print('Db',item)
            # self.f.write(item['href']+'
    ')
            return item
    
        def close_spider(self,spider):
            """
            爬虫关闭时,被调用
            :param spider:
            :return:
            """
            print('Db.close_spider')
            self.f.close()
    

    settings.py

    ITEM_PIPELINES = {
       'nj.pipelines.FilePipeline': 300,
       'nj.pipelines.DbPipeline': 400,
    }
    
  • 相关阅读:
    Laravel 中使用 Redis 数据库
    PHP 安装 phpredis 扩展(二)
    Redis 安装(一)
    macOS 中使用 phpize 动态添加 PHP 扩展的错误解决方法
    macOS 中的 Rootless 机制
    Homebrew
    macOS 下配置 MAMP 开发环境(Mac + Apache + Mysql + PHP)
    常系数齐次线性递推
    任意模数FFT
    猫树总结
  • 原文地址:https://www.cnblogs.com/longyunfeigu/p/9485291.html
Copyright © 2011-2022 走看看