dySE:一个 Java 搜索引擎的实现,第 1 部分: 网络爬虫

本身动手写一个搜索引擎,想一想这有多 cool:在界面上输入关键词,点击搜索,获得本身想要的结果;那么它还能够作什么呢?也许是本身的网站须要一个站内搜索功能,抑或是对于硬盘中文档的搜索 —— 最重要的是,是否是以为众多 IT 公司都在向你招手呢?若是你心动了,那么,Let's Go!html

这里首先要说明使用 Java 语言而不是 C/C++ 等其它语言的缘由,由于 Java 中提供了对于网络编程众多的基础包和类,好比 URL 类、InetAddress 类、正则表达式,这为咱们的搜索引擎实现提供了良好的基础,使咱们能够专一于搜索引擎自己的实现,而不须要由于这些基础类的实现而分心。java

这个分三部分的系列将逐步说明如何设计和实现一个搜索引擎。在第一部分中,您将首先学习搜索引擎的工做原理,同时了解其体系结构,以后将讲解如何实现搜索引擎的第一部分,网络爬虫模块,即完成网页搜集功能。在系列的第二部分中,将介绍预处理模块,即如何处理收集来的网页,整理、分词以及索引的创建都在这部分之中。在系列的第三部分中,将介绍信息查询服务的实现,主要是查询界面的创建、查询结果的返回以及快照的实现。正则表达式

dySE 的总体结构数据库

在开始学习搜索引擎的模块实现以前,您须要了解 dySE 的总体结构以及数据传输的流程。事实上,搜索引擎的三个部分是相互独立的,三个部分分别工做,主要的关系体如今前一部分获得的数据结果为后一部分提供原始数据。三者的关系以下图所示:编程


图 1. 搜索引擎三段式工做流程
图 1. 搜索引擎三段式工做流程  

在介绍搜索引擎的总体结构以前,咱们借鉴《计算机网络——自顶向下的方法描述因特网特点》一书的叙事方法,从普通用户使用搜索引擎的角度来介绍搜索引擎的具体工做流程。设计模式

自顶向下的方法描述搜索引擎执行过程:浏览器

  • 用户经过浏览器提交查询的词或者短语 P,搜索引擎根据用户的查询返回匹配的网页信息列表 L;
  • 上述过程涉及到两个问题,如何匹配用户的查询以及网页信息列表从何而来,根据什么而排序?用户的查询 P 通过分词器被切割成小词组 <p1,p2 … pn> 并被剔除停用词 ( 的、了、啊等字 ),根据系统维护的一个倒排索引能够查询某个词 pi 在哪些网页中出现过,匹配那些 <p1,p2 … pn> 都出现的网页集便可做为初始结果,更进一步,返回的初始网页集经过计算与查询词的相关度从而获得网页排名,即 Page Rank,按照网页的排名顺序便可获得最终的网页列表;
  • 假设分词器和网页排名的计算公式都是既定的,那么倒排索引以及原始网页集从何而来?原始网页集在以前的数据流程的介绍中,能够得知是由爬虫 spider 爬取网页而且保存在本地的,而倒排索引,即词组到网页的映射表是创建在正排索引的基础上的,后者是分析了网页的内容并对其内容进行分词后,获得的网页到词组的映射表,将正排索引倒置便可获得倒排索引;
  • 网页的分析具体作什么呢?因为爬虫收集来的原始网页中包含不少信息,好比 html 表单以及一些垃圾信息好比广告,网页分析去除这些信息,并抽取其中的正文信息做为后续的基础数据。

在有了上述的分析以后,咱们能够获得搜索引擎的总体结构以下图:安全


图 2. 搜索引擎总体结构
图 2. 搜索引擎总体结构  

爬虫从 Internet 中爬取众多的网页做为原始网页库存储于本地,而后网页分析器抽取网页中的主题内容交给分词器进行分词,获得的结果用索引器创建正排和倒排索引,这样就获得了索引数据库,用户查询时,在经过分词器切割输入的查询词组并经过检索器在索引数据库中进行查询,获得的结果返回给用户。网络

不管搜索引擎的规模大小,其主要结构都是由这几部分构成的,并无大的差异,搜索引擎的好坏主要是决定于各部分的内部实现。多线程

有了上述的对与搜索引擎的总体了解,咱们来学习 dySE 中爬虫模块的具体设计和实现。

回页首

Spider 的设计

网页收集的过程如同图的遍历,其中网页就做为图中的节点,而网页中的超连接则做为图中的边,经过某网页的超连接 获得其余网页的地址,从而能够进一步的进行网页收集;图的遍历分为广度优先和深度优先两种方法,网页的收集过程也是如此。综上,Spider 收集网页的过程以下:从初始 URL 集合得到目标网页地址,经过网络链接接收网页数据,将得到的网页数据添加到网页库中而且分析该网页中的其余 URL 连接,放入未访问 URL 集合用于网页收集。下图表示了这个过程:


图 3. Spider 工做流程
图 3. Spider 工做流程  

回页首

Spider 的具体实现

网页收集器 Gather

网页收集器经过一个 URL 来获取该 URL 对应的网页数据,其实现主要是利用 Java 中的 URLConnection 类来打开 URL 对应页面的网络链接,而后经过 I/O 流读取其中的数据,BufferedReader 提供读取数据的缓冲区提升数据读取的效率以及其下定义的 readLine() 行读取函数。代码以下 ( 省略了异常处理部分 ):


清单 1. 网页数据抓取
URL url = new URL(“http://www.xxx.com”); 
URLConnection conn = url.openConnection(); 
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 
String line = null; 
while((line = reader.readLine()) != null) 
    document.append(line + "\n");

使用 Java 语言的好处是不须要本身处理底层的链接操做,喜欢或者精通 Java 网络编程的读者也能够不用上述的方法,本身实现 URL 类及相关操做,这也是一种很好的锻炼。

网页处理

收集到的单个网页,须要进行两种不一样的处理,一种是放入网页库,做为后续处理的原始数据;另外一种是被分析以后,抽取其中的 URL 链接,放入 URL 池等待对应网页的收集。

网页的保存须要按照必定的格式,以便之后数据的批量处理。这里介绍一种存储数据格式,该格式从北大天网的存储格式简化而来:

  • 网页库由若干记录组成,每一个记录包含一条网页数据信息,记录的存放为顺序添加;
  • 一条记录由数据头、数据、空行组成,顺序为:头部 + 空行 + 数据 + 空行;
  • 头部由若干属性组成,有:版本号,日期,IP 地址,数据长度,按照属性名和属性值的方式排列,中间加冒号,每一个属性占用一行;
  • 数据即为网页数据。

须要说明的是,添加数据收集日期的缘由,因为许多网站的内容都是动态变化的,好比一些大型门户网站的首页内容,这就意味着若是不是当天爬取的网页数据,极可能发生数据过时的问题,因此须要添加日期信息加以识别。

URL 的提取分为两步,第一步是 URL 识别,第二步再进行 URL 的整理,分两步走主要是由于有些网站的连接是采用相对路径,若是不整理会产生错误。URL 的识别主要是经过正则表达式来匹配,过程首先设定一个字符串做为匹配的字符串模式,而后在 Pattern 中编译后便可使用 Matcher 类来进行相应字符串的匹配。实现代码以下:


清单 2. URL 识别
public ArrayList<URL> urlDetector(String htmlDoc){
    final String patternString = "<[a|A]\\s+href=([^>]*\\s*>)";           
    Pattern pattern = Pattern.compile(patternString,Pattern.CASE_INSENSITIVE);   
    ArrayList<URL> allURLs = new ArrayList<URL>();
    Matcher matcher = pattern.matcher(htmlDoc);
    String tempURL;
    //初次匹配到的url是形如:<a href="http://bbs.life.xxx.com.cn/" target="_blank">
    //为此,须要进行下一步的处理,把真正的url抽取出来,
	//能够对于前两个"之间的部分进行记录获得url
    while(matcher.find()){
        try {
            tempURL = matcher.group();            
            tempURL = tempURL.substring(tempURL.indexOf("\"")+1);        
            if(!tempURL.contains("\""))
                continue;
            tempURL = tempURL.substring(0, tempURL.indexOf("\""));        
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }
    return allURLs;    
}

按照“<[a|A]\\s+href=([^>]*\\s*>)”这个正则表达式能够匹配出 URL 所在的整个标签,形如“<a href="http://bbs.life.xxx.com.cn/" target="_blank">”,因此在循环得到整个标签以后,须要进一步提取出真正的 URL,咱们能够经过截取标签中前两个引号中间的内容来得到这段内容。如此以后,咱们能够获得一个初步的属于该网页的 URL 集合。

接下来咱们进行第二步操做,URL 的整理,即对以前得到的整个页面中 URL 集合进行筛选和整合。整合主要是针对网页地址是相对连接的部分,因为咱们能够很容易的得到当前网页的 URL,因此,相对连接只须要在当前网页的 URL 上添加相对连接的字段便可组成完整的 URL,从而完成整合。另外一方面,在页面中包含的全面 URL 中,有一些网页好比广告网页是咱们不想爬取的,或者不重要的,这里咱们主要针对于页面中的广告进行一个简单处理。通常网站的广告链接都有相应的显示表达,好比链接中含有“ad”等表达时,能够将该连接的优先级下降,这样就能够必定程度的避免广告连接的爬取。

通过这两步操做时候,能够把该网页的收集到的 URL 放入 URL 池中,接下来咱们处理爬虫的 URL 的派分问题。

Dispatcher 分配器

分配器管理 URL,负责保存着 URL 池而且在 Gather 取得某一个网页以后派分新的 URL,还要避免网页的重复收集。分配器采用设计模式中的单例模式编码,负责提供给 Gather 新的 URL,由于涉及到以后的多线程改写,因此单例模式显得尤其重要。

重复收集是指物理上存在的一个网页,在没有更新的前提下,被 Gather 重复访问,形成资源的浪费,主要缘由是没有清楚的记录已经访问的 URL 而没法辨别。因此,Dispatcher 维护两个列表 ,“已访问表”,和“未访问表”。每一个 URL 对应的页面被抓取以后,该 URL 放入已访问表中,而从该页面提取出来的 URL 则放入未访问表中;当 Gather 向 Dispatcher 请求 URL 的时候,先验证该 URL 是否在已访问表中,而后再给 Gather 进行做业。

Spider 启动多个 Gather 线程

如今 Internet 中的网页数量数以亿计,而单独的一个 Gather 来进行网页收集显然效率不足,因此咱们须要利用多线程的方法来提升效率。Gather 的功能是收集网页,咱们能够经过 Spider 类来开启多个 Gather 线程,从而达到多线程的目的。代码以下:

/** 
* 启动线程 gather,而后开始收集网页资料
*/ 
public void start() { 
    Dispatcher disp = Dispatcher.getInstance(); 
    for(int i = 0; i < gatherNum; i++){ 
        Thread gather = new Thread(new Gather(disp)); 
        gather.start(); 
    }
}

在开启线程以后,网页收集器开始做业的运做,并在一个做业完成以后,向 Dispatcher 申请下一个做业,由于有了多线程的 Gather,为了不线程不安全,须要对 Dispatcher 进行互斥访问,在其函数之中添加 synchronized 关键词,从而达到线程的安全访问。

回页首

小结

Spider 是整个搜索引擎的基础,为后续的操做提供原始网页资料,因此了解 Spider 的编写以及网页库的组成结构为后续预处理模块打下基础。同时 Spider 稍加修改以后也能够单独用于某类具体信息的搜集,好比某个网站的图片爬取等。

回页首

后续内容

在本系列的第 2 部分中,您将了解到爬虫获取的网页库如何被预处理模块逐步提取内容信息,经过分词并建成倒排索引;而在第 3 部分中,您将了解到,如何编写网页来提供查询服务,而且如何显示的返回的结果和完成快照的功能。


参考资料

学习

相关文章
相关标签/搜索