原文链接

前言

分析这个项目的源码原因是需要有去重过滤,增量爬取两个功能,而scrapy-redis项目已经帮我们实现了,想看看他是怎么实现的。这里只贴出部分主要代码,查看时请打开源码对照,笔记有点长,建议看的童鞋按部分看。这是第一次分析源码,限于个人水平,如有错误恳请指正,谢谢!

地址:https://github.com/rmax/scrapy-redis/tree/master/src/scrapy_redis

tips: 源码涉及scrapy的方法,不知道的在文档里搜一下就知道它的作用了,redis也是。

正文部分

克隆源码

安装git:https://git-scm.com/download/win

  1. git clone https://github.com/rmax/scrapy-redis.git

查看源码的话还是visual studio code 和pycharm方便,只要安装了某个包,在源码里用Ctrl+鼠标左键点击方法名就可以跳转到包的源码里。在linux下,则麻烦点要安装vim跳转的插件exuberant-ctags,有兴趣可以自己百度。

  • 在Windows环境安装需要用到的包
  1. pip install --upgrade scrapy
  2. pip install --upgrade redis

一、init部分

这里仅copy部分关键代码用于理清原理,全部代码请查看GitHub源码。

  • 首先看下init文件__init__.py。从当前目录下的connection.py文件中import两个函数get_redisget_redis_from_settings
1. __init__.py文件
  1. from .connection import ( # NOQA
  2. get_redis,
  3. get_redis_from_settings,
  4. )
  • 接着先看看get_redis函数。此函数返回一个redis客户端实例。此函数定义了一个redis_cls类,其值为redis.StrictRedis,是从default.py里的设置的值,所有默认值都放在了这个文件。(这里就忽略了)

  • 还有一个url,默认为None。如果scrapy的settings.py启用了REDIS_URL这个参数,就会传递到这里,然后调用redis.StrictRedis的类方法from_url,这个方法返回一个连接到传入url的redis客户端对象。如果scrapy的settings.py没有启用REDIS_URL这个参数,则返回一个redis的默认客户端对象,即默认连接到redis://[:password]@localhost:6379/0,而不是给定的redis地址。

2. connection.py文件
  1. def get_redis(**kwargs):
  2. redis_cls = kwargs.pop('redis_cls', defaults.REDIS_CLS)
  3. url = kwargs.pop('url', None)
  4. if url:
  5. return redis_cls.from_url(url, **kwargs)
  6. else:
  7. return redis_cls(**kwargs)
  • 再看第二个函数get_redis_from_settings,它有个参数settings。这个函数首先设置了一个从当前目录下的defaults.py获取默认参数的副本,然后再用从scrapy项目的settings.py中获取的有关redis字典型的配置参数来更新替换默认参数;然后引用six库做了python版本兼容,最后返回个redis客户端实例。注:scrapy的getdict方法用于将settings里的配置转为字典

  • connection.py

  1. def get_redis_from_settings(settings):
  2. params = defaults.REDIS_PARAMS.copy()
  3. params.update(settings.getdict('REDIS_PARAMS'))
  4. # XXX: Deprecate REDIS_* settings.
  5. for source, dest in SETTINGS_PARAMS_MAP.items():
  6. val = settings.get(source)
  7. if val:
  8. params[dest] = val
  9. # Allow ``redis_cls`` to be a path to a class.
  10. if isinstance(params.get('redis_cls'), six.string_types):
  11. params['redis_cls'] = load_object(params['redis_cls'])
  12. return get_redis(**params)
小结

这样__init__之后,就能够实例化一个连接到自己设置的redis地址的redis客户端实例了。

二、scheduler部分

能够连接redis后就要将scrapy请求的url存到redis。这里作者实现了个调度器Scheduler类来替换scrapy默认的调度器scrapy.core.scheduler.Scheduler。在自己项目的配置文件settings.py中设置成SCHEDULER = "scrapy_redis.scheduler.Scheduler"来替换默认的调度器。

  1. SCHEDULER = "scrapy_redis.scheduler.Scheduler"
  • scheduler.py就只有一个Scheduler类。先看__init__函数,只需传入一个server参数,即自己的redis实例,其他均是选默认参数。
  1. def __init__(self, server, ...):
  2. self.server = server
  3. ...
  • 接着看Scheduler类的类方法from_settings,设置了个字典kargs用于从自己项目的settings.py中读取参数SCHEDULER_PERSISTSCHEDULER_FLUSH_ON_STARTSCHEDULER_IDLE_BEFORE_CLOSE等,其中SCHEDULER_PERSIST这个参数就是用于实现增量爬取功能的,如果为TRUE则已经存入redis队列里的url就会一直保存不会清空,在我们停止了爬虫,下次再继续运行时就可以直接跳过已经在redis队列里的url了;接着这里还有个可选字典optional用于替换刚才提到的初始化函数init里的默认参数;接着将optional设置了的值加入到kwargs里;接着作者为了支持本地文件能像包一样导入,就用importlib.import_module函数转化了下;最后实例化一个连接到自己的redis实例对象,检查对象连通性;return cls()中的cls返回的是一个redis实例作为参数的Scheduler类对象本身,它被下一个类方法from_crawler调用,这样调用类方法后返回cls就会调用这个类的__init__方法再次初始化。
  1. @classmethod
  2. def from_settings(cls, settings):
  3. kwargs = {
  4. 'persist': settings.getbool('SCHEDULER_PERSIST'),
  5. ...
  6. }
  7. optional = {
  8. 'queue_key': 'SCHEDULER_QUEUE_KEY',
  9. ...
  10. }
  11. for name, setting_name in optional.items():
  12. val = settings.get(setting_name)
  13. if val:
  14. kwargs[name] = val
  15. server = connection.from_settings(settings)
  16. server.ping()
  17. return cls(server=server, **kwargs)

scrapy是如何调用自定义scheduler的

  • from_crawler也是类方法,它需要传入一个crawler对象作为参数,即是自己项目中的crawler;接着调用该类本身的类方法from_settings,并将crawler.settings作为参数传入,像上面说的一样,就会得到一个包含自己项目配置的redis实例;获取自己项目crawlerstats状态,返回实例。
  1. @classmethod
  2. def from_crawler(cls, crawler):
  3. instance = cls.from_settings(crawler.settings)
  4. instance.stats = crawler.stats
  5. return instance
  • 接着看open,传入一个spider对象作为参数;用load_object模块加载scrapy-redis项目默认配置中的队列类,默认为scrapy_redis.queue.PriorityQueue,传入参数队列类必须的参数server, spider, key, serializer等(作者在queue.py定义了3中队列类,都是操作redis的,等下我们再看)。跟队列类一样,加载去重过滤类,调用这个类的类方法from_spider,即dupefilter.py里的类方法,而from_spider,传入一个自己项目的spider对象,获取spider对象对应的配置,获取一个连接到自己配置的redis地址的redis对象,然后将自己项目的spider name结合scrapy-redis的默认配置生成spider_name:dupefilter作为过滤去重的redis key,搜索官网可知debug为自己项目的默认值False,最后返回调用此类方法的对象本身,这样就得到了一个连接到自己配置的redis实例和自己项目配置及默认配置的spider对象这两者结合的对象;接着判断是否清空redis的去重队列,默认不清空;通过判断队列长度判断是否还有请求在爬取。(这个过程感觉挺难理解的,下一篇笔记会用视频记录pycharm debug类方法调用的过程)
  1. def open(self, spider):
  2. self.spider = spider
  3. ...
  4. self.df = load_object(self.dupefilter_cls).from_spider(spider)
  5. ...

dupefilter.py的类方法from_spider

  1. class RFPDupeFilter(BaseDupeFilter):
  2. @classmethod
  3. def from_spider(cls, spider):
  4. settings = spider.settings
  5. server = get_redis_from_settings(settings)
  6. dupefilter_key = settings.get("SCHEDULER_DUPEFILTER_KEY", defaults.SCHEDULER_DUPEFILTER_KEY)
  7. key = dupefilter_key % {'spider': spider.name}
  8. debug = settings.getbool('DUPEFILTER_DEBUG')
  9. return cls(server, key=key, debug=debug)
  • 接着看closeflush函数,通过persist来确定是否清空去重队列和请求队列,默认False,但是语句为if not False,即为True,所以默认会清空;close的参数reasonscrapy默认异常cancelled操作。
  1. def close(self, reason):
  2. if not self.persist:
  3. self.flush()
  4. def flush(self):
  5. self.df.clear()
  6. self.queue.clear()

queue.py的实例方法clear

  1. def clear(self):
  2. """Clear queue/stack"""
  3. self.server.delete(self.key)

dupfilter.py的实例方法clear

  1. def clear(self):
  2. """Clears fingerprints data."""
  3. self.server.delete(self.key)
  • 再看enqueue_request,顾名思义即为入列请求的函数;传入scrapyrequest对象,判断requestdont_filter参数,默认为False和上面得到的去重过滤对象self.dfrequest_seen方法,它又调用request_fingerprint方法,request_fingerprint方法调用request的默认方法request_fingerprint来获取请求的指纹,然后将指纹作为值存入redis的去重队列中,如果存入成功,则redis返回0,即该请求的指纹没有重复,返回added == 0if not request.dont_filter and self.df.request_seen(request)也就是if not False and 0 == 0时,调用去重过滤对象self.dflog函数,当这个函数的参数debugTrue时启用log库的debug模式记录日志,否则记录日志添加no more duplicates will be shown,记录后将参数self.logdupes设置为False,之后返回False;接着,用scrapystats.inc_value收集统计spider的状态(不太理解这个函数,有知道的童鞋可以告知下,谢谢),最后入列请求后返回True
  1. def enqueue_request(self, request):
  2. if not request.dont_filter and self.df.request_seen(request):
  3. self.df.log(request, self.spider)
  4. return False
  5. if self.stats:
  6. self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
  7. self.queue.push(request)
  8. return True

dupefilter.py的实例方法request_seen

  1. def request_seen(self, request):
  2. fp = self.request_fingerprint(request)
  3. added = self.server.sadd(self.key, fp)
  4. return added == 0

dupefilter.py的实例方法log

  1. def log(self, request, spider):
  2. if self.debug:
  3. msg = "Filtered duplicate request: %(request)s"
  4. self.logger.debug(msg, {'request': request}, extra={'spider': spider})
  5. elif self.logdupes:
  6. msg = ("Filtered duplicate request %(request)s"
  7. " - no more duplicates will be shown"
  8. " (see DUPEFILTER_DEBUG to show all duplicates)")
  9. self.logger.debug(msg, {'request': request}, extra={'spider': spider})
  10. self.logdupes = False
  • 最后看看next_request函数;block_pop_timeout为默认值0,调用redispop每隔0秒从队列取出一个请求,取出操作使用redispipeline,要先执行multi()操作,然后执行取请求操作zrange(0, 0)取出一个请求并用zremrangebyrank(0, 0)删除这个索引对应的请求,然后执行execute获取结果;最后返回解码json格式后的结果;如果取出了请求并且状态不为None时,用scrapystats.inc_value收集统计spider的状态,之后返回request请求。
  1. def next_request(self):
  2. block_pop_timeout = self.idle_before_close
  3. request = self.queue.pop(block_pop_timeout)
  4. if request and self.stats:
  5. self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
  6. return request

queue.py的实例方法pop

  1. def pop(self, timeout=0):
  2. pipe = self.server.pipeline()
  3. pipe.multi()
  4. pipe.zrange(self.key, 0, 0).zremrangebyrank(self.key, 0, 0)
  5. results, count = pipe.execute()
  6. if results:
  7. return self._decode_request(results[0])
小结

在自己项目的settings替换成scrapy-redisScheduler后,就会将项目的crawler对象传入到Scheduler类中,Scheduler类会生成一个自己定义的redis对象,一个去重过滤的df对象,一个存取请求的队列对象queue,再进行一些spider对象状态值的统计,请求的入列、清空等操作,经过这部分就实现了去重过滤功能了。dupefilter.py的方法基本在这里都用到了,所以就不单独再分析了。

三、queue部分

scheduler部分只用到了PriorityQueue有优先级的队列。作者其实在queue.py中实现了FifoQueuePriorityQueueLifoQueue3中操作redis的方案,下面逐一看一下。

1. Base类
  • 先看3个类都继承了的基类Base;初始化需要一个redis客户端实例server,一个spider实例,一个key,一个默认为Noneserializer;如果serializerNonepass,异常处理,如果没有loadsdumps则报错;初始化参数。

  • 接下来是私有方法_encode_request_decode_request,这里用到了scrapy内置的函数request_to_dictrequest_from_dict来实现;_encode_request将请求转为字典类型,用serializer.dumps转换成json格式类型的数据并返回;_decode_request也类似,过程相反,将json类型数据转为字典类型后返回。

  1. def _encode_request(self, request):
  2. """Encode a request object"""
  3. obj = request_to_dict(request, self.spider)
  4. return self.serializer.dumps(obj)
  5. def _decode_request(self, encoded_request):
  6. """Decode an request previously encoded"""
  7. obj = self.serializer.loads(encoded_request)
  8. return request_from_dict(obj, self.spider)
  • 再有就是__len__pushpop等,它们都是没有实现的,clear则是通用的实现,用于删除redis指定key
2. FifoQueue类
  • FifoQueue类实现了基类Base没有实现的__len__pushpop方法;__len__返回列表类型的redis队列长度;

  • push将编码成json格式的数据存入列表类型的redis队列;

  • pop判断timeout参数是否大于0,是则使用redisbrpop(key, timeout)方法,当这个key里面没有值时会等待n秒后才返回tuple类型的数据,返回第一个是key键,第二个是值;如果timeout不大于0,则用rpop方法删除并获取列表中的最后一个元素,当队列里面没有值时,2种方法都会返回None,即dataNone,最后如果data不为None返回解码后的请求数据。

  1. def pop(self, timeout=0):
  2. """Pop a request"""
  3. if timeout > 0:
  4. data = self.server.brpop(self.key, timeout)
  5. if isinstance(data, tuple):
  6. data = data[1]
  7. else:
  8. data = self.server.rpop(self.key)
  9. if data:
  10. return self._decode_request(data)
3. PriorityQueue类
  • FifoQueue类类似,PriorityQueue类也实现了基类Base没有实现的__len__pushpop方法,不过这里用的是redis的有序集合sorted set__len__方法用zcard获取有序集合长度;

  • push方法用_encode_request获取请求,设置了score值为scrapy的内置属性-request.priority默认值为0,最后用rediszadd方法将keyspider.namescoredatarequest请求等添加到有序集合中。

  1. def push(self, request):
  2. data = self._encode_request(request)
  3. score = -request.priority
  4. self.server.execute_command('ZADD', self.key, score, data)
  • popscheduler部分调用的时候已经分析过了。就是调用redispop每隔0秒从队列取出一个请求,取出操作使用redispipeline,要先执行multi()操作,然后执行取请求操作zrange(0, 0)取出一个请求并用zremrangebyrank(0, 0)删除这个索引对应的请求,然后执行execute获取结果,如果取出了结果就返回解码后的results[0]request对象。
  1. def pop(self, timeout=0):
  2. pipe = self.server.pipeline()
  3. pipe.multi()
  4. pipe.zrange(self.key, 0, 0).zremrangebyrank(self.key, 0, 0)
  5. results, count = pipe.execute()
  6. if results:
  7. return self._decode_request(results[0])
4. LifoQueue类
  • 同样LifoQueue类也实现了__len__pushpop方法;__len__push方法跟FifoQueue类的一致,不再赘述;pop方法,其实也差不多,不过FifoQueue类用的是redisbrpoprpop用于从最先进入队列删除key(最旧),LifoQueue类用的是blpoplpop用于从最后进入队列删除key(最新)。
  1. def pop(self, timeout=0):
  2. if timeout > 0:
  3. data = self.server.blpop(self.key, timeout)
  4. if isinstance(data, tuple):
  5. data = data[1]
  6. else:
  7. data = self.server.lpop(self.key)
  8. if data:
  9. return self._decode_request(data
小结

作者实现了3中队列类,先进先出队列、优先级队列、后进先出队列,项目用到的是优先级队列。

其实到这里已经能够满足我的去重过滤、增量爬取的需求了。但作者还提供了自己的spiders.py来执行爬取请求,和pipelines.py来将数据存储到redis的功能,有兴趣的接着看。

可选部分

四、spiders部分

先看spider部分,这里定义了3个类,RedisMixin类用于实现从redis队列读取urlsRedisSpider类继承RedisMixin和scrapy的Spider类;RedisCrawlSpider继承RedisMixin和scrapy的CrawlerSpider类;都用于空闲时从redis队列中读取爬取的请求urls

1. RedisSpider类和RedisCrawlSpider类
  • 先看看RedisSpider类和RedisCrawlSpider。它们都实现了scrapy的一个类方法from_crawler。这个类方法是干嘛的呢,不知道,所以去官网文档搜索下Spider from_crawler,发现method -- scrapy.spiders.Spider.from_crawler -- in Spiders这个内容比较符合我们想找的类方法。进去文档搜索from_crawler,的确找到一模一样的类方法from_crawler(crawler, *args, **kwargs),它是scrapy下的class scrapy.spiders.Spider类下的类方法,用来创建spider对象。

  • RedisSpider类和RedisCrawlSpider功能是一样的,就拿RedisSpider这个类来说吧,调用from_crawler类方法,里面的super会继承scrapy.spiders.CrawlSpider创建的spider对象,然后RedisSpider类也就具有了spider对象的所有属性和方法,同时又继承了RedisMixin类,那么类RedisSpider又具有了RedisMixin类的所有属性和方法,所以就可以调用RedisMixin类里的setup_redis方法了。(这个过程感觉挺难理解的,下一篇笔记会用视频记录pycharm debug类方法调用的过程)

  1. from scrapy.spiders import Spider, CrawlSpider
  2. class RedisMixin(object):
  3. def setup_redis(self, crawler=None):
  4. pass
  5. class RedisSpider(RedisMixin, Spider):
  6. @classmethod
  7. def from_crawler(self, crawler, *args, **kwargs):
  8. obj = super(RedisSpider, self).from_crawler(crawler, *args, **kwargs)
  9. obj.setup_redis(crawler)
  10. return obj
  11. class RedisCrawlSpider(RedisMixin, CrawlSpider):
  12. @classmethod
  13. def from_crawler(self, crawler, *args, **kwargs):
  14. obj = super(RedisCrawlSpider, self).from_crawler(crawler, *args, **kwargs)
  15. obj.setup_redis(crawler)
  16. return obj
2. RedisMixin类
  • 接着说RedisMixin类,经过调用类方法from_crawlerRedisMixin类已经具有了spider对象的所有属性和方法,那么就可以在RedisMixin类里面使用它们了。

  • 首先这个类定义了start_requests函数直接返回next_requests函数,next_requests函数返回一个要调度的request或返回none

  • next_requests函数具体实现:先设置了个标志位use_set,其名为REDIS_START_URLS_AS_SET,其值为default.py设置的默认值False;因为use_setFalse,所以fetch_one调用上面说过的init部分生成的redis实例的spop方法,否则调用lpop方法;初始化时found为0,进入循环,redis_batch_size的值为scrapy项目的settings.py设置的CONCURRENT_REQUESTS的值,默认并发值是16;调用fetch_oneredis获取一个redis_key即经过去重过滤的请求url,如果没有获取到请求就说明队列为空,跳出循环;有的话接着调用make_request_from_data方法将字节类型url编码成str类型再返回(这个函数返回make_requests_from_url,但我找不到哪里有定义,不知道是不是作者写错了,在github问也没人回答。。);如果有返回,则用yield同时处理多个请求url,然后将请求个数加一,并将日志输出。

  1. def next_requests(self):
  2. use_set = self.settings.getbool('REDIS_START_URLS_AS_SET', defaults.START_URLS_AS_SET)
  3. fetch_one = self.server.spop if use_set else self.server.lpop
  4. found = 0
  5. while found < self.redis_batch_size:
  6. data = fetch_one(self.redis_key)
  7. if not data:
  8. # Queue empty.
  9. break
  10. req = self.make_request_from_data(data)
  11. if req:
  12. yield req
  13. found += 1
  14. else:
  15. self.logger.debug("Request not made from data: %r", data)
  16. if found:
  17. self.logger.debug("Read %s requests from '%s'", found, self.redis_key)
  18. def make_request_from_data(self, data):
  19. url = bytes_to_str(data, self.redis_encoding)
  20. return self.make_requests_from_url(url)
  • 接着看setup_redis方法,按照注释,这是用于设置redis连接和空闲信号的,需要在spider对象设置了它的crawler对象之后才可以被调用,也就是要使用上面提到的RedisSpiderRedisCrawlSpider两个类之后,继承了spidercrawler之后才行;这个方法需要传入参数crawler,默认为None,如果传入值为None则会报错,提示crawler is required;如果没报错说明已经继承了spidercrawler对象,那接着就是从crawler对象获取配置信息等属性;接着判断redis队列中是否有请求url,如果没有则将本项目 的配置赋值给redis_key,然后格式化成'name': 自己项目爬虫名这样的格式;接着用字符串的strip()方法判断redis_key是否为空字符串,是则报错;然后判断redis_batch_size,如果为None则将默认值赋值给它,然后异常处理redis_batch_size是否为整形;再来就是判断redis_encoding,为None则将默认值赋值给它;判断参数都ok后,将参数信息作为日志输出;然后如init部分分析的,生成一个redis客户端对象;最后调用crawler.signals.connect方法,这个方法调用spider_idle方法,spider_idle方法又调用schedule_next_requests方法,schedule_next_requests方法调用next_requests方法从reids队列来获取新的请求url,然后用scrapycrawler.engine.crawl方法在爬虫空闲时来获取并执行爬取请求,执行完了返回到spider_idle执行raise DontCloseSpider来禁止关闭爬虫spider,正常来说执行完了请求就会关闭爬虫,到无法再获取到新的请求时,也就是redis请求队列没有请求了才会关闭spider。(其实这里有点蒙,作者用了DontCloseSpider来禁止关闭爬虫spider,最后是怎么关闭爬虫的?有知道的童鞋望告知,谢谢!)
  1. class RedisMixin(object):
  2. """Mixin class to implement reading urls from a redis queue."""
  3. redis_key = None
  4. redis_batch_size = None
  5. redis_encoding = None
  6. server = None
  7. def setup_redis(self, crawler=None):
  8. #...忽略部分内容
  9. self.server = connection.from_settings(crawler.settings)
  10. # The idle signal is called when the spider has no requests left,
  11. # that's when we will schedule new requests from redis queue
  12. crawler.signals.connect(self.spider_idle, signal=signals.spider_idle)
  13. def schedule_next_requests(self):
  14. # TODO: While there is capacity, schedule a batch of redis requests.
  15. for req in self.next_requests():
  16. self.crawler.engine.crawl(req, spider=self)
  17. def spider_idle(self):
  18. """Schedules a request if available, otherwise waits."""
  19. # XXX: Handle a sentinel to close the spider.
  20. self.schedule_next_requests()
  21. raise DontCloseSpider

五、pipelines部分

根据scrapy的原理可知,经过spider模块engine模块scheduler模块后到达pipelines模块,请求url爬取的内容将在这里被处理。

根据文档,自定义pipeline要实现from_crawlerprocess_itemopen_spiderclose_spider这几个方法(这里没有实现open_spiderclose_spider,有点不理解)。

  • 先看__init__,初始化一个server对象,也就是自己的redis客户端对象,一个用于存储爬取数据的itemkey,还有个用于编码成json格式的序列化函数,默认使用ScrapyJSONEncoder().encode

  • 接着是2个类方法,from_crawler类方法调用from_settings类方法,from_settings类方法首先从项目配置settings.py读取配置,如果读取到有REDIS_ITEMS_KEY这个关键字就将其作为参数添加到params字典中,REDIS_ITEMS_SERIALIZER也是一样,最后return cls(**params),此时类方法就会再次初始化,将params字典中的参数赋值给__init__中的参数。(这里的类方法跟上面scheduler,spiders提到的是类似的)

  • 然后是实现process_item方法,它调用deferToThread方法,这个方法的作用是在线程中运行函数,并将结果作为延迟返回。这个方法传入一个私有方法_process_item来处理item,首先调用item_key格式化,将spidername:items作为key,然后序列化items的内容作为值,最后用rpush将键值对存入redis并返回item,让它继续走完scrapy的流程。

小结

经过spiders.pypipelines.py后就可以将爬虫爬取的内容存储到自己的redis了。

结语

如果有童鞋看到了最后,手动给你点个赞!真有耐心,哈哈。本人水平有限,有问题欢迎留言交流,谢谢。

公众号往期文章

scrapy过滤重复数据和增量爬取

redis基础笔记

scrapy电影天堂实战(二)创建爬虫项目

scrapy电影天堂实战(一)创建数据库

scrapy基础笔记

在docker镜像中加入环境变量

笔记 | mongodb 入门操作

笔记 | python元类

笔记 | python2和python3使用super()

那些你在python3中可能没用到但应该用的东西

superset docker 部署

开机启动容器里面的程序

博客 | 三步部署hitchhiker-api

scrapy-redis源码浅析的更多相关文章

  1. Redis源码漂流记(二)-搭建Redis调试环境

    Redis源码漂流记(二)-搭建Redis调试环境 一.目标 搭建Redis调试环境 简要理解Redis命令运转流程 二.前提 1.有一些c知识简单基础(变量命名.常用数据类型.指针等) 可以参考这篇 ...

  2. 【深入浅出jQuery】源码浅析--整体架构

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  3. 【深入浅出jQuery】源码浅析2--奇技淫巧

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...

  4. Struts2源码浅析-ConfigurationProvider

    ConfigurationProvider接口 主要完成struts配置文件 加载 注册过程 ConfigurationProvider接口定义 public interface Configurat ...

  5. (转)【深入浅出jQuery】源码浅析2--奇技淫巧

    [深入浅出jQuery]源码浅析2--奇技淫巧 http://www.cnblogs.com/coco1s/p/5303041.html

  6. HashSet其实就那么一回事儿之源码浅析

    上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap,  本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...

  7. Android 手势识别类 ( 三 ) GestureDetector 源码浅析

    前言:上 篇介绍了提供手势绘制的视图平台GestureOverlayView,但是在视图平台上绘制出的手势,是需要存储以及在必要的利用时加载取出手势.所 以,用户绘制出的一个完整的手势是需要一定的代码 ...

  8. Android开发之Theme、Style探索及源码浅析

    1 背景 前段时间群里有伙伴问到了关于Android开发中Theme与Style的问题,当然,这类东西在网上随便一搜一大把模板,所以关于怎么用的问题我想这里也就不做太多的说明了,我们这里把重点放在理解 ...

  9. Redis源码研究--字典

    计划每天花1小时学习Redis 源码.在博客上做个记录. --------6月18日----------- redis的字典dict主要涉及几个数据结构, dictEntry:具体的k-v链表结点 d ...

  10. 一起学习redis源码

    redis的一些介绍,麻烦阅读前面的几篇文章,想对redis的详细实现有所了解,强力推荐<redis设计与实现>(不仅仅从作者那儿学习到redis的实现,还有项目的管理.思想等,作者可能比 ...

随机推荐

  1. 通过挂载系统U盘搭建本地yum仓库

    首先打开hbza(CentOS)和yum,两者要连接上 第1步:在hbza中创建一个目录 输入mkdir /lxk,名字随便起.输入mount  /dev/cdrom  /lxk 第2步:打开yum, ...

  2. python字典使用总结

    作者:python技术人 博客:https://www.cnblogs.com/lpdeboke 字典是另一种可变容器模型,且可存储任意类型对象. 字典的每个键值 key=>value 对用冒号 ...

  3. java_第一年_JDBC(6)

    DataBaseMetaData对象:由Connection.getDataBaseMetaData()方法获得,可以用来获取数据库的元数据,提供的方法有: getURL():返回一个String类, ...

  4. 浏览器输入url按回车背后经历了哪些?

    在PC浏览器的地址栏输入一串URL,然后按Enter键这个页面渲染出来,这个过程中都发生了什么事? 1.首先,在浏览器地址栏中输入url,先解析url,检测url地址是否合法2.浏览器先查看浏览器缓存 ...

  5. HDU-2068 RPG的错排(组合, 错排)

    RPG的错排 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Sub ...

  6. 问题 L: 超超的中等意思

    问题 L: 超超的中等意思 时间限制: 1 Sec  内存限制: 128 MB提交: 366  解决: 27[提交] [状态] [命题人:jsu_admin] 题目描述 已知p,q,k和一个难搞得多项 ...

  7. 最好用的 Kafka Json Logger Java客户端,赶紧尝试一下

    最好用的 Kafka Json Logger Java客户端. slf4j4json 最好用的 Kafka Json Logger 库:不尝试一下可惜了! Description 一款为 Kafka ...

  8. BUUCTF--SimpleRev

    测试文件:https://buuoj.cn/files/7458c5c0ce999ac491df13cf7a7ed9f1/SimpleRev?token=eyJ0ZWFtX2lkIjpudWxsLCJ ...

  9. JavaScript、ES6中类的this指向问题

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  10. 2018-11-26-win10-UWP-Controls-by-function

    title author date CreateTime categories win10 UWP Controls by function lindexi 2018-11-26 20:0:6 +08 ...