皮皮网

皮皮网

【三三棋源码】【地区网站系统源码】【APIT算法改进源码】bloom源码解析

时间:2024-12-29 11:29:40 分类:综合

1.如何用Python做爬虫
2.布隆过滤器(Bloom Filter)详解
3.自然语言处理大模型BLOOM模型结构源码解析(张量并行版)
4.深入源码解析LevelDB
5.详解布隆过滤器的码解原理和实现
6.如何用JAVA写一个知乎爬虫

bloom源码解析

如何用Python做爬虫

       1)首先你要明白爬虫怎样工作。

       æƒ³è±¡ä½ æ˜¯ä¸€åªèœ˜è››ï¼ŒçŽ°åœ¨ä½ è¢«æ”¾åˆ°äº†äº’联“网”上。那么,你需要把所有的网页都看一遍。怎么办呢?没问题呀,你就随便从某个地方开始,比如说人民日报的首页,这个叫initial pages,用$表示吧。

       åœ¨äººæ°‘日报的首页,你看到那个页面引向的各种链接。于是你很开心地从爬到了“国内新闻”那个页面。太好了,这样你就已经爬完了俩页面(首页和国内新闻)!暂且不用管爬下来的页面怎么处理的,你就想象你把这个页面完完整整抄成了个html放到了你身上。

       çªç„¶ä½ å‘现, 在国内新闻这个页面上,有一个链接链回“首页”。作为一只聪明的蜘蛛,你肯定知道你不用爬回去的吧,因为你已经看过了啊。所以,你需要用你的脑子,存下你已经看过的页面地址。这样,每次看到一个可能需要爬的新链接,你就先查查你脑子里是不是已经去过这个页面地址。如果去过,那就别去了。

       å¥½çš„,理论上如果所有的页面可以从initial page达到的话,那么可以证明你一定可以爬完所有的网页。

       é‚£ä¹ˆåœ¨python里怎么实现呢?

       å¾ˆç®€å•

       import Queue

       initial_page = "初始化页"

       url_queue = Queue.Queue()

       seen = set()

       seen.insert(initial_page)

       url_queue.put(initial_page)

       while(True): #一直进行直到海枯石烂

        if url_queue.size()>0:

        current_url = url_queue.get() #拿出队例中第一个的url

        store(current_url) #把这个url代表的网页存储好

        for next_url in extract_urls(current_url): #提取把这个url里链向的url

        if next_url not in seen:

        seen.put(next_url)

        url_queue.put(next_url)

        else:

        break

       å†™å¾—已经很伪代码了。

       æ‰€æœ‰çš„爬虫的backbone都在这里,下面分析一下为什么爬虫事实上是个非常复杂的东西——搜索引擎公司通常有一整个团队来维护和开发。

       2)效率

       å¦‚果你直接加工一下上面的代码直接运行的话,你需要一整年才能爬下整个豆瓣的内容。更别说Google这样的搜索引擎需要爬下全网的内容了。

       é—®é¢˜å‡ºåœ¨å“ªå‘¢ï¼Ÿéœ€è¦çˆ¬çš„网页实在太多太多了,而上面的代码太慢太慢了。设想全网有N个网站,那么分析一下判重的复杂度就是N*log(N),因为所有网页要遍历一次,而每次判重用set的话需要log(N)的复杂度。OK,OK,我知道python的set实现是hash——不过这样还是太慢了,至少内存使用效率不高。

       é€šå¸¸çš„判重做法是怎样呢?Bloom Filter. 简单讲它仍然是一种hash的方法,但是它的特点是,它可以使用固定的内存(不随url的数量而增长)以O(1)的效率判定url是否已经在set中。可惜天下没有白吃的午餐,它的唯一问题在于,如果这个url不在set中,BF可以%确定这个url没有看过。但是如果这个url在set中,它会告诉你:这个url应该已经出现过,不过我有2%的不确定性。注意这里的不确定性在你分配的内存足够大的时候,可以变得很小很少。一个简单的教程:Bloom Filters by Example

       æ³¨æ„åˆ°è¿™ä¸ªç‰¹ç‚¹ï¼Œurl如果被看过,那么可能以小概率重复看一看(没关系,多看看不会累死)。但是如果没被看过,一定会被看一下(这个很重要,不然我们就要漏掉一些网页了!)。 [IMPORTANT: 此段有问题,请暂时略过]

       å¥½ï¼ŒçŽ°åœ¨å·²ç»æŽ¥è¿‘处理判重最快的方法了。另外一个瓶颈——你只有一台机器。不管你的带宽有多大,只要你的机器下载网页的速度是瓶颈的话,那么你只有加快这个速度。用一台机子不够的话——用很多台吧!当然,我们假设每台机子都已经进了最大的效率——使用多线程(python的话,多进程吧)。

       3)集群化抓取

       çˆ¬å–豆瓣的时候,我总共用了多台机器昼夜不停地运行了一个月。想象如果只用一台机子你就得运行个月了...

       é‚£ä¹ˆï¼Œå‡è®¾ä½ çŽ°åœ¨æœ‰å°æœºå™¨å¯ä»¥ç”¨ï¼Œæ€Žä¹ˆç”¨python实现一个分布式的爬取算法呢?

       æˆ‘们把这台中的台运算能力较小的机器叫作slave,另外一台较大的机器叫作master,那么回顾上面代码中的url_queue,如果我们能把这个queue放到这台master机器上,所有的slave都可以通过网络跟master联通,每当一个slave完成下载一个网页,就向master请求一个新的网页来抓取。而每次slave新抓到一个网页,就把这个网页上所有的链接送到master的queue里去。同样,bloom filter也放到master上,但是现在master只发送确定没有被访问过的url给slave。Bloom Filter放到master的内存里,而被访问过的url放到运行在master上的Redis里,这样保证所有操作都是O(1)。(至少平摊是O(1),Redis的访问效率见:LINSERT – Redis)

       è€ƒè™‘如何用python实现:

       åœ¨å„台slave上装好scrapy,那么各台机子就变成了一台有抓取能力的slave,在master上装好Redis和rq用作分布式队列。

       ä»£ç äºŽæ˜¯å†™æˆ

       #slave.py

       current_url = request_from_master()

       to_send = []

       for next_url in extract_urls(current_url):

        to_send.append(next_url)

       store(current_url);

       send_to_master(to_send)

       #master.py

       distributed_queue = DistributedQueue()

       bf = BloomFilter()

       initial_pages = "www.renmingribao.com"

       while(True):

        if request == 'GET':

        if distributed_queue.size()>0:

        send(distributed_queue.get())

        else:

        break

        elif request == 'POST':

        bf.put(request.url)

       å¥½çš„,其实你能想到,有人已经给你写好了你需要的:darkrho/scrapy-redis · GitHub

       4)展望及后处理

       è™½ç„¶ä¸Šé¢ç”¨å¾ˆå¤šâ€œç®€å•â€ï¼Œä½†æ˜¯çœŸæ­£è¦å®žçŽ°ä¸€ä¸ªå•†ä¸šè§„模可用的爬虫并不是一件容易的事。上面的代码用来爬一个整体的网站几乎没有太大的问题。

       ä½†æ˜¯å¦‚果附加上你需要这些后续处理,比如

       æœ‰æ•ˆåœ°å­˜å‚¨ï¼ˆæ•°æ®åº“应该怎样安排)

       æœ‰æ•ˆåœ°åˆ¤é‡ï¼ˆè¿™é‡ŒæŒ‡ç½‘页判重,咱可不想把人民日报和抄袭它的大民日报都爬一遍)

       æœ‰æ•ˆåœ°ä¿¡æ¯æŠ½å–(比如怎么样抽取出网页上所有的地址抽取出来,“朝阳区奋进路中华道”),搜索引擎通常不需要存储所有的信息,比如图片我存来干嘛...

       åŠæ—¶æ›´æ–°ï¼ˆé¢„测这个网页多久会更新一次)

       å¦‚你所想,这里每一个点都可以供很多研究者十数年的研究。虽然如此,

       â€œè·¯æ¼«æ¼«å…¶ä¿®è¿œå…®,吾将上下而求索”。

       æ‰€ä»¥ï¼Œä¸è¦é—®æ€Žä¹ˆå…¥é—¨ï¼Œç›´æŽ¥ä¸Šè·¯å°±å¥½äº†ï¼šï¼‰

布隆过滤器(Bloom Filter)详解

       布隆过滤器(Bloom Filter),一种年由布隆提出的码解高效数据结构,用于判断元素是码解否在集合中。其优势在于空间效率和查询速度,码解但存在误判率和删除难题。码解布隆过滤器由长二进制数组和多个哈希函数构成,码解三三棋源码新元素映射位置置1。码解判断时,码解若所有映射位置均为1,码解则认为在集合;有0则判断不在。码解尽管可能产生误报,码解但通过位数组节省空间,码解比如MB内存可处理亿长度数组。码解常用MurmurHash哈希算法,码解如mmh3库,码解它的随机分布特性使其在Redis等系统中广泛使用。

       在Scrapy-Redis中,可以将布隆过滤器与redis的bitmap结合,设置位长度为2的次方,通过setbit和getbit操作实现。将自定义的bloomfilter.py文件添加到scrapy_redis源码目录,并在dupefilter.py中进行相应修改。需要注意的是,爬虫结束后可通过redis_conn.delete(key名称)释放空间。使用时,只需将scrapy_redis替换到项目中,遵循常规的Scrapy-Redis设置即可。

自然语言处理大模型BLOOM模型结构源码解析(张量并行版)

       BLOOM模型结构解析,采用Megatron-DeepSpeed框架进行训练,张量并行采用1D模式。基于BigScience开源代码仓库,本文将详细介绍张量并行版BLOOM的原理和结构。

       单机版BLOOM解析见文章。

       模型结构实现依赖mpu模块,推荐系列文章深入理解mpu工具。

       Megatron-DeepSpeed张量并行工具代码mpu详解,覆盖并行环境初始化、Collective通信封装、张量并行层实现、测试以及Embedding层、交叉熵实现与测试。

       Embedding层:Transformer Embedding层包含Word、Position、TokenType三类,分别将输入映射为稠密向量、注入位置信息、类别信息。通常,地区网站系统源码位置信息通过ALiBi注入,无需传统Position Embedding,TokenType Embedding为可选项。张量并行版BLOOM Embedding层代码在megatron/model/language_model.py,通过参数控制三类Embedding使用。

       激活函数:位于megatron/model/utils.py,BLOOM激活函数采用近似公式实现。

       掩码:张量并行版模型用于预训练,采用Causal Mask确保当前token仅见左侧token。掩码实现于megatron/model/fused_softmax.py,将缩放、mask、softmax融合。

       ALiBi:位置信息注入机制,通过调整query-key点积中静态偏差实现。8个注意力头使用等比序列m计算斜率,个头则有不同序列。实现于megatron/model/transformer.py。

       MLP层:全连接层结构,列并行第一层,行并行第二层,实现于megatron/model/transformer.py。

       多头注意力层:基于标准多头注意力添加ALiBi,简化版代码位于megatron/model/transformer.py。

       并行Transformer层:对应单机版BlookBlock,实现于megatron/model/transformer.py。

       并行Transformer及语言模型:ParallelTransformer类堆叠多个ParallelTransformerLayer,TransformerLanguageModel类在开始添加Embedding层,在末尾添加Pooler,逻辑简单,代码未详述。

       相关文章系列覆盖大模型研究、RETRO、MPT、ChatGLM-6B、BLOOM、LoRA、推理工具测试、LaMDA、Chinchilla、GLM-B等。

深入源码解析LevelDB

       深入源码解析LevelDB

       LevelDB总体架构中,sstable文件的生成过程遵循一系列精心设计的步骤。首先,遍历immutable memtable中的key-value对,这些对被写入data_block,每当data_block达到特定大小,APIT算法改进源码构造一个额外的key-value对并写入index_block。在这里,key为data_block的最大key,value为该data_block在sstable中的偏移量和大小。同时,构造filter_block,默认使用bloom filter,用于判断查找的key是否存在于data_block中,显著提升读取性能。meta_index_block随后生成,存储所有filter_block在sstable中的偏移和大小,此策略允许在将来支持生成多个filter_block,进一步提升读取性能。meta_index_block和index_block的偏移和大小保存在sstable的脚注footer中。

       sstable中的block结构遵循一致的模式,包括data_block、index_block和meta_index_block。为提高空间效率,数据按照key的字典顺序存储,采用前缀压缩方法处理。查找某一key时,必须从第一个key开始遍历才能恢复,因此每间隔一定数量(block_restart_interval)的key-value,全量存储一个key,并设置一个restart point。每个block被划分为多个相邻的key-value组成的集合,进行前缀压缩,并在数据区后存储起始位置的偏移。每一个restart都指向一个前缀压缩集合的起始点的偏移位置。最后一个位存储restart数组的大小,表示该block中包含多少个前缀压缩集合。

       filter_block在写入data_block时同步存储,当一个new data_block完成,根据data_block偏移生成一份bit位图存入filter_block,并清空key集合,重新开始存储下一份key集合。

       写入流程涉及日志记录,包括db的sequence number、本次记录中的操作个数及操作的key-value键值对。WriteBatch的batch_data包含多个键值对,leveldb支持延迟写和停止写策略,导致写队列可能堆积多个WriteBatch。为了优化性能,写入时会合并多个WriteBatch的batch_data。日志文件只记录写入memtable中的key-value,每次申请新memtable时也生成新日志文件。

       在写入日志时,anaconda查看python源码对日志文件进行划分为多个K的文件块,每次读写以这样的每K为单位。每次写入的日志记录可能占用1个或多个文件块,因此日志记录块分为Full、First、Middle、Last四种类型,读取时需要拼接。

       读取流程从sstable的层级结构开始,0层文件特别,可能存在key重合,因此需要遍历与查找key有重叠的所有文件,文件编号大的优先查找,因为存储最新数据。非0层文件,一层中的文件之间key不重合,利用版本信息中的元数据进行二分搜索快速定位,仅需查找一个sstable文件。

       LevelDB的sstable文件生成与合并管理版本,通过读取log文件恢复memtable,仅读取文件编号大于等于min_log的日志文件,然后从日志文件中读取key-value键值对。

       LevelDB的LruCache机制分为table cache和block cache,底层实现为个shard的LruCache。table cache缓存sstable的索引数据,类似于文件系统对inode的缓存;block cache缓存block数据,类似于Linux中的page cache。table cache默认大小为,实际缓存的是个sstable文件的索引信息。block cache默认缓存8M字节的block数据。LruCache底层实现包含两个双向链表和一个哈希表,用于管理缓存数据。

       深入了解LevelDB的源码解析,有助于优化数据库性能和理解其高效数据存储机制。

详解布隆过滤器的原理和实现

       为什么需要布隆过滤器

       想象一下遇到下面的场景你会如何处理:

       手机号是否重复注册

       用户是否参与过某秒杀活动

       伪造请求大量 id 查询不存在的记录,此时缓存未命中,如何避免缓存穿透

       针对以上问题常规做法是:查询数据库,数据库硬扛,如果压力并不大可以使用此方法,保持简单即可。

       改进做法:用 list/set/tree 维护一个元素集合,判断元素是否在集合内,时间复杂度或空间复杂度会比较高。如果是微服务的话可以用 redis 中的 list/set 数据结构, 数据规模非常大此方案的内存容量要求可能会非常高。

       这些场景有个共同点,可以将问题抽象为:如何高效判断一个元素不在集合中? 那么有没有一种更好方案能达到时间复杂度和空间复杂双优呢?

       有!布隆过滤器。

什么是路单记录源码布隆过滤器

       布隆过滤器(英语:Bloom Filter)是 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间都远远超过一般的算法。

       工作原理

       布隆过滤器的原理是,当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点(offset),把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就(大约)知道集合中有没有它了:如果这些点有任何一个 0,则被检元素一定不在;如果都是 1,则被检元素很可能在。这就是布隆过滤器的基本思想。

       简单来说就是准备一个长度为 m 的位数组并初始化所有元素为 0,用 k 个散列函数对元素进行 k 次散列运算跟 len(m)取余得到 k 个位置并将 m 中对应位置设置为 1。

布隆过滤器优缺点

       优点:

       空间占用极小,因为本身不存储数据而是用比特位表示数据是否存在,某种程度有保密的效果。

       插入与查询时间复杂度均为 O(k),常数级别,k 表示散列函数执行次数。

       散列函数之间可以相互独立,可以在硬件指令层加速计算。

       缺点:

       误差(假阳性率)。

       无法删除。

       误差(假阳性率)

       布隆过滤器可以 % 判断元素不在集合中,但是当元素在集合中时可能存在误判,因为当元素非常多时散列函数产生的 k 位点可能会重复。 维基百科有关于假阳性率的数学推导(见文末链接)这里我们直接给结论(实际上是我没看懂...),假设:

       位数组长度 m

       散列函数个数 k

       预期元素数量 n

       期望误差ε

       在创建布隆过滤器时我们为了找到合适的 m 和 k ,可以根据预期元素数量 n 与 ε 来推导出最合适的 m 与 k 。

       java 中 Guava, Redisson 实现布隆过滤器估算最优 m 和 k 采用的就是此算法:

//计算哈希次数@VisibleForTestingstaticintoptimalNumOfHashFunctions(longn,longm){ //(m/n)*log(2),butavoidtruncationduetodivision!returnMath.max(1,(int)Math.round((double)m/n*Math.log(2)));}//计算位数组长度@VisibleForTestingstaticlongoptimalNumOfBits(longn,doublep){ if(p==0){ p=Double.MIN_VALUE;}return(long)(-n*Math.log(p)/(Math.log(2)*Math.log(2)));}

       无法删除

       位数组中的某些 k 点是多个元素重复使用的,假如我们将其中一个元素的 k 点全部置为 0 则直接就会影响其他元素。 这导致我们在使用布隆过滤器时无法处理元素被删除的场景。

       可以通过定时重建的方式清除脏数据。假如是通过 redis 来实现的话重建时不要直接删除原有的 key,而是先生成好新的再通过 rename 命令即可,再删除旧数据即可。

go-zero 中的 bloom filter 源码分析

       core/bloom/bloom.go 一个布隆过滤器具备两个核心属性:

       位数组:

       散列函数

       go-zero实现的bloom filter中位数组采用的是Redis.bitmap,既然采用的是 redis 自然就支持分布式场景,散列函数采用的是MurmurHash3

       Redis.bitmap 为什么可以作为位数组呢?

       Redis 中的并没有单独的 bitmap 数据结构,底层使用的是动态字符串(SDS)实现,而 Redis 中的字符串实际都是以二进制存储的。 a 的ASCII码是 ,转换为二进制是:,如果我们要将其转换为b只需要进一位即可:。下面通过Redis.setbit实现这个操作:

       set foo a \ OK \ get foo \ "a" \ setbit foo 6 1 \ 0 \ setbit foo 7 0 \ 1 \ get foo \ "b"

       bitmap 底层使用的动态字符串可以实现动态扩容,当 offset 到高位时其他位置 bitmap 将会自动补 0,最大支持 2^-1 长度的位数组(占用内存 M),需要注意的是分配大内存会阻塞Redis进程。 根据上面的算法原理可以知道实现布隆过滤器主要做三件事情:

       k 次散列函数计算出 k 个位点。

       插入时将位数组中 k 个位点的值设置为 1。

       查询时根据 1 的计算结果判断 k 位点是否全部为 1,否则表示该元素一定不存在。

       下面来看看go-zero 是如何实现的:

       对象定义

//表示经过多少散列函数计算//固定次maps=type(//定义布隆过滤器结构体Filterstruct{ bitsuintbitSetbitSetProvider}//位数组操作接口定义bitSetProviderinterface{ check([]uint)(bool,error)set([]uint)error})

       位数组操作接口实现

       首先需要理解两段 lua 脚本:

//ARGV:偏移量offset数组//KYES[1]:setbit操作的key//全部设置为1setScript=`for_,offsetinipairs(ARGV)doredis.call("setbit",KEYS[1],offset,1)end`//ARGV:偏移量offset数组//KYES[1]:setbit操作的key//检查是否全部为1testScript=`for_,offsetinipairs(ARGV)doiftonumber(redis.call("getbit",KEYS[1],offset))==0thenreturnfalseendendreturntrue`

       为什么一定要用 lua 脚本呢? 因为需要保证整个操作是原子性执行的。

//redis位数组typeredisBitSetstruct{ store*redis.Clientkeystringbitsuint}//检查偏移量offset数组是否全部为1//是:元素可能存在//否:元素一定不存在func(r*redisBitSet)check(offsets[]uint)(bool,error){ args,err:=r.buildOffsetArgs(offsets)iferr!=nil{ returnfalse,err}//执行脚本resp,err:=r.store.Eval(testScript,[]string{ r.key},args)//这里需要注意一下,底层使用的go-redis//redis.Nil表示key不存在的情况需特殊判断iferr==redis.Nil{ returnfalse,nil}elseiferr!=nil{ returnfalse,err}exists,ok:=resp.(int)if!ok{ returnfalse,nil}returnexists==1,nil}//将k位点全部设置为1func(r*redisBitSet)set(offsets[]uint)error{ args,err:=r.buildOffsetArgs(offsets)iferr!=nil{ returnerr}_,err=r.store.Eval(setScript,[]string{ r.key},args)//底层使用的是go-redis,redis.Nil表示操作的key不存在//需要针对key不存在的情况特殊判断iferr==redis.Nil{ returnnil}elseiferr!=nil{ returnerr}returnnil}//构建偏移量offset字符串数组,因为go-redis执行lua脚本时参数定义为[]stringy//因此需要转换一下func(r*redisBitSet)buildOffsetArgs(offsets[]uint)([]string,error){ varargs[]stringfor_,offset:=rangeoffsets{ ifoffset>=r.bits{ returnnil,ErrTooLargeOffset}args=append(args,strconv.FormatUint(uint(offset),))}returnargs,nil}//删除func(r*redisBitSet)del()error{ _,err:=r.store.Del(r.key)returnerr}//自动过期func(r*redisBitSet)expire(secondsint)error{ returnr.store.Expire(r.key,seconds)}funcnewRedisBitSet(store*redis.Client,keystring,bitsuint)*redisBitSet{ return&redisBitSet{ store:store,key:key,bits:bits,}}

       到这里位数组操作就全部实现了,接下来看下如何通过 k 个散列函数计算出 k 个位点

       k 次散列计算出 k 个位点

//k次散列计算出k个offsetfunc(f*Filter)getLocations(data[]byte)[]uint{ //创建指定容量的切片locations:=make([]uint,maps)//maps表示k值,作者定义为了常量:fori:=uint(0);i<maps;i++{ //哈希计算,使用的是"MurmurHash3"算法,并每次追加一个固定的i字节进行计算hashValue:=hash.Hash(append(data,byte(i)))//取下标offsetlocations[i]=uint(hashValue%uint(f.bits))}returnlocations}

       插入与查询

       添加与查询实现就非常简单了,组合一下上面的函数就行。

//添加元素func(f*Filter)Add(data[]byte)error{ locations:=f.getLocations(data)returnf.bitSet.set(locations)}//检查是否存在func(f*Filter)Exists(data[]byte)(bool,error){ locations:=f.getLocations(data)isSet,err:=f.bitSet.check(locations)iferr!=nil{ returnfalse,err}if!isSet{ returnfalse,nil}returntrue,nil}改进建议

       整体实现非常简洁高效,那么有没有改进的空间呢?

       个人认为还是有的,上面提到过自动计算最优 m 与 k 的数学公式,如果创建参数改为:

       预期总数量expectedInsertions

       期望误差falseProbability

       就更好了,虽然作者注释里特别提到了误差说明,但是实际上作为很多开发者对位数组长度并不敏感,无法直观知道 bits 传多少预期误差会是多少。

//NewcreateaFilter,storeisthebackedredis,keyisthekeyforthebloomfilter,//bitsishowmanybitswillbeused,mapsishowmanyhashesforeachaddition.//bestpractices://elements-meanshowmanyactualelements//whenmaps=,formula:0.7*(bits/maps),bits=*elements,theerrorrateis0.<1e-4//fordetailederrorratetable,see/zeromicro/go-zero

       欢迎使用 go-zero 并 star 支持我们!

微信交流群

       关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

如何用JAVA写一个知乎爬虫

       ä¸‹é¢è¯´æ˜ŽçŸ¥ä¹Žçˆ¬è™«çš„源码和涉及主要技术点:

       ï¼ˆ1)程序package组织

       ï¼ˆ2)模拟登录(爬虫主要技术点1)

        要爬去需要登录的网站数据,模拟登录是必要可少的一步,而且往往是难点。知乎爬虫的模拟登录可以做一个很好的案例。要实现一个网站的模拟登录,需要两大步骤是:(1)对登录的请求过程进行分析,找到登录的关键请求和步骤,分析工具可以有IE自带(快捷键F)、Fiddler、HttpWatcher;(2)编写代码模拟登录的过程。

       ï¼ˆ3)网页下载(爬虫主要技术点2)

        模拟登录后,便可下载目标网页html了。知乎爬虫基于HttpClient写了一个网络连接线程池,并且封装了常用的get和post两种网页下载的方法。

       ï¼ˆ4)自动获取网页编码(爬虫主要技术点3)

        自动获取网页编码是确保下载网页html不出现乱码的前提。知乎爬虫中提供方法可以解决绝大部分乱码下载网页乱码问题。

       ï¼ˆ5)网页解析和提取(爬虫主要技术点4)

        使用Java写爬虫,常见的网页解析和提取方法有两种:利用开源Jar包Jsoup和正则。一般来说,Jsoup就可以解决问题,极少出现Jsoup不能解析和提取的情况。Jsoup强大功能,使得解析和提取异常简单。知乎爬虫采用的就是Jsoup。

       ï¼ˆ6)正则匹配与提取(爬虫主要技术点5)

        虽然知乎爬虫采用Jsoup来进行网页解析,但是仍然封装了正则匹配与提取数据的方法,因为正则还可以做其他的事情,如在知乎爬虫中使用正则来进行url地址的过滤和判断。

       ï¼ˆ7)数据去重(爬虫主要技术点6)

        对于爬虫,根据场景不同,可以有不同的去重方案。(1)少量数据,比如几万或者十几万条的情况,使用Map或Set便可;(2)中量数据,比如几百万或者上千万,使用BloomFilter(著名的布隆过滤器)可以解决;(3)大量数据,上亿或者几十亿,Redis可以解决。知乎爬虫给出了BloomFilter的实现,但是采用的Redis进行去重。

       ï¼ˆ8)设计模式等Java高级编程实践

        除了以上爬虫主要的技术点之外,知乎爬虫的实现还涉及多种设计模式,主要有链模式、单例模式、组合模式等,同时还使用了Java反射。除了学习爬虫技术,这对学习设计模式和Java反射机制也是一个不错的案例。

       4. 一些抓取结果展示

leveldb之数据存储结构

       leveldb中的数据存储结构设计巧妙,尽管在源码中编码和反编码较为复杂,但理解时可以将其当作黑盒子。本文主要讨论几个关键组件:Slice、Varint/、InternalKey、Comparator、SSTable、DataBlock、IndexBlock、FilterBlock、MetaIndexBlock以及Log和WriteBatch。

       Slice是一个轻量级的数据结构,类似Go语言的切片,用于方便传递和引用数据子串,尤其在处理C++标准库中的std::string时,Slice更轻便,不需复制子串。

       Varint/是变长编码,用于节省存储空间,如位整型,通过MSB和后续7位表示数据,最长可编码到5字节。这种编码方式使得数字存储更加紧凑。

       InternalKey是存储用户数据的关键,由user_key、sequence和type组成,sequence用于版本控制和数据合并,type区分值类型和删除标记。删除时,leveldb通过日志追加而非直接修改,确保数据一致性。

       Comparator接口用于自定义key的比较逻辑,而InternalKeyComparator结合user_comparator,通过用户键和序列进行排序,保证新数据在旧数据的前面。

       SSTable由DataBlock、MetaIndexBlock和IndexBlock组成,DataBlock采用前缀压缩和重启点设计,提高了空间效率。IndexBlock则用于记录DataBlock的映射,采用跳点策略来压缩key。

       FilterBlock在构建Block的同时生成BloomFilter,用于快速过滤查找。MetaIndexBlock存储元信息到MetaBlock的映射。

       Footer用于文件校验和解析,包含索引和元数据信息。MemTable使用skiplist结构,支持高效查找,通过墓碑标记删除,保持数据一致性。

       Log负责持久化数据,避免内存丢失。WriteBatch用于批量操作,保证原子性,并进行序列化,便于数据恢复。

Catlike Coding Custom SRP学习之旅——Post Processing

       来到了后处理环节,这是渲染管线中关键的一环。后处理技术能够显著提升画面效果,比如色调映射、Bloom、抗锯齿等,都能在后处理中实现。除了改善整体画面效果,后处理还能用于实现描边等美术效果。本文将主要介绍后处理堆栈和Bloom效果等内容。

       考虑到篇幅和工作量,本文将从第4章节后半部分开始,以及未来的章节,主要提炼原教程的内容,尽量减少篇幅和实际代码。在我的Github工程中,包含了对源代码的详细注释,需要深入了解代码细节的读者可以查看我的Github工程。对于文章中的错误,欢迎读者批评指正。

       以下是原教程链接和我的Github工程:

       CatlikeCoding-SRP-Tutorial

       我的Github工程

       1. 后处理堆栈(Post-FX Stack)

       FX,全称是Special Effects,即特殊效果,也称为VFX(Visual Special Effects),即视觉特效。参考维基百科,视觉效果(Visual effects,简称VFX)是在**制作中,在真人动作镜头之外创造或操纵图像的过程。游戏很多技术都会沿用影视技术上的一些技术,比如在色调映射时,可以采用ACES(**色调映射)等。关于Special Effects为什么叫FX,而不是SE,网上似乎只是因为FX谐音Effects,让人不知道从哪吐槽。

       通常来说,因为后处理会包含很多不同的效果,如色调映射、Bloom、抗锯齿等等,因此后处理在渲染管线中的结构往往是一个堆栈式的结构(URP中也是如此,使用了Post Process Volume)。因此,在本篇中,我们将搭建这样一个堆栈结构,并实现Bloom效果。

       1.1 配置资源(Settings Asset)

       首先,我们定义PostFXSettings资源,即Scriptable Object,将其作为渲染管线的一项可配置属性,这样便于我们配置不同的后处理堆栈,并可以方便地切换。

       1.2 栈对象(Stack Object)

       类似于Light和Shadows,我们同样使用一个类来存储包括Camera、ScriptableRenderContext、PostFXSettings,并在其中执行后处理堆栈。

       1.3 使用堆栈(Using the Stack)

       在进行后处理前,我们首先需要获取当前摄像机画面的标识RenderTargetIdentifier,RenderTargetIdentifier用于标识CommandBuffer的RenderTexture。在这里,我们使用一个简单的int来标识sourceRT。

       对于一个后处理效果而言,其实现过程说来很简单,传入一个矩形Mesh(其纹理即当前画面),使用一个Shader渲染该矩形Mesh,将其覆盖回Camera的RT上,我们通过Blit函数来实现该功能。

       1.4 强制清除(Forced Clearing)

       因为我们将摄像机渲染到了中间RT上,我们虽然会在每帧结束时释放该RT空间,但是基于Unity自身对RT的管理策略,其并不会真正地清除该RT,因此我们在下一帧时,该RT中会留存上一帧的渲染结果,导致了每一帧画面都是在前一帧的结果之上绘制的。

       1.5 Gizmos

       我们还需要在后处理前后绘制不同的Gizmos部分,这部分略~

       1.6 自定义绘制(Custom Drawing)

       使用Blit方法绘制后处理,实际上会绘制一个矩形,也就是2个三角面,即6个顶点。但我们完全可以只用一个三角面来绘制整个画面,因此我们使用自定义的绘制函数代替Blit。

       1.7 屏蔽部分FX(Don't Always Apply FX)

       目前,我们对于所有摄像机都执行了后处理。但是,我们希望只对Game视图和Scene视图摄像机进行后处理,并对不同Scene视图提供单独的开关控制。很简单,通过判断摄像机类型来屏蔽。

       1.8 复制(Copying)

       接下来,完善下Copy Pass。我们在片元着色器中,对原画面进行采样,并且由于其不存在Mip,我们可以指定mip等级0进行采样,避免一部分性能消耗。

       2. 辉光(Bloom)

       目前,我们已经实现了后处理堆栈的框架,接下来实现一个Bloom效果。Bloom效果应该非常常见,也是经常被用于美化画面,其主要作用就是让画面亮的区域更亮。

       2.1 Bloom金字塔(Bloom Pyramid)

       为了实现Bloom效果,我们需要提取画面中亮的像素,并让这些亮的像素影响周围暗的像素。因此,需要首先实现RT的降采样。通过降采样,我们可以很轻易地实现模糊功能。

       2.2 配置辉光(Configurable Bloom)

       通常来说,我们并不需要降采样到很小的尺寸,因此我们将最大降采样迭代次数和最小尺寸作为可配置选项。

       2.3 高斯滤波(Gaussian Filtering)

       目前,我们使用双线性滤波来实现降采样,这样的结果会有很多颗粒感,因此我们可以使用高斯滤波,并且使用更大的高斯核函数,通过9x9的高斯滤波加上双线性采样,实现x的模糊效果。

       2.4 叠加模糊(Additive Blurring)

       对于Bloom的增亮,我们直接将每次降采样后的Pyramid一步步叠加到原RT上,即直接让两张不同尺寸的以相同尺寸采样,叠加颜色,这一步也叫上采样。

       2.5 双三次上采样(Bicubic Upsampling)

       在上采样过程中,我们使用了双线性采样,这样可能依然会导致块状的模糊效果,因此我们可以增加双三次采样Bicubic Sampling的可选项,以此提供更高质量的上采样。

       2.6 半分辨率(Half Resolution)

       由于Bloom会渲染多张Pyramid,因此其消耗是比较大的,其实我们完全没必要从初始分辨率开始降采样,从一半的分辨率开始采样的效果也很好。

       2.7 阈值(Threshold)

       目前,我们对整个RT的每个像素都进行了增亮,这让这个画面看起来过曝了一般,但其实Bloom只需要对亮的区域增亮,本身暗的地方就不需要增亮了。

       2.8 强度(Intensity)

       最后,提供一个Intensity选项,控制Bloom的整体强度。

       结束语

       大功告成,我们在渲染管线中增加了后处理堆栈,以及实现了一个Bloom效果,其实在做完这篇之后,我觉得这个渲染管线才算基本上达成了大部分需要的功能,也算是一个里程碑吧。