Searcharoo源码学习日志(二)
2009-10-08 02:19 Yin.P 阅读(1860) 评论(0) 编辑 收藏 举报Searcharoo第二版相对第一版做了很多改动,如将之前的基于当前站点的文件索引换成了基于Internet的网络爬行器、新增了HtmlDocument对象作为中间文档对象用于暂存文档信息、增加对已访问页面的判断处理、新增使用HttpWebRequest对象去获取Internet文档内容、新增去除页面内容中的脚本块样式块等功能。
在新一版片的Searcharoo中,对象模型没有发生变化,和前一版是完全是一样的,如下图。下面从爬行器开始分析。
图一:Searcharoo v2的对象模型(转自www.searcharoo.net)
l Spider(Crawler)
Searcharoo第二版爬行器(第一版叫Crawler,从第二版开始名称改为Spider)的核心方法是Process,在刚进入索引构造的时候,应用程序会先从配置信息中获取一个入口URI,以这个入口URI作为索引的起点。在Process方法中有三个主要的处理步骤,分别为Download方法、Parse方法以及填充索引数据。Download方法是从网络上下载页面内容,Parse是对页面内容进行处理,当下载的页面内容处理完毕后将其填充到索引数据结构中。下面先看看Process方法的整体结构:
if (visited.Contains(url.ToLower()))
{
…
}
else
{
…
visited.Add(url.ToLower());
…
if (Download(htmldoc))
{
if (htmldoc.RobotIndexOK)
{
Parse(htmldoc);
wordcount = Catalog(htmldoc);
}
}
…
if (null != htmldoc.LocalLinks && htmldoc.RobotFollowOK)
{
foreach (object link in htmldoc.LocalLinks)
{
try
{
Uri linkToFollow = new Uri(htmldoc.Uri, link.ToString());
if (linkToFollow.Host == startingUri.Host)
{
HtmlDocument hd = new HtmlDocument(linkToFollow);
Process(hd);
hd = null;
}
…
}
…
}
以上是Process方法的一个框架,从这个框架中可以看出Process方法的主要处理步骤:首先判断在已访问的URI列表中是不是已经存在当前待处理的URI,即当前URL是不是已经被访问过。如果已经访问则要跳过,否则继续处理这个URI。如果当前的URI没有被访问过那么要先将这个URI放到已访问列表中。然后下载这个URI对应的页面内容,内容下载成功后就调用Parse方法对下载下来的内容进行处理,将当前URI对应页面中的所有符合规则的URI提取出来并保存起来。最后将这个文档存到索引中。网页获取是一个递归的过程,因此接下来是对之前保存下来的URI进行递归处理。在第二版中,URI被分为两类,一类是外部URI,另一类是内部URI。外部URI的目标指向当前站点之外,反之内部URI的目标指向的是本站点。递归处理是基于内部URI这个范围的,其原因是为了防止无止境的递归(当然,控制递归层次和次数的还有其它一些方法)。
下面再来看看Download这个方法的代码:
…
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(htmldoc.Uri.AbsoluteUri);
…
webresponse = (HttpWebResponse)req.GetResponse();
…
if (webresponse != null)
{
htmldoc.ContentType = webresponse.ContentType;
switch (htmldoc.MimeType.ToLower())
{
case "text/css":break;
default:
if (htmldoc.MimeType.IndexOf("text") >= 0)
{
string enc = "utf-8"; // default
…
StreamReader stream = new StreamReader(
webresponse.GetResponseStream(),
Encoding.GetEncoding(htmldoc.Encoding));
htmldoc.Uri = webresponse.ResponseUri;
htmldoc.Length = webresponse.ContentLength;
htmldoc.All = stream.ReadToEnd();
…
首先初始化一个HttpWebRequest文档用于从Internet获取网页内容,当收到响应后,判断响应内容的MIME类型,如果是CSS文本则跳过不处理,如果MIME类型标识中带有text的,则将其编码方式设置为UTF8,然后将文档的基本信息设置到HtmlDocument对象中。如果这个阶段处理成功则返回成功标志。
Process方法的第二步是Parse方法,下面看看它的代码。首先是提取内容中的文档标题:
htmldoc.Title = Regex.Match(htmlData, @"(?<=<title>).*?(?=</title>)",RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture).Value;
然后是提取网页中的META元素信息:
foreach (Match metamatch in Regex.Matches(htmlData , @"<meta\s*(?:(?:\b(\w|-)+\b\s*(?:=\s*(?:""[^""]*""|'[^']*'|[^""'<> ]+)\s*)?)*)/?\s*>"
, RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture))
{
…
foreach (Match submetamatch in Regex.Matches(metamatch.Value.ToString(), @"(?<name>\b(\w|-)+\b)\s*=\s*(""(?<value>[^""]*)""|'(?<value>[^']*)'|(?<value>[^""'<> ]+)\s*)+", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture))
{
if ("http-equiv" == submetamatch.Groups[1].ToString().ToLower())
{
metaKey = submetamatch.Groups[2].ToString();
}
if (("name" == submetamatch.Groups[1].ToString().ToLower()) && (metaKey == String.Empty))
{
metaKey = submetamatch.Groups[2].ToString();
}
if ("content" == submetamatch.Groups[1].ToString().ToLower())
{
metaValue = submetamatch.Groups[2].ToString();
}
}
遍历每个META标签中的键值对,并对每个键进行如下的分析,通过这样的分析来提取当前文档中的描述信息、关键词和爬行器对这个页面文档的处理行为。
switch (metaKey.ToLower())
{
case "description":htmldoc.Description = metaValue;
break;
case "keywords":
case "keyword":htmldoc.Keywords = metaValue;
break;
case "robots":
case "robot":htmldoc.SetRobotDirective(metaValue);
break;
}
接下来就要提取这个文档中的所有链接信息了,如下面的代码:
ArrayList linkLocal = new ArrayList();
ArrayList linkExternal = new ArrayList();
foreach (Match match in Regex.Matches(htmlData, @"(?<anchor><\s*(a|area)\s*(?:(?:\b\w+\b\s*(?:=\s*(?:""[^""]*""|'[^']*'|[^""'<> ]+)\s*)?)*)?\s*>)", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture))
{
…
foreach (Match submatch in Regex.Matches(match.Value.ToString(), @"(?<name>\b\w+\b)\s*=\s*(""(?<value>[^""]*)""|'(?<value>[^']*)'|(?<value>[^""'<> \s]+)\s*)+", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture))
{
if ("href" == submatch.Groups[1].ToString().ToLower())
{
link = submatch.Groups[2].ToString();
break;
}
}
if (link.IndexOf("#") > -1)
{
link = link.Substring(0, link.IndexOf("#"));
}
if (link.IndexOf("javascript:") == -1 && link.IndexOf("mailto:") == -1 && !link.StartsWith("#") && link != String.Empty)
{
if ((link.Length > 8) && (link.StartsWith("http://") || link.StartsWith("https://") || link.StartsWith("file://") || link.StartsWith("//") || link.StartsWith(@"\\")))
{
linkExternal.Add(link);
}
else if (link.StartsWith("?"))
{
LinkLocal.Add(htmldoc.Uri.AbsolutePath + link);
}
else
{
linkLocal.Add(link);
}
}
首先初始化一个内部连接集合和一个外部连接集合。然后通过正则表达式解析文档中所有的<anchor>标签, <a>标签以及<area>标签,并提取它们的href属性值,再对这个属性值进行分析,将文档中的链接分别存放到外部链接集合和内部链接集合中。外部链接集合在这个版本中不会被使用,而内部链接则用于进行递归获取。最后将内部和外部链接信息设置到HtmlDocument对象,完成Parse过程的处理。
完成Parse方法后就回到了前面的Process方法,接下来进行的就是将得到的HtmlDocument对象进行索引,存放到索引数据结构中。最后就是递归处理,对所有的内部链接执行上面的过程。第二版中索引的处理和第一版基本相同,只是在切分内容为关键词集合的时候,这个版本是把文档删除HTML标签后的内容、文档描述以及META信息中的关键词合并到一起处理的。
从第一版本到第二版本的变化主要体现在索引过程中,而检索和前一版是差不多的,索引数据同样是缓存在内存中,在检索的时候先从缓存取索引,如果取不到就自动转到索引生成页生成索引后再回到搜索界面。 下一篇文章就要接着分析Searcharoo第三版的内容了,从第二版到第三版有相对较大的变化,不但对基本对象模型进行了扩展,而且还增加一些新的功能特性,使Searcharoo更趋近于一个标准的搜索引擎。