• scrapy框架——架构介绍、安装、项目创建、目录介绍、使用、持久化方案、集成selenium、去重规则源码分析、布隆过滤器使用、redis实现分布式爬虫



    前言

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


    一、架构介绍

    在这里插入图片描述

    引擎(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)


    一、安装

    针对于mac、linux基本不会出现问题

    pip3 install scrapy
    
    • 1

    针对windows有概率失败
    失败解决方案:

    1、pip3 install wheel #安装后,便支持通过wheel文件安装软件,wheel文件官网:https://www.lfd.uci.edu/~gohlke/pythonlibs
    2、pip3 install lxml
    3、pip3 install pyopenssl
    4、下载并安装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


    一、项目创建

    scrapy与django类似

    1 创建scrapy项目

    scrapy startproject 项目名

    2 创建爬虫

    scrapy genspider 爬虫名 爬取网址

    3启动爬虫,爬取数据

    方法一指令启动:
    scrapy crawl 爬虫名字 --nolog(该参数为不打印日志,可忽略)

    方法二使用脚本来启动:
    项目路径下新建main.py

    from scrapy.cmdline import execute
    execute(['scrapy','crawl','爬虫名','--nolog'])# --nolog不打印日志,可不带
    
    
    • 1
    • 2
    • 3

    二、目录介绍

    在这里插入图片描述

    firstscrapy         # 项目名
       firstscrapy     # 文件夹
            spiders     # 文件夹,一个个的爬虫
                cnblogs.py # 其中一个爬虫,重点写代码的地方(解析数据,发起请求)
            items.py    # 类比djagno的models,表模型         
            middlewares.py # 中间件:爬虫中间件和下载中间件都在里面  
            pipelines.py   # 管道,做持久化需要在这写代码           
            settings.py    # 配置文件                             
        scrapy.cfg    # 上线配置,开发阶段不用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    三、解析数据

    分析数据写在爬虫文件中的parse方法里(spiders下的爬虫文件,本文为cnblogs.py)

    response对象有css方法和xpath方法

    1. css中写css选择器
    2. xpath中写xpath选择

    俩种方法可以独立使用也可以混合使用,俩种方法找到的是标签对象,会转化为类对象,所以可以根据类对象再次进行标签查找

    示例:
    -xpath取文本内容
    ‘.//a[contains(@class,“link-title”)]/text()’
    -xpath取属性
    ‘.//a[contains(@class,“link-title”)]/@href’
    -css取文本
    ‘a.link-title::text’
    -css取属性
    ‘img.image-scale::attr(src)’

    取到的标签对象可以是多个或者一个再选择对应的方法即可(使用以下俩个方法获取的标签不再是类对象,无法再次对其下内容进行查找)
    .extract_first() 取一个,使得提取内容转换为unicodez字符串
    .extract() 取所有,使得提取内容转换为unicodez字符串,返回一个list

    示例代码:

    import scrapy
    
    class CnblogsSpider(scrapy.Spider):
        name = 'cnblogs' # 爬虫名字
        allowed_domains = ['cnblogs.com'] #爬取网址名
        start_urls = ['http://cnblogs.com/'] # 爬取的url
    
        def parse(self, response):
            articles = response.xpath('.//article[@class="post-item"]') # 找到article标签
            for article in articles: # 对每一个article标签中再次查找标签
                title = article.xpath('.//a[@class="post-item-title"]/text()').extract_first()
                url = article.xpath('.//a[@class="post-item-title"]/@href').extract_first()
                desc = article.xpath('.//p[@class="post-item-summary"]/text()').extract()
                author = article.xpath('.//footer[@class="post-item-foot"]/a/span/text()').extract_first()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    四、配置

    修改settings.py

    1.基础配置

    #1  是否遵循爬虫协议,默认为True
    ROBOTSTXT_OBEY = False
    
    #2 LOG_LEVEL 日志级别
    LOG_LEVEL='ERROR'  # 报错如果不打印日志,在控制台看不到错误
    
    # 3 USER_AGENT
    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'
    
    # 4 DEFAULT_REQUEST_HEADERS 默认请求头
    DEFAULT_REQUEST_HEADERS = {
       'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
       'Accept-Language': 'en',
    }
    
    # 5 SPIDER_MIDDLEWARES 爬虫中间件
    SPIDER_MIDDLEWARES = {
        'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
    }
    # 6 DOWNLOADER_MIDDLEWARES  下载中间件
    DOWNLOADER_MIDDLEWARES = {
        'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
    }
    
    # 7 ITEM_PIPELINES 持久化配置,配置数据持久化方式(如文件、数据库等)
    ITEM_PIPELINES = {
        'cnblogs.pipelines.CnblogsPipeline': 300,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    2.增加爬虫的爬取效率配置

    #1 增加并发:
    #默认scrapy开启的并发线程为32个,可以适当进行增加。在settings配置文件中修改
    CONCURRENT_REQUESTS = 100
    #值为100,并发设置成了为100。
    
    #2 降低日志级别:
    #在运行scrapy时,会有大量日志信息的输出,为了减少CPU的使用率。可以设置log输出信息为INFO或者ERROR即可。在配置文件中编写:
    LOG_LEVEL = 'INFO'
    
    # 3 禁止cookie:
    #如果不是真的需要cookie,则在scrapy爬取数据时可以禁止cookie从而减少CPU的使用率,提升爬取效率。在配置文件中编写:
    COOKIES_ENABLED = False
    
    # 4 禁止重试:
    #对失败的HTTP进行重新请求(重试)会减慢爬取速度,因此可以禁止重试。在配置文件中编写:
    RETRY_ENABLED = False
    
    # 5 减少下载超时:
    #如果对一个非常慢的链接进行爬取,减少下载超时可以能让卡住的链接快速被放弃,从而提升效率。在配置文件中进行编写:
    DOWNLOAD_TIMEOUT = 10 #超时时间为10s
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    五、持久化方案

    第一种:parse返回值(很少使用)

    解析函数中parse,要return [{},{},{}],返回一个列表套字典的数据

        def parse(self, response):
            articles = response.xpath('.//article[@class="post-item"]')
            article_list = []
            for article in articles:
                item = CnblogsItem()
                title = article.xpath('.//a[@class="post-item-title"]/text()').extract_first()
                url = article.xpath('.//a[@class="post-item-title"]/@href').extract_first()
                desc = article.xpath('.//p[@class="post-item-summary"]/text()').extract()
                author = article.xpath('.//footer[@class="post-item-foot"]/a/span/text()').extract_first()
                article_list.append({'title':title, 'url':url, 'desc':desc, 'desc':desc, 'author':author, })
            return article_list
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    使用指令 scrapy crawl cnblogs -o 文件名(json,csv等后缀)

    scrapy crawl cnblogs -o data.json
    
    • 1

    第二种:pipline模式(通用的)

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

    import scrapy
    
    class CnblogsItem(scrapy.Item):
        title = scrapy.Field()
        desc = scrapy.Field()
        pub_time = scrapy.Field()
        author = scrapy.Field()
        url = scrapy.Field()
        content = scrapy.Field()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    import scrapy
    from scrapy import Request
    from testscrapy.items import CnblogsItem
    
    class CnblogsSpider(scrapy.Spider):
        name = 'cnblogs'
        allowed_domains = ['cnblogs.com']
        start_urls = ['http://cnblogs.com/']
        
        def parse(self, response):
            articles = response.xpath('.//article[@class="post-item"]')
            for article in articles:
                item = CnblogsItem()
                title = article.xpath('.//a[@class="post-item-title"]/text()').extract_first()
                url = article.xpath('.//a[@class="post-item-title"]/@href').extract_first()
                desc = article.xpath('.//p[@class="post-item-summary"]/text()').extract()
                author = article.xpath('.//footer[@class="post-item-foot"]/a/span/text()').extract_first()
                res_desc = desc[0].replace('\n', '').replace(' ', '')
                if not res_desc:
                    res_desc = desc[1].replace('\n', '').replace(' ', '')
                item['title'] = title
                item['url'] = url
                item['desc'] = desc
                item['author'] = author
                item['desc'] = res_desc
                # 由于需要拿到文章正文,再次发送request,url为request的爬取地址,callback为解析数据的函数,meta为需要传递过去的参数
                yield item
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

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

    	ITEM_PIPELINES = {
       	'crawl_cnblogs.pipelines.CrawlCnblogsPipeline': 300,
    		}
    
    • 1
    • 2
    • 3

    4.写一个pipline:CrawlCnblogsPipeline
    -open_spider:数据初始化,打开文件,打开数据库链接
    -process_item:真正存储的地方
    -一定不要忘了return item,交给后续的pipline继续使用
    -close_spider:销毁资源,关闭文件,关闭数据库链接

    pipelines.py

    # 数据持久化文件形式
    class CnblogsFilePipeline:
        def open_spider(self, spider):
            # print(spider)  # 就是爬虫类的对象
            # print(type(spider))  # 就是爬虫类的对象
            print('open')
            # 打开文件,链接数据库
            self.f = open('cnblogs.txt', 'w', encoding='utf-8')
    
        def close_spider(self, spider):
            # print(spider)  # 就是爬虫类的对象
            # print(type(spider))  # 就是爬虫类的对象
            print('close')
            # 关闭文件,关闭数据库
            self.f.close()
    
        def process_item(self, item, spider):
            # 具体存数据,存文件
            # with open('cnblog.txt', 'a', encoding='utf-8') as f:
            #     f.write('标题:%s,作者:%s' % (item['title'], item['author']))
            # print(item)
            self.f.write('标题:%s,作者:%s\n' % (item['title'], item['author']))
    
            return item  # 一定不要忘了返回
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    import pymysql
    
    class CnblogsMysqlPipeline:
        def open_spider(self, spider):
            self.conn = pymysql.connect(user='root',
                                        password="密码",
                                        host='127.0.0.1',
                                        port=3306,
                                        database='cnblogs')
            self.cursor = self.conn.cursor()
    
        def close_spider(self, spider):
            # self.conn.commit()
            self.cursor.close()
            self.conn.close()
    
        def process_item(self, item, spider):
            sql = 'insert into article (title,`desc`,url,pub_time,author,content) values (%s,%s,%s,%s,%s,%s)'
            self.cursor.execute(sql, args=[item['title'], item['desc'], item['url'], item['pub_time'], item['author'],
                                           item['content']])
            self.conn.commit()
    
            return item  # 一定不要忘了返回
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    六、爬虫和下载中间件

    在settings.py中可看到以下配置

    SPIDER_MIDDLEWARES 爬虫中间件 (了解即可,用的少)

    SPIDER_MIDDLEWARES = {
        'cnblogs.middlewares.CnblogsSpiderMiddleware': 543,
    }
    
    • 1
    • 2
    • 3

    DOWNLOADER_MIDDLEWARES 下载中间件(用的多)

    DOWNLOADER_MIDDLEWARES = {
        'cnblogs.middlewares.CnblogsDownloaderMiddleware': 543,
    }
    
    • 1
    • 2
    • 3

    最重要的是下载中间件,里面的两个方法
    middlewares.py

    class CnblogsDownloaderMiddleware:
        # 请求来的时候
        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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    七、加代理,cookie,header

    1.加代理

    在下载中间件中重写process_request

    def process_request(self, request, spider):
        request.meta['proxy'] = 'http://221.6.215.202:9091' #设置proxy即可
        return None
    
    • 1
    • 2
    • 3

    2.cookie

    在中间件中重写process_request

    def process_request(self, request, spider):
        request.cookies['name']='bbc‘
        return None
    
    • 1
    • 2
    • 3

    3.header

    在中间件中重写process_request

    def process_request(self, request, spider):
    	request.headers['Auth']='asdfasdfasdfasdf'
    	request.headers['USER-AGENT']='ssss'
    
    • 1
    • 2
    • 3

    八、集成selenium

    scrapy框架的下载器,是无法触发js的,当我们需要的内容时通过js获取到的,可以在scrapy框架中对下载器进行selenium的集成,实质就是将一些需要触发js的请求交由selenium来处理,其他的请求照常走下载器。

    第一步:
    在爬虫类中为需要使用selenium的爬虫添加一个类属性以及一个类方法

    import scrapy
    from selenium import webdriver
    
    class CnblogsSpider(scrapy.Spider):
        name = 'cnblogs'
        allowed_domains = ['cnblogs.com']
        start_urls = ['http://cnblogs.com/']
        driver = webdriver.Edge() # 定义一个selenium对象
    
    def close(spider, reason):
          spider.driver.close() # 使用完毕后关闭selenium对象
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    第二步:
    创建一个下载中间件,重写process_request

    class CnblogsDownloaderMiddlewareProxy:
    	def process_request(self, request, spider): # 通过分辨url来判断是否使用selenium来获取html源码
    		url=request.url
            if 'sitehome' in url:
                spider.driver.get(url)
                response=HtmlResponse(url=url,body=spider.driver.page_source.encode('utf-8'))
                return response
            else:
                return None
    	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    九、去重规则源码分析

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

    scrapy使用了集合进行去重

    第一步:
    找到scrapy的配置文件
    scrapy.settings下的default_settings.py中
    找到DUPEFILTER_CLASS = ‘scrapy.dupefilters.RFPDupeFilter’

    第二步:
    找到RFPDupeFilter,该类负责处理指纹判断是否爬取过

    class RFPDupeFilter(BaseDupeFilter):
    	    def request_seen(self, request: Request) -> bool:
            fp = self.request_fingerprint(request) # 生成指纹
            if fp in self.fingerprints: # 对比指纹是否存在
                return True # 返回True就是已爬取
            self.fingerprints.add(fp)
            if self.file:
                self.file.write(fp + '\n')
            return False # 返回false就是未爬取过
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第三步:
    找到爬虫对象执行的入口
    在scrapy.spiders下的_ init _.py中

    class Spider(object_ref):
        def start_requests(self):
            if not self.start_urls and hasattr(self, 'start_url'):
                raise AttributeError(
                    "Crawling could not start: 'start_urls' not found "
                    "or empty (but found 'start_url' attribute instead, "
                    "did you miss an 's'?)")
            for url in self.start_urls:
                yield Request(url, dont_filter=True)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第四步:
    爬虫执行后在调度器中去重
    在scrapy.core下的scheduler中

    class Scheduler(BaseScheduler):
        def enqueue_request(self, request: Request) -> bool:
            if not request.dont_filter and self.df.request_seen(request): #此处调用了之前找到的去重类
                self.df.log(request, self.spider)
                return False
            dqok = self._dqpush(request)
            if dqok:
                self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)
            else:
                self._mqpush(request)
                self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)
            self.stats.inc_value('scheduler/enqueued', spider=self.spider)
            return True
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    以上就是scrapy框架的去重解析


    十、布隆过滤器使用

    中小型的去重使用集合、字典等去重比较适合,并发量大了之后再使用这些方式进行就会极大的影响效率和内存。
    这个时候可以使用布隆过滤器。

    布隆过滤器是以bit为单位的数组,数组初始状态每一位都是0,当需要去重的数据经过哈希函数处理后,将数组对应位置的0变为1,一但哈希函数计算的位置都是1就表明该数据已存在,反之只要有一位不为1就表示该数据此前没有存在过。布隆过滤器,占用空间很小,但只能进行过滤对比,无法取出值,所以应用时需要考虑是否需要取出已存在的值。

    在这里插入图片描述

    安装

    pip3 install pybloom_live
    
    • 1

    1.定长的布隆过滤器

    from pybloom_live import BloomFilter
    
    bf = BloomFilter(capacity=1000) # capacity为数组长度,持续使用后,定长布隆过滤器准确性会越来越低
    url='www.baidu.com'
    bf.add(url)
    print(url in bf)
    print("www.liuqingzheng.top" in bf)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.可自动扩容的布隆过滤器

    from pybloom_live import ScalableBloomFilter
    
    bloom = ScalableBloomFilter(initial_capacity=100, error_rate=0.001, mode=ScalableBloomFilter.LARGE_SET_GROWTH) # initial_capacity为数组初始长度 error_rate为误判概率 
    url = "www.cnblogs.com"
    url2 = "www.liuqingzheng.top"
    bloom.add(url)
    print(url in bloom)
    print(url2 in bloom)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.布隆过滤器方法

    add、madd

    布隆过滤器对象.add {key} {item}
    
    布隆过滤器对象.madd {key} {item} [item…]
    
    • 1
    • 2
    • 3

    往过滤器中添加元素。如果key不存在,过滤器会自动创建。

    exists、 mexists

    布隆过滤器对象.exists {key} {item}
    
    布隆过滤器对象.mexists {key} {item} [item…]
    
    • 1
    • 2
    • 3

    判断过滤器中是否存在该元素,不存在返回0,存在返回1。


    十一、redis实现分布式爬虫

    第一步:
    安装scrapy-redis

    pip3 install scrapy-redis
    
    • 1

    第二步:
    改造爬虫类

    from scrapy_redis.spiders import RedisSpider
        class CnblogSpider(RedisSpider):
            name = 'cnblog_redis'
            allowed_domains = ['cnblogs.com']
            # start_urls = ['http://cnblogs.com/'] 该属性去掉
            # 写一个key:redis列表的key,起始爬取的地址
            redis_key = 'myspider:start_urls'
    
    	# 正常编写解析函数
    	def parse(self, response):
    		...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    第三步:
    配置文件配置

    # 分布式爬虫配置
    # 去重规则使用redis
    REDIS_HOST = 'localhost'                            # 主机名
    REDIS_PORT = 6379                                   # 端口
    DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
    SCHEDULER = "scrapy_redis.scheduler.Scheduler"
    # 持久化:文件,mysql,redis
    ITEM_PIPELINES = {
       # 'cnblogs.pipelines.CnblogsFilePipeline': 300, # 写入文件 数字越小的先进行操作
       # 'cnblogs.pipelines.CnblogsMysqlPipeline': 100, # 写入mysql
       'scrapy_redis.pipelines.RedisPipeline': 400, # 写入redis
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    第五步:
    向redis放入一个起始url,之后分布式项目才会开始爬取

    可以在redis中添加、或者使用脚本添加
    lpush myspider:start_urls value http://www.cnblogs.com/
    
    • 1
    • 2
  • 相关阅读:
    mac pro M1(ARM)安装:Nginx安装并开启错误、访问日志
    Spring Cache组件
    【Docker】Dockerfile使用技巧
    浅析Java设计模式【2.1】——代理
    中国氢能汽车商业化之路,还要开多远?
    .Net Core使用Coravel实现任务调度
    Go变量作用域精讲及代码实战
    初识Java
    wordpress redirected you too many times.(多重重定向)
    vue-axios
  • 原文地址:https://blog.csdn.net/kdq18486588014/article/details/126164451