网络引擎.docx
《网络引擎.docx》由会员分享,可在线阅读,更多相关《网络引擎.docx(27页珍藏版)》请在冰豆网上搜索。
网络引擎
一个Java搜索引擎的实现
第1部分:
网络爬虫自己动手写一个搜索引擎,想想这有多cool:
在界面上输入关键词,点击搜索,得到自己想要的结果;那么它还可以做什么呢?
也许是自己的网站需要一个站内搜索功能,抑或是对于硬盘中文档的搜索,这里说明使用Java语言而不是C/C++等其它语言的原因,因为Java中提供了对于网络编程众多的基础包和类,比如URL类、InetAddress类、正则表达式,这为我们的搜索引擎实现提供了良好的基础,使我们可以专注于搜索引擎本身的实现,而不需要因为这些基础类的实现而分心。
分三部分的系列将逐步说明如何设计和实现一个搜索引擎。
在第一部分中,您将首先学习搜索引擎的工作原理,同时了解其体系结构,之后将讲解如何实现搜索引擎的第一部分,网络爬虫模块,即完成网页搜集功能。
在系列的第二部分中,将介绍预处理模块,即如何处理收集来的网页,整理、分词以及索引的建立都在这部分之中。
在系列的第三部分中,将介绍信息查询服务的实现,主要是查询界面的建立、查询结果的返回以及快照的实现。
搜索引擎的三个部分是相互独立的,三个部分分别工作,主要的关系体现在前一部分得到的数据结果为后一部分提供原始数据。
三者的关系如下图所示:
图1.搜索引擎三段式工作流程
自顶向下的方法描述搜索引擎执行过程:
∙用户通过浏览器提交查询的词或者短语P,搜索引擎根据用户的查询返回匹配的网页信息列表L;
∙上述过程涉及到两个问题,如何匹配用户的查询以及网页信息列表从何而来,根据什么而排序?
用户的查询P经过分词器被切割成小词组并被剔除停用词(的、了、啊等字),根据系统维护的一个倒排索引可以查询某个词pi在哪些网页中出现过,匹配那些都出现的网页集即可作为初始结果,更进一步,返回的初始网页集通过计算与查询词的相关度从而得到网页排名,即PageRank,按照网页的排名顺序即可得到最终的网页列表;
∙假设分词器和网页排名的计算公式都是既定的,那么倒排索引以及原始网页集从何而来?
原始网页集在之前的数据流程的介绍中,可以得知是由爬虫spider爬取网页并且保存在本地的,而倒排索引,即词组到网页的映射表是建立在正排索引的基础上的,后者是分析了网页的内容并对其内容进行分词后,得到的网页到词组的映射表,将正排索引倒置即可得到倒排索引;
∙网页的分析具体做什么呢?
由于爬虫收集来的原始网页中包含很多信息,比如html表单以及一些垃圾信息比如广告,网页分析去除这些信息,并抽取其中的正文信息作为后续的基础数据。
在上述的分析之后,我们可以得到搜索引擎的整体结构如下图:
图2.搜索引擎整体结构
爬虫从Internet中爬取众多的网页作为原始网页库存储于本地,然后网页分析器抽取网页中的主题内容交给分词器进行分词,得到的结果用索引器建立正排和倒排索引,这样就得到了索引数据库,用户查询时,在通过分词器切割输入的查询词组并通过检索器在索引数据库中进行查询,得到的结果返回给用户。
无论搜索引擎的规模大小,其主要结构都是由这几部分构成的,并没有大的差别,搜索引擎的好坏主要是决定于各部分的内部实现。
Spider的设计
网页收集的过程如同图的遍历,其中网页就作为图中的节点,而网页中的超链接则作为图中的边,通过某网页的超链接得到其他网页的地址,从而可以进一步的进行网页收集;图的遍历分为广度优先和深度优先两种方法,网页的收集过程也是如此。
综上,Spider收集网页的过程如下:
从初始URL集合获得目标网页地址,通过网络连接接收网页数据,将获得的网页数据添加到网页库中并且分析该网页中的其他URL链接,放入未访问URL集合用于网页收集。
下图表示了这个过程:
图3.Spider工作流程
Spider的具体实现
[网页收集器Gather]
网页收集器通过一个URL来获取该URL对应的网页数据,其实现主要是利用Java中的URLConnection类来打开URL对应页面的网络连接,然后通过I/O流读取其中的数据,BufferedReader提供读取数据的缓冲区提高数据读取的效率以及其下定义的readLine()行读取函数。
代码如下(省略了异常处理部分):
清单1.网页数据抓取
URLurl=newURL(“”);
URLConnectionconn=url.openConnection();
BufferedReaderreader=newBufferedReader(newInputStreamReader(conn.getInputStream()));
Stringline=null;
while((line=reader.readLine())!
=null)
document.append(line+"\n");
使用Java语言的好处是不需要自己处理底层的连接操作,喜欢或者精通Java网络编程的读者也可以不用上述的方法,自己实现URL类及相关操作,这也是一种很好的锻炼。
[网页处理]
收集到的单个网页,需要进行两种不同的处理,一种是放入网页库,作为后续处理的原始数据;另一种是被分析之后,抽取其中的URL连接,放入URL池等待对应网页的收集。
网页的保存需要按照一定的格式,以便以后数据的批量处理。
这里介绍一种存储数据格式,该格式从北大天网的存储格式简化而来:
∙网页库由若干记录组成,每个记录包含一条网页数据信息,记录的存放为顺序添加;
∙一条记录由数据头、数据、空行组成,顺序为:
头部+空行+数据+空行;
∙头部由若干属性组成,有:
版本号,日期,IP地址,数据长度,按照属性名和属性值的方式排列,中间加冒号,每个属性占用一行;
∙数据即为网页数据。
需要说明的是,添加数据收集日期的原因,由于许多网站的内容都是动态变化的,比如一些大型门户网站的首页内容,这就意味着如果不是当天爬取的网页数据,很可能发生数据过期的问题,所以需要添加日期信息加以识别。
URL的提取分为两步,第一步是URL识别,第二步再进行URL的整理,分两步走主要是因为有些网站的链接是采用相对路径,如果不整理会产生错误。
URL的识别主要是通过正则表达式来匹配,过程首先设定一个字符串作为匹配的字符串模式,然后在Pattern中编译后即可使用Matcher类来进行相应字符串的匹配。
实现代码如下:
清单2.URL识别
publicArrayListurlDetector(StringhtmlDoc){
finalStringpatternString="<[a|A]\\s+href=([^>]*\\s*>)";
Patternpattern=Ppile(patternString,Pattern.CASE_INSENSITIVE);
ArrayListallURLs=newArrayList();
Matchermatcher=pattern.matcher(htmlDoc);
StringtempURL;
//初次匹配到的url是形如:
//为此,需要进行下一步的处理,把真正的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(MalformedURLExceptione){
e.printStackTrace();
}
}
returnallURLs;
}
按照“<[a|A]\\s+href=([^>]*\\s*>)”这个正则表达式可以匹配出URL所在的整个标签,形如“”,所以在循环获得整个标签之后,需要进一步提取出真正的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,然后开始收集网页资料
*/
publicvoidstart(){
Dispatcherdisp=Dispatcher.getInstance();
for(inti=0;iThreadgather=newThread(newGather(disp));
gather.start();
}
}
在开启线程之后,网页收集器开始作业的运作,并在一个作业完成之后,向Dispatcher申请下一个作业,因为有了多线程的Gather,为了避免线程不安全,需要对Dispatcher进行互斥访问,在其函数之中添加synchronized关键词,从而达到线程的安全访问。
第2部分:
网页预处理
预处理模块的整体结构
预处理模块的整体结构如下:
图1.预处理模块的整体结构
通过spider的收集,保存下来的网页信息具有较好的信息存储格式,但是还是有一个缺点,就是不能按照网页URL直接定位到所指向的网页。
所以,在第一个流程中,需要先建立网页的索引,如此通过索引,我们可以很方便的从原始网页库中获得某个URL对应的页面信息。
之后,我们处理网页数据,对于一个网页,首先需要提取其网页正文信息,其次对正文信息进行分词,之后再根据分词的情况建立索引和倒排索引,这样,网页的预处理也全部完成。
可能读者对于其中的某些专业术语会有一些不明白之处,在后续详述各个流程的时候会给出相应的图或者例子来帮助大家理解。
[建立索引网页库]
原始网页库是按照格式存储的,这对于网页的索引建立提供了方便,下图给出了一条网页信息记录:
清单1.原始网页库中的一条网页记录
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx//之前的记录
version:
1.0//记录头部
url:
date:
MonApr0514:
22:
53CST2010
IP:
218.241.236.72
length:
3981
DOCTYPE……//记录数据部分
……
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx//之后的记录
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
我们采用“网页库名—偏移”的信息对来定位库中的某条网页记录。
由于数据量比较大,这些索引网页信息需要一种保存的方法。
数据库们采用mysql。
我们用一个表来记录这些信息,表的内容如下:
url、content、offset、raws。
URL是某条记录对应的URL,因为索引数据库建立之后,我们是通过URL来确定需要的网页的;raws和offset分别表示网页库名和偏移值,这两个属性唯一确定了某条记录,content是网页内容的摘要,网页的数据量一般较大,把网页的全部内容放入数据库中显得不是很实际,所以我们将网页内容的MD5摘要放入到content属性中,该属性相当于一个校验码,在实际运用中,当我们根据URL获得某个网页信息是,可以将获得的网页做MD5摘要然后与content中的值做一个匹配,如果一样则网页获取成功,如果不一样,则说明网页获取出现问题。
这里简单介绍一下mySql的安装以及与Java的连接:
∙安装mySql,最好需要三个组件,mySql,mySql-front,mysql-connector-java-5.1.7-bin.jar,分别可以在网络中下载。
注意:
安装mySql与mySql-front的时候要版本对应,MySql5.0+MySql-Front3.2和MySql5.1+MySql-Front4.1,这个组合是不能乱的,可以根据相应的版本号来下载,否则会爆“‘10.000000’istkeingUltigerIntegerwert”的错误。
∙导入mysql-connector-java-5.1.7-bin.jar到eclipse的项目中,打开eclipse,右键点需要导入jar包的项目名,选属性(properties),再选java构建路径(javaBuildPath),后在右侧点(libraries),选addexternalJARs,之后选择你要导入的jar包确定。
∙接着就可以用代码来测试与mySql的连接了,代码见本文附带的testMySql.java程序,这里限于篇幅就不在赘述。
∙对于数据库的操作,我们最好进行一定的封装,以提供统一的数据库操作支持,而不需要在其他的类中显示的进行数据库连接操作,而且这样也就不需要建立大量的数据库连接从而造成资源的浪费,代码详见DBConnection.java。
主要提供的操作是:
建立连接、执行SQL语句、返回操作结果。
介绍了数据库的相关操作时候,现在我们可以来完成网页索引库的建立过程。
这里要说明的是,第一条记录的偏移是0,所以在当前记录record处理之前,该记录的偏移是已经计算出来的,处理record的意义在于获得下一个记录在网页库中的偏移。
假设当前record的偏移为offset,定位于头部的第一条属性之前,我们通过读取记录的头部和记录的数据部分来得到该记录的长度length,从而,offset+length即为下一条记录的偏移值。
读取头部和读取记录都是通过数据间的空行来标识的,其伪代码如下:
清单2.索引网页库建立
ForeachrecordinRawsdo
begin
读取record的头部和数据,从头部中抽取URL;
计算头部和数据的长度,加到当前偏移值上得到新的偏移;
从record中数据中计算其MD5摘要值;
将数据插入数据库中,包括:
URL、偏移、数据MD5摘要、Raws;
end;
MessageDigestAlgorithmMD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。
MD5的典型应用是对一段信息(Message)产生一个128位的二进制信息摘要(Message-Digest),即为32位16进制数字串,以防止被篡改。
对于我们来说,比如通过MD5计算,某个网页数据的摘要是00902914CFE6CD1A959C31C076F49EA8,如果我们任意的改变这个网页中的数据,通过计算之后,该摘要就会改变,我们可以将信息的MD5摘要视作为该信息的指纹信息。
所以,存储该摘要可以验证之后获取的网页信息是否与原始网页一致。
对MD5算法简要的叙述可以为:
MD5以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。
其中“一系列的处理”即为计算流程,MD5的计算流程比较多,但是不难,同时也不难实现,您可以直接使用网上现有的java版本实现或者使用本教程提供的源码下载中的MD5类。
对于MD5,我们知道其功能,能使用就可以,具体的每个步骤的意义不需要深入理解。
[正文信息抽取]
PageGetter
在正文信息抽取之前,我们首先需要一个简单的工具类,该工具类可以取出数据库中的内容并且去原始网页集中获得网页信息,对于该功能的实现在originalPageGetter.java中,该类通过URL从数据库中获得该URL对应的网页数据的所在网页库名以及偏移,然后就可以根据偏移来读取该网页的数据内容,同样以原始网页集中各记录间的空行作为数据内容的结束标记,读取内容之后,通过MD5计算当前读取的内容的摘要,校验是否与之前的摘要一致。
对于偏移的使用,BufferedReader类提供一个skip(intoffset)的函数,其作用是跳过文档中,从当前开始计算的offset个字符,用这个函数我们就可以定位到我们需要的记录。
清单3.获取原始网页库中内容
publicStringgetContent(StringfileName,intoffset)
{
Stringcontent="";
try{
FileReaderfileReader=newFileReader(fileName);
BufferedReaderbfReader=newBufferedReader(fileReader);
bfReader.skip(offset);
readRawHead(bfReader);
content=readRawContent(bfReader);
}catch(Exceptione){e.printStackTrace();}
returncontent;
}
上述代码中,省略了readRawHead和readRawContent的实现,这些都是基本的I/O操作,详见所附源码。
[正文抽取]
对于获得的单个网页数据,我们就可以进行下一步的处理,首先要做的就是正文内容的抽取,从而剔除网页中的标签内容,这一步的操作主要采用正则表达式来完成。
我们用正则表达式来匹配html的标签,并且把匹配到的标签删除,最后,剩下的内容就是网页正文。
限于篇幅,我们以过滤script标签为示例,其代码如下:
清单4.标签过滤
publicStringhtml2Text(StringinputString){
StringhtmlStr=inputString;//含html标签的字符串
Patternp_script;Matcherm_script;
try{
StringregEx_script="";
p_script=Ppile(regEx_script,Pattern.CASE_INSENSITIVE);
m_script=p_script.matcher(htmlStr);
htmlStr=m_script.replaceAll("");//过滤script标签
}catch(Exceptione){e.printStackTrace();}
returnhtmlStr;//返回文本字符串
}
通过一系列的标签过滤,我们可以得到网页的正文内容,就可以用于下一步的分词了。
[分词]
中文分词是指将一个汉字序列切分成一个一个单独的词,从而达到计算机可以自动识别的效果。
中文分词主要有三种方法:
第一种基于字符串匹配,第二种基于语义理解,第三种基于统计。
由于第二和第三种的实现需要大量的数据来支持,所以我们采用的是基于字符串匹配的方法。
基于字符串匹配的方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与一个“充分大的”机器词典中的词条进行配,若在词典中找到某个字符串,则匹配成功(识别出一个词)。
按照扫描方向的不同,串匹配分词方法可以分为正向匹配和逆向匹配;按照不同长度优先匹配的情况,可以分为最大(最长)匹配和最小(最短)匹配。
常用的几种机械分词方法如下:
1.正向减字最大匹配法(由左到右的方向);
2.逆向减字最大匹配法(由右到左的方向);
3.最少切分(使每一句中切出的词数最小);
4.双向最大减字匹配法(进行由左到右、由右到左两次扫描);
我们采用其中的正向最大匹配法。
算法描述如下:
输入值为一个中文语句S,以及最大匹配词n
1.取S中前n个字,根据词典对其进行匹配,若匹配成功,转3,否则转2;
2.n=n–1:
如果n为1,转3;否则转1;
3.将S中的前n个字作为分词结果的一部分,S除去前n个字,若S为空,转4;否则,转1;
4.算法结束。
需要说明的是,在第三步的起始,n如果不为1,则意味着有匹配到的词;而如果n为