自己动手实现搜索引擎之数据采集
曾经在学生时代,就很想实现一个自己的搜索引擎,但当时没有这个技术,偶然的一个机会,有幸参加了王争老师的《数据结构与算法之美》的课程,在这个专栏里,我感觉通过几个月的练习,觉得自己的数据结构与算法,有了很大的进步,老师还特别分享了自己动手实现一个搜索引擎的原理以及关键的技术点,通过这个的学习,我了解了一个简单的搜索引擎的原理与实现,我也就将自己动手实现搜索引擎的技术实现分享出来。我将分为4个章节来介绍,分别是数据的采集,分析,索引,以及最后的搜索过程
首先是数的采集
简要流程
首先,数据要能搜索肯定要采集,如果把互联网比作一个海洋,那么数据就是海里的鱼,我们要怎么从海里去捞鱼呢?
此为一个简单的原理介绍:
搜索引擎把整个互联网看作是一个有向图,把每一个网页看作是一个顶点,如果一个网页存在另外一个网页的链接,那就添加一条有向边,这样我们就可以利用图的搜索算法,来搜索整个互联网。
具体到实现层页,我们可挑一些知名互联网网站,作为图的搜索起始顶点,使用图的广度搜索算法进行互联网的搜索,当爬取到一个网页,解析分析网页链接,将网页中的链接加入到图中,继续使用广度优先的遍历策略向外扩张搜索。
具体实现流程
现在来详细的说一说网页爬虫的处理流程
- 种子网页
种子网页这个可以这样理解:我们平时上网,经常会打开一些门户网站,例如,新浪,搜狐等等,这里也是一样的,我们就使用新浪作为种子网页开始进行搜索引擎的入口。
- 网页链接队列
此队列与我们平常使用的内存队列不一样,它是一个文件队列
我们所处理的网页链接是无限制的,经过多次爬虫爬取的网页链接可能已经超过了我们的内存大小了,所以需要采用文件队列来实现网页链接队列。
那网页链接如何存储在文件队列中呢?其实这个问题相对来说比较容易解决,我们可以按行进行网页链接的存储,即每次在网页链接队列中添加一个链接,就在数据的末尾添加一个换行符。针对读取来说使用一个偏移指针来记录下已经读取到的网页的位置,每次读取一个链接,就将偏移指针移动到读取到网页链接结束位置。
- 网页下载
这里网页下载可以直接使用已有的网络库直接下载,例如apache的网络库,
- 网页判重
网页判重使用布隆过滤器来实现,不过与标准的只在内存中存放数据的布隆过滤器不同,因为考虑到了服务需要重启的问题,需要将布隆过滤器中的数据持久化到磁盘中,以防数据丢失,我的实现方式是序列化,在需要保存时将数据通过JAVA提供的序列化机制持久到磁盘,当重新启动需要加载数据时,通过将磁盘中的数据反序列化到内存中,便可以直接使用。
受限于我机器的硬件条件,没有使用特别大的布隆过滤器,仅使用了10亿个位大小的char数组。也仅占用1百多M的内存
布隆过滤器如何实现网页的判重呢?
使用一组hash函数对同一个网页计算hash值,就可以得到一组hash值,然后检查布隆过滤器中,这些hash值所在的位是否被占用,如果所有位都被占用,那么说明网页已经存在,相反,只要有一个没有被占用,说明网页没有被存储,可以继续流程处理。
那布隆过滤器有没有什么问题呢?
当然是有的,它存在误判的问题,虽然这个概率非常小,但是当数据接近于布隆过滤器的最大值时,误判的概率就会增大,虽然存在误判,仅限于已经存在的数据,如果布隆过滤器判断数据不存在,那数据肯定是不存在于布隆过滤器中的。但搜索引擎对于数据的要求没有那么严格,也就是必须要做到100%的无重复。它容忍一定的数据存在重复。
- 分配网页ID
此操作相对来说是个非常简单的操作,即实现一个自增长序列。但在程序关闭时需要将最后分配的id保存,下次可以继续处理。我的实现是采用java的AtomicLong来实现,通过CAS原子操作,避免了锁的开销,在多线程环境下,也是线程安全的。
- 存储网页id与网页链接至doc_id.bin文件中
下图为网页id与网页链接的行存储格式
- 原始网页存储
下图为原始网页的存储格式
分网页原始文件需有自动分折文件的能力,当一个文件满足一定的大小阈值后,自动可以将数据写入到下一文件中。我的实现方式是在doc_raw_xxxx.bin添加4位固定的数字编写,从0000开始,这样生成的文件就为doc_raw_0000.bin、doc_raw_0001.bin文件。
- 分析网页中的链接
针对一个网页来说,网页内容中存在着很多的网页链接,这就是一个循环处理网页链接的过程.
使用图来表示如下
- 从指定位置查找<a标签开始,使用的算法是KMP算法,这是一个非常高效的单模式串字符查找函数,通过它,可以很快速的找到<a标签开始的位置.
- 获取网页中需要过滤标签的位置,因为在网页中存在一些需处理的代码段,例如<script>,<!---->,它们都是中间如果包含了<a标签是不应该被处理的,所以需要将位置给找出来.
- 检查当前<a标签位置是否在需要过滤的位置中,如果是,则跳过当前循环,进行下一次查找,回到1,否则继续
- 获取网页链接内容信息并查找<a标签的结束位置,即</a>的位置。设置下次遍历的开始的位置。即结束位置开始
- 针对网页中的链接内容进行过滤。例如:以javascipr:为前缀的js方法,或者以#开始的锚,以.jpg,.ico,.png等常用的图片过滤,再就是邮件地址过滤,等等这些页内容如果规则如果满跳,则跳过当前的链接地址,否则,注释继续
- 如果网页中存在特殊的编码,则需要处理,例如空格,需要替换为%20
- 链接的处理,主要是针对一些特殊的网页链接,需要添加http://或者https
- 去布隆过滤器中去检查下是否已经存在此网页链接,如果存在,则跳过当前,进行下一次。否则继续
- 将当前已经处理完成的链接放入到当前网页的地址集合中
经过这些步骤的处理,一个网页中所有的网页链接就被提取出来了。
- 将链接加入到网页链接队列。
经过这些处理,一个网页的处理就结束了,然后就是一直不断进行链接地址的向外扩长。
这就是我自己实现搜索引擎的数据采集的处理。通过这个采集过程,我将图的广度遍历、布隆过滤器、KMP算法,AC自动机等算法应用于其中,觉得对我的算法的提高非常的有帮助,我也希望我的分享对你的能有所帮助。
最后我将代码贴出来
github地址:
https://github.com/kkzfl22/searchEngine/tree/master/src/main/java/com/liujun/search/engine/collect