65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# Spider 填充搜索引擎

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (37投票s)

2004年7月2日

20分钟阅读

viewsIcon

418426

downloadIcon

4552

如何使用 C# 构建 ASP.NET 搜索引擎链接蜘蛛。

Searcharoo Too

引言

第一篇文章描述了构建一个简单的搜索引擎,该引擎从指定文件夹抓取文件系统,并索引所有 HTML(或其他类型)文档。开发了基本设计和对象模型,以及一个查询/结果页面,您可以在此处查看

本系列的第二篇文章讨论了用“网络蜘蛛”替换“文件系统爬虫”,通过跟随 HTML 中的链接来搜索和编目网站。涉及的挑战包括:

  • 通过 HTTP 下载 HTML(和其他文档类型)
  • 解析 HTML 以查找其他页面的链接
  • 确保我们不会持续递归搜索相同的页面,导致无限循环
  • 解析 HTML 以提取单词,用于填充第一篇文章中的搜索目录。

设计

来自第一篇文章的设计保持不变...

目录包含单词集合,每个单词包含对其出现过的每个文件的引用.

...对象模型也相同...

Object Model

改变的是目录填充的方式。代码不再遍历文件系统中的文件夹以查找要打开的文件,而是需要一个起始页面的 URL,它将加载、索引该页面,然后尝试跟随该页面内的每个链接,并索引这些页面。为了防止代码索引整个互联网(在此版本中),它只尝试下载与起始页面位于同一服务器上的页面。

代码结构

来自第一篇文章的一些代码将再次被引用,但我们添加了一个新页面——SearcharooSpider.aspx——它负责 HTTP 访问和 HTML 链接解析 [使遍历文件系统中目录的代码——SearcharooCrawler.aspx——过时]。我们还将搜索页面名称更改为SearcharooToo.aspx,以便您可以与旧页面并排使用。

Searcharoo.cs

对象模型的实现;编译到两个 ASPX 页面中

从第1篇文章中重复使用

SearcharooCrawler.aspx

已过时,替换为 Spider

SearcharooToo.aspx
<%@ Page Language="C#" Src="Searcharoo.cs" %>
<%@ import Namespace="Searcharoo.Net"%>

从缓存中检索Catalog对象,并允许通过 HTML 表单进行搜索。

自第一篇文章以来已更新,以提高可用性,并重命名为 SearcharooToo.aspx

SearcharooSpider.aspx
<%@ Page Language="C#" Src="Searcharoo.cs" %>
<%@ import Namespace="Searcharoo.Net"%>

从起始页开始,下载并索引每个链接的页面。

本文新增页面

搜索蜘蛛有三个基本任务

  1. 查找要索引的页面
  2. 成功下载每个页面
  3. 解析页面内容并进行索引

Yahoo、Google、MSN 等大型搜索引擎都通过“爬行”互联网来构建其搜索目录。跟随链接查找文档需要我们编写一个 HTML 解析器,能够找到并解释链接,然后跟随它们!这包括能够跟随 HTTP-302 重定向、识别返回的文档类型、确定使用的字符集/编码(对于文本和 HTML 文档)等——基本上是一个迷你浏览器!我们将从小处着手,尝试使用 C# 构建一个合格的蜘蛛程序...

构建蜘蛛程序 [SearcharooSpider_alpha.aspx]

入门 - 下载页面

为了快速实现功能,我们先尝试下载“起始页”——比如本地机器的根页面(即步骤 2 - 下载页面)。这是从网站(本例中为localhost)获取 HTML 页面内容的最简单代码

using System.Net; 
/*...*/ 
string url = "https:///"; // just for testing 
WebClient browser = new WebClient(); 
UTF8Encoding enc = new UTF8Encoding(); 
string fileContents = enc.GetString(browser.DownloadData(url));

列表1 - 下载HTML文档最简单的方法

首先要注意的是包含了 System.Net 命名空间。它包含许多有用的类,包括 WebClient,这是一个非常简单的“类似浏览器”的对象,可以从给定的 URL 下载文本或数据。其次,我们假设页面使用 UTF-8 编码,使用 UTF8Encoding 类将下载的 Byte[] 数组转换为字符串。如果返回的页面编码不同(例如 Shift_JIS 或 GB2312),则此转换将产生乱码。我们稍后必须修复这个问题。第三点,可能不那么明显,是我实际上没有在 url 中指定一个页面。我们依赖服务器来解析请求并向我们返回默认文档——但是,服务器可能已经发布了 302 重定向到另一个页面(或另一个目录,甚至另一个站点)。WebClient 将成功遵循这些重定向,但其接口没有简单的方法让代码查询页面的实际 URL 是什么(在重定向之后)。我们也必须稍后修复这个问题,否则无法正确解析页面内的相对 URL。

尽管存在这些问题,我们现在已经将“起始页”的完整文本存储在一个变量中。这意味着,我们可以开始编写步骤 1 - 查找要索引的页面的代码。

解析页面

从 HTML 中解析链接(和其他数据)有两种选择(好的,可能更多,但主要有两种选择)

  1. 读取整个页面字符串,构建 DOM,并遍历其元素以查找链接,或者
  2. 使用正则表达式在页面字符串中查找链接模式。

尽管我怀疑“商业”搜索引擎可能会使用选项 1(构建 DOM),但使用正则表达式要简单得多。由于我最初的测试网站有非常规范的 HTML,所以我可以使用以下代码

// Create ArrayLists to hold the links we find... 
ArrayList linkLocal    = new ArrayList(); 
ArrayList linkExternal = new ArrayList(); 
// Dodgy Regex will find *some* links 
foreach (Match match in Regex.Matches(htmlData 
    , @"(?<=<(a|area)\s+href="").*?(?=""\s*/?>)" 
    , RegexOptions.IgnoreCase|RegexOptions.ExplicitCapture)) { 

    // Regex matches from opening "quote
    link = match.Value;
    // find first space (ie no spaces in Url)
    int spacePos = link.IndexOf(' ');
    // or first closing quote (NO single quotes) 
    int quotePos = link.IndexOf('"');
    int chopPos = (quotePos<spacePos?quotePos:spacePos);
    if (chopPos > 0) {
    // chopPos if quote or space first the at URL end
        link = link.Substring(0,chopPos);
    } 
    if ( (link.Length > 8) && 
         (link.Substring(0, 7).ToLower() == "http://") ) {
        // Assumes all links beginning with http:// are _external_ 
        linkExternal.Add(link) ; 
    } else { 
        // otherwise they're "relative"/internal links
        // so we concatenate the base URL 
        link = startingUrl + link; 
        linkLocal.Add(link); 
    } 
} // end looping through Matches of the 'link' pattern in the HTML data

列表2 - 在页面中查找链接的最简单方法

与页面下载的第一次尝试一样,此代码存在许多问题。首先,用于查找链接的正则表达式*非常*严格,即它将找到 -

<a href="News.htm">News</a>
<area href="News.htm" shape="rect" coords="0,0,110,20">

- 因为 href 作为 a(或 area)之后的第一个属性出现,并且 URL 本身用双引号括起来。但是,该代码将难以处理许多有效链接,包括

<a href='News.htm'>News</a>
<a href=News.htm>News</a>
<a class="cssLink" href="News.htm">News</a>
<area shape="rect" coords="0,0,110,20" href="News.htm">
<area href='News.htm' shape="rect" coords="0,0,110,20">

它还将尝试使用“内部页面链接”(以#开头),并且它假定任何以http://开头的链接都是外部链接,而没有首先将服务器名称与目标服务器进行核对。尽管存在这些错误,通过针对定制的 HTML 页面进行测试,此代码将成功地将链接解析到linkLocal ArrayList中,准备进行处理——将该 URL 列表与下载 URL 的代码结合起来,我们就可以有效地“爬取”一个网站!

下载更多页面

基本代码如下所示——注释显示了需要额外代码的地方,这些代码来自上面的列表或第一篇文章

protected void Page_Load (object sender, System.EventArgs e) { 
    /* The initial function call */ 
    startingPageUrl = "https:///"; // Get from web.config 
    parseUrl (startingPageUrl, new UTF8Encoding(), new WebClient() ); 
} 

/* This is called recursively for EVERY link we find */ 
public void parseUrl (string url, UTF8Encoding enc, WebClient browser) { 
    if (visited.Contains(url)) { 
        // Url already spidered, skip and go to next link 
        Response.Write ("<br><font size=-2>  
                 "+ url +" already spidered</font>"); 
    } else { 
        // Add this URL to the 'visited' list, so we'll
        // skip it if we come across it again 
        visited.Add(url); 
        string fileContents =
           enc.GetString (browser.DownloadData(url)); // from Listing 1  
        // ### Pseudo-code ### 
        // 1. Find links in the downloaded page
        //      (add to linkLocal ArrayList - code in Listing 2) 
        // 2. Extract <TITLE> and <META> Description,
        //      Keywords (as Version 1 Listing 4) 
        // 3. Remove all HTML and whitespace (as Version 1) 
        // 4. Convert words to string array,
        //      and add to catalog  (as Version 1 Listing 7) 
        // 5. If any links were found, recursively call this page 
        if (null != pmd.LocalLinks) 
        foreach (object link in pmd.LocalLinks) { 
            parseUrl (Convert.ToString(link), enc, browser); 
        } 
    } 
}

清单3 - 结合链接解析和页面下载代码。

回顾搜索蜘蛛的三个基本任务,您会发现我们已经开发了足够的代码来构建它

  1. 查找要索引的页面 - 我们可以从特定的 URL 开始,并使用列表 2 和 3 查找链接。
  2. 成功下载每个页面 - 我们可以使用列表 1 和 2 中的 WebClient 完成此操作。
  3. 解析页面内容并进行索引 - 我们已经从第一篇文章中获得了此代码。

尽管上面的例子对它将找到的链接很挑剔,但它*确实*可以“爬取”并搜索一个网站!供您参考,代码的“alpha 版本”可在ZIP 文件中找到,以及本文的完整代码。本文的其余部分讨论了修复 alpha 版本中所有“问题”所需的更改。

修复蜘蛛程序 [SearcharooSpider.aspx]

问题1 - 正确解析相对链接

alpha 代码无法遵循“相对”和“绝对”链接(例如,分别为“../../News/Page.htm”和“/News/Page2.htm”),部分原因在于它没有“记住”正在解析的文件夹/子目录。我的第一个想法是构建一个新的“Url”类,它将接受页面 URL 和链接,并封装构建完整链接所需的代码,通过解析目录遍历(例如,“../”)和绝对引用(例如,以“/”开头)。代码需要做类似以下的事情

页面 URL 页面中的链接 结果应该是
https:///News/ Page2.htm https:///News/Page2.htm
https:///News/ ../Contact.htm https:///Contact.htm
https:///News/ /Downloads/ https:///Downloads/
等等。
解决方案:Uri 类

使用类库时要学习的第一课是在编码之前先查看。我几乎是偶然发现了Uri类,它有一个构造函数 -

new  Uri (baseUri, relativeUri)

这正是我需要的。无需重复造轮子!

问题2 - 遵循重定向

由于 WebClient 类虽然使我们能够快速启动蜘蛛程序,但它相当“笨”,因此遵循相对链接变得更加困难。它没有公开正确模拟网络浏览器行为所需的所有属性和方法...它能够遵循服务器发出的重定向,但它没有简单的接口来向调用代码准确地传达它最终请求的 URL。

解决方案:HttpWebRequest 和 HttpWebResponse 类

HttpWebRequestHttpWebResponse 类为 HTTP 通信提供了更强大的接口。HttpWebRequest 具有许多有用的属性,包括

  • AllowAutoRedirect - 可配置!
  • MaximumAutomaticRedirections - 重定向可以限制,以防止不良页面中的“无限循环”。
  • UserAgent - 设置为 "Mozilla/6.0 (MSIE 6.0; Windows NT 5.1; Searcharoo.NET Robot)"(参见下面的问题 5)。
  • KeepAlive - 高效利用连接。
  • Timeout - 可根据目标网站的预期性能进行配置。

这些设置在代码中,以帮助我们获取所需的页面。HttpWebResponse 有一个关键属性 - ResponseUri - 它返回读取的最终 URI;例如,如果我们尝试访问https:///,并且服务器发出了 302 重定向到/en/index.html,那么HttpWebResponseInstance.ResponseUri将是https:///en/index.html,而不是仅仅是https:///。这很重要,因为除非我们知道当前页面的 URL,否则我们无法正确处理相对链接(参见问题 1)。

问题3 - 下载文件时使用正确的字符集

假设所有网页都使用 ASCII 将导致许多页面被“索引”为乱码,因为字节将被转换为“随机”字符,而不是它们实际表示的文本。

解决方案:HttpWebResponse 和 Encoding 命名空间

HttpWebResponse 相对于 WebClient 还有另一个优点:它更容易访问 HTTP 服务器头,例如 ContentTypeContentEncoding。这使得可以编写以下代码:

if (webresponse.ContentEncoding != String.Empty) { 
    // Use the HttpHeader Content-Type
    // in preference to the one set in META 
    htmldoc.Encoding = webresponse.ContentEncoding; 
} else if (htmldoc.Encoding == String.Empty) { 
    // TODO: if still no encoding determined,
    // try to readline the stream until we 
    // find either * META Content-Type
    // or * </head> (ie. stop looking for META) 
    htmldoc.Encoding = "utf-8"; // default 
} 
//http://www.c-sharpcorner.com/Code/2003/Dec/ReadingWebPageSources.asp 
System.IO.StreamReader stream = new System.IO.StreamReader 
          (webresponse.GetResponseStream(), 
          Encoding.GetEncoding(htmldoc.Encoding) ); 

// we *may* have been redirected... and we want the *final* URL 
htmldoc.Uri = webresponse.ResponseUri; 
htmldoc.Length = webresponse.ContentLength; 
htmldoc.All = stream.ReadToEnd (); 
stream.Close();

列表4 - 检查HTTP内容编码,并使用正确的编码类来处理从服务器返回的Byte[] Array

在代码的其他地方,我们使用 ContentType 来解析数据的 MIME 类型,以便我们可以忽略图像和样式表(并且,对于此版本,还包括 Word、PDF、ZIP 和其他文件类型)。

问题4 - 无法识别许多有效链接格式

在构建 alpha 代码时,我实现了我能找到的最简单的正则表达式来定位字符串中的链接——(?<=<(a|area)\s+href=").*?(?="\s*/?>)。问题在于它太简单了,无法找到大多数链接。

解决方案:更智能的正则表达式

正则表达式可以非常强大,显然需要一个更复杂的表达式。由于我不是这方面的专家,我求助于 Google,最终找到了Matt Bourne,他发布了几个非常有用的正则表达式模式,从而产生了以下代码

// http://msdn.microsoft.com/library/en-us/script56/html/js56jsgrpregexpsyntax.asp 
// Original Regex, just found <a href=""> links; and was "broken"
// by spaces, out-of-order, etc 
// @"(?<=<a\s+href="").*?(?=""\s*/?>)" 
foreach (Match match in Regex.Matches(htmlData, 
  @"(?<anchor><\s*(a|area)\s*(?:(?:\b\" + 
  @"w+\b\s*(?:=\s*(?:""[^""]*""|'[^']*'|[^""'<> ]+)\s*)?)*)?\s*>)" 
, RegexOptions.IgnoreCase|RegexOptions.ExplicitCapture)) { 
    // Parse ALL attributes from within tags...
    // IMPORTANT when they're out of order!! 
    // in addition to the 'href' attribute, there might
    // also be 'alt', 'class', 'style', 'area', etc... 
    // there might also be 'spaces' between the attributes
    // and they may be ", ', or unquoted 
    link=String.Empty; 

    foreach (Match submatch in Regex.Matches(match.Value.ToString(), 
      @"(?<name>\b\w+\b)\s*=\s*(""(?<value" + 
      @">[^""]*)""|'(?<value>[^']*)'|(?<value>" + 
      @"[^""'<> \s]+)\s*)+", 
      RegexOptions.IgnoreCase|RegexOptions.ExplicitCapture)) { 
        // we're only interested in the href attribute 
        // (although in future maybe index the 'alt'/'title'?) 
        if ("href" == submatch.Groups[1].ToString().ToLower() ) { 
            link = submatch.Groups[2].ToString(); 
            break; 
        } 
    } 
    /* check for internal/external link 
       and supported scheme, then add to ArrayList */ 
} // foreach

列表5 - 更强大的正则表达式匹配

  1. 匹配完整的链接标签(从 <>),包括标签名称和所有属性。每个匹配的 Match.Value 可以是前面显示的任何链接示例。
    <a href='News.htm'> 
    <a href=News.htm>
    <a class="cssLink" href="News.htm"> 
    <area shape="rect" coords="0,0,110,20" href="News.htm">
    <area href='News.htm' shape="rect" coords="0,0,110,20">
  2. 第二个表达式匹配每个属性的键值对,因此它将返回
    href='News.htm' 
    href=News.htm 
    class="cssLink" href="News.htm" 
    shape="rect" coords="0,0,110,20" href="News.htm" 
    href='News.htm' shape="rect" coords="0,0,110,20"
  3. 我们访问匹配中的组,并且只获取href属性的值,这成为我们处理的链接。

这两个正则表达式的结合使得链接解析更加健壮。

问题5 - META标签处理不佳

alpha 版本的 META 标签处理非常初级——以至于它意外地假设了 <META NAME="" CONTENT=""> 而不是正确的 <META HTTP-EQUIV="" CONTENT=""> 格式。正确处理 META 标签有两种方法

  1. 获取此文档的DescriptionKeywords,以及
  2. 读取ROBOTS标签,以便我们的蜘蛛程序在遇到不应被索引的内容时表现良好。
解决方案:更智能的正则表达式和对更多标签的支持

使用问题 4 中正则表达式的一个变体,代码按需解析 META 标签,将 KeywordsDescription 添加到索引内容中,并存储 Description 以在搜索结果页面上显示。

string metaKey = String.Empty, metaValue = String.Empty; 
foreach (Match metamatch in Regex.Matches (htmlData, 
  @"<meta\s*(?:(?:\b(\w|-)+\b\s*(?:=\s*(?:""[^""]*""|'" + 
  @"[^']*'|[^""'<> ]+)\s*)?)*)/?\s*>", 
  RegexOptions.IgnoreCase|RegexOptions.ExplicitCapture)) { 
    metaKey = String.Empty; 
    metaValue = String.Empty; 
    // Loop through the attribute/value pairs inside the tag 
    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) ) {
            // if already set, HTTP-EQUIV overrides NAME
            metaKey = submetamatch.Groups[2].ToString(); 
        } 
        if ("content" == submetamatch.Groups[1].ToString().ToLower() ) { 
            metaValue = submetamatch.Groups[2].ToString(); 
        } 
    } 
    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; 
    } 
}

清单6 - 解析META标签是一个两步过程,因为我们必须检查'name/http-equiv',这样我们才知道内容与什么相关!

如果 META 标签中出现 ROBOTS NOINDEXNOFOLLOW 指令,它也会遵守(您可以阅读更多关于与 META 标签相关的机器人排除协议;请注意,我们尚未实现对网站根目录下的 robots.txt 文件的支持——也许在版本 3 中会实现!)。我们还将我们的 User-Agent(解决方案 2)设置为指示我们是一个机器人,以便我们爬行的任何网站的网络日志将明确区分我们的请求与普通浏览器;它还使我们能够防止 Searcharoo 索引自身。

蜘蛛爬行网络!

当您加载 SearcharooSpider.aspx 页面时,它会立即开始爬取,起始点可以是

  1. 文件所在文件夹中的根文档,

  2. web.config中指定的位置(如果存在)。

Screenshot 1 - testing the file crawler

屏幕截图1 - 每个被爬取的页面标题都会显示。我们使用CIA世界概况作为测试数据.

目录建成后,您就可以进行搜索了。

执行搜索

所有艰苦的工作都在第一篇文章中完成——这些代码重复供您参考...

/// <summary>Returns all the Files which
/// contain the searchWord</summary> 
/// <returns>Hashtable </returns> 
public Hashtable Search (string searchWord) { 
    // apply the same 'trim' as when we're building the catalog 
    searchWord = 
      searchWord.Trim('?','\"', ',', '\'', ';', ':', '.', '(', ')').ToLower(); 
    Hashtable retval = null; 
    if (index.ContainsKey (searchWord) ) { // does all the work !!! 
        Word thematch = (Word)index[searchWord]; 
        retval = thematch.InFiles(); // return the collection of File objects 
    } 
    return retval; 
}

第一篇文章 列表8 - Catalog 对象的 Search 方法

我们没有修改本文开头图表中的任何搜索对象,旨在展示数据封装如何让您在不影响数据层的情况下更改数据收集方式(即从文件系统爬行到网站爬行)和数据呈现方式(即更新搜索结果页面)。在第三篇文章中,我们将探讨是否有可能将搜索对象转换为使用数据库后端,而不影响收集和呈现类...

改进结果 [SearcharooToo.aspx]

我们将对结果页进行以下更改

  • 启用对多个单词的搜索,并要求所有术语都出现在结果文档匹配中(布尔 AND 搜索)
  • 改进的格式,包括
    • 结果页上预填充的搜索框
    • 查询中每个词的文档计数,以及查看这些结果的链接
    • 执行查询所需的时间

支持多词搜索的首个更改是“解析”用户输入的查询。这意味着:修剪查询周围的空格,并压缩查询词之间的空格。然后我们将查询Split成一个词的Array[],并Trim掉每个词周围的标点符号。

searchterm = Request.QueryString["searchfor"].ToString().Trim(' '); 
Regex r = new Regex(@"\s+");              //remove all whitespace 
searchterm = r.Replace(searchterm, " ");  // to a single space 
searchTermA = searchterm.Split(' ');      // then split 
for (int i = 0; i < searchTermA.Length; i++) {  // array of search terms  
    searchTermA[i] = searchTermA[i].Trim 
            (' ', '?','\"', ',', '\'', ';', ':', '.', '(', ')').ToLower();
}

清单7 - Catalog 对象的 Search 方法

现在我们已经有了各个搜索词的Array,我们将找到匹配每个单独词的所有文档。这是通过使用第一篇文章中相同的m_catalog.Search()方法完成的。每次搜索后,我们检查是否返回了任何结果,并将它们存储在searchResultsArrayArray中以便进一步处理。

// Array of arrays of results that match ONE of the search criteria 
Hashtable[] searchResultsArrayArray = new Hashtable[searchTermA.Length]; 
// finalResultsArray is populated with pages
// that *match* ALL the search criteria 
HybridDictionary finalResultsArray = new HybridDictionary(); 
// Html output string 
string matches=""; 
bool botherToFindMatches = true; 
int indexOfShortestResultSet = -1, lengthOfShortestResultSet = -1; 

for (int i = 0; i < searchTermA.Length; i++) {
// ##### THE SEARCH ##### 
    searchResultsArrayArray[i] = m_catalog.Search (searchTermA[i].ToString()); 
    if (null == searchResultsArrayArray[i]) { 
    matches += searchTermA[i] 
            + " <font color=gray " + 
            "style='font-size:xx-small'>(not found)</font> "; 
    // if *any one* of the terms isn't found,
    // there won't be a 'set' of matches 
    botherToFindMatches = false; 
    } else { 
        int resultsInThisSet = searchResultsArrayArray[i].Count; 
        matches += "<a href=\"?searchfor="+searchTermA[i]+"\">" 
            + searchTermA[i] 
            + "</a> <font color=gray style='font-size:xx-small'>(" 
            + resultsInThisSet + ")</font> "; 
        if ( (lengthOfShortestResultSet == -1) || 
             (lengthOfShortestResultSet > resultsInThisSet) ) { 
        indexOfShortestResultSet = i; 
        lengthOfShortestResultSet = resultsInThisSet; 
        } 
    }
}

列表8 - 分别查找每个词的结果

描述我们如何找到匹配查询中所有单词的文档,最简单的方法是举例说明,想象我们在CIA 世界概况中搜索查询“snow cold weather”。列表 8 找到了匹配每个单词的文档数组,并将它们放在另一个数组中。“snow”有 10 个匹配文档,“cold”有 43 个匹配文档,“weather”有 22 个匹配文档。

显然,整体匹配的最大可能数量是 10(最小结果集),最小值为零——可能没有文档出现在所有三个集合中。这两种可能性都得到了考虑——indexOfShortestResultSet 记住哪个单词的结果最少,如果任何单词未能获得单个匹配,则 botherToFindMatches 设置为false

Conceptual diagram - intersection of result sets

图1 - 查找每个词的结果集交集涉及遍历“数组的数组”

清单9 展示了我们如何解决这个问题。它可能不是最有效的方法,但它有效!基本上,我们选择最小的结果集并遍历其匹配文件,遍历 SearchResultsArrayArray(计数器“cx”)在所有其他结果集中查找相同的文件。

想象一下,参考上图,我们从[0][0]文件D开始(我们从索引[0]“snow”开始,因为它最小,而不是仅仅因为它在第0项)。下面的循环现在将开始检查所有其他文件,看是否再次找到D...但它不会从集合[0]开始,因为我们已经知道D在该集合中是唯一的。“if (cx==c)”检查该条件并防止遍历结果集[0]。

计数器“cx”将递增到1,循环将开始检查项[1][0]、[1][1]、[1][2]、[1][3]、[1][4](文件GESHKD),但是“if (fo.Key = fox.Key)”不会匹配,因为我们仍在搜索文件[0][0] D的匹配项。然而,在下一次迭代中,文件[1][5]被发现是文件D,所以我们知道文件D匹配“snow”和“cold”!

下一个问题是,我们如何记住这个文件存在于这两个集合中?我选择了一个非常简单的解决方案——计算我们正在比较的集合数量totalcount——当我们在一个集合中找到该文件时,不断增加matchcount。然后我们可以安全地break出那个循环(知道该文件在结果集中是唯一的,而且即使它在那里重复我们也不关心),并开始检查下一个结果集。

循环完成后,“if (matchcount == totalcount)”表示我们知道此文件存在于所有集合中,可以将其添加到FinalResultsArray中,这就是我们将用于向用户显示结果页面的内容。

循环将继续,‘cx’递增到 2,“weather”匹配项将被检查文件 D。它在位置 [2][2] 被找到,matchcount 将相应调整。整个循环过程将再次从“snow”匹配项 [0][1] 文件 G 开始,所有其他文件将再次与此文件进行检查,以查看它是否存在于所有集合中。

经过大量循环后,代码将发现只有文件DG存在于所有三个集合中,并且finalResultsArray将只有两个元素,它将这些元素传递给第一篇文章中列表10-13的相同显示代码。

// Find the common files from the array of arrays of documents 
// matching ONE of the criteria 
if (botherToFindMatches) {
// all words have *some* matches
    // loop through the *shortest* resultset
    int c = indexOfShortestResultSet;
    Hashtable searchResultsArray = searchResultsArrayArray[c]; 

    if (null != searchResultsArray) 
    foreach (object foundInFile in searchResultsArray) {
    // for each file in the *shortest* result set 
        DictionaryEntry fo = (DictionaryEntry)foundInFile;
        // find matching files in the other resultsets 

        int matchcount=0, totalcount=0, weight=0; 

        for (int cx = 0; cx < searchResultsArrayArray.Length; cx++) { 
            // keep track, so we can compare at the end (if term is in ALL)
            totalcount+=(cx+1);
            if (cx == c) {
            // current resultset
                // implicitly matches in the current resultset
                matchcount += (cx+1);
                // sum the weighting
                weight += (int)fo.Value;
            } else { 
                Hashtable searchResultsArrayx = searchResultsArrayArray[cx]; 
                if (null != searchResultsArrayx) 
                foreach (object foundInFilex in searchResultsArrayx) {
                // for each file in the result set 
                    DictionaryEntry fox = (DictionaryEntry)foundInFilex; 
                    if (fo.Key == fox.Key) {
                    // see if it matches
                        // and if it matches, track the matchcount
                        matchcount += (cx+1);
                        // and weighting; then break out of loop, since
                        weight += (int)fox.Value;
                        break;
                        // no need to keep looking through this resultset 
                    } 
                } // foreach 
            } // if 
        } // for 
        if ( (matchcount>0) && (matchcount == totalcount) ) {
        // was matched in each Array
            // set the final 'weight'
            // to the sum of individual document matches
            fo.Value = weight;
            if ( !finalResultsArray.Contains (fo.Key) ) 
                  finalResultsArray.Add ( fo.Key, fo); 
        } // if 
    } // foreach 
} // if

清单9 - 查找包含查询中所有单词的文档子集。其中有三个嵌套循环——我从没说过这很高效!

上面描述的算法正在对查询中的所有单词执行布尔 AND 查询,即,示例正在搜索“snow AND cold AND weather”。如果我们要构建一个 OR 查询,我们可以简单地遍历所有文件并过滤掉重复项。OR 查询并不是那么有用,除非您可以将它们与 AND 子句结合起来,例如“snow AND (cold OR weather)”——但版本 2 不支持此功能!

顺便说一下,我在代码中为简单起见称之为“Array”的变量,实际上是HashTableHybridDictionary。查看代码时请不要感到困惑——选择每个Collection类都有充分的理由(主要是因为我事先不知道最终的项数,所以使用Array太难了)。

最终结果

Screenshot 2 - Minor changes

屏幕截图2 - 搜索输入页面有细微改动,包括文件名改为 SearcharooToo.aspx!

Screenshot 3 - Lots of changes to the search results

截图3 - 您可以优化搜索,查看每个搜索词的匹配数量,查看执行搜索所花费的时间,最重要的是,查看包含查询中所有词的文档!

使用示例代码

本文的目标是构建一个简单的搜索引擎,您只需将一些文件放在您的网站上即可安装;因此您可以将 Searcharoo.csSearcharooSpider.aspxSearcharooToo.aspx 复制到您的网站根目录即可!然而,这意味着您接受所有默认设置,例如从网站根目录爬取,以及下载页面时五秒的超时。

要更改这些默认设置,您需要向 web.config 添加一些设置

<appSettings>
   <!--website to spider-->
   <add key="Searcharoo_VirtualRoot" value="https:///" />
   <!--5 second timeout when downloading-->  
   <add key="Searcharoo_RequestTimeout" value="5" />  
   <!--Max pages to index--> 
   <add key="Searcharoo_RecursionLimit" value="200" />
</appSettings>

清单14 - web.config

然后,只需导航到 https:///SearcharooToo.aspx(或您放置 Searcharoo 文件的地方),它将首次构建目录。

如果您的应用程序因任何原因重新启动(例如,您将代码编译到/bin/文件夹中,或更改web.config设置),则需要重新构建目录——下一个执行搜索的用户将触发目录构建。这是通过检查缓存是否包含有效的Catalog来完成的,如果没有,则使用Server.Transfer启动蜘蛛并在完成后返回搜索页面。

未来

SearcharooSpider.aspx 大大增强了 Searcharoo 的实用性,因为您现在可以索引您的静态动态(例如,数据库生成)页面,以允许访问者搜索您的网站。这意味着您可以将其与 Microsoft 内容管理服务器 (CMS) 等产品一起使用,该产品不直接公开其内容数据库。

Searcharoo 剩下的两个(主要)问题是

  1. 它无法将目录持久化到磁盘或数据库——这意味着一个非常大的网站将导致大量内存用于存储目录,并且
  2. 大多数网站不仅包含 HTML 页面;它们还链接到 Microsoft Word 或其他 Office 文件、Adobe Acrobat(PDF 文件)以及 Searcharoo 目前无法“理解”(即,解析和编目)的其他形式的内容。

本系列的下一篇文章(希望)将在 CodeProject 和ConceptDevelopment.NET上更详细地探讨这两个问题...

词汇表

术语 含义
HTML 超文本标记语言
HTTP 超文本传输协议
URL 统一资源定位符
URI 统一资源标识符
DOM 文档对象模型
302 重定向 HTTP 状态码,告诉浏览器重定向到不同的 URL/页面。
UTF-8 Unicode 转换格式 - 8 位
MIME 类型 多用途互联网邮件扩展
蜘蛛 通过查找并跟随 HTML 中的链接,从一个网页到另一个网页的程序:想象一只蜘蛛在网上爬行 :)
爬虫 尽管“蜘蛛”和“爬虫”这两个术语经常互换使用,但我们将“爬虫”用于指在文件系统或外部“列表”上定位目标页面的程序;而“蜘蛛”将通过嵌入链接查找其他页面。
Shift_JIS, GB2312 字符集...
搜索引擎词汇表

后记:关于 Code-behind 和 Visual Studio .NET?(摘自第一篇文章)

您会注意到两个 ASPX 页面使用 src="Searcharoo.cs" @Page 属性来共享公共对象模型,而无需编译成程序集,其中特定于页面的“内联”代码使用 <script runat="server"> 标签(类似于 ASP 3.0)。

这种方法的优点是您可以将这三个文件放置在任何 ASP.NET 网站中,它们就能“正常工作”。没有其他依赖项(尽管如果您设置了一些 web.config 设置,它们会工作得更好),也不需要担心 DLL 文件。

然而,这也意味着这些页面无法在 Visual Studio .NET 中编辑,因为它不支持 @Page src="" 属性,而是倾向于使用 codebehind="" 属性与编译到 /bin/ 目录的 CS 文件结合使用。要在 Visual Studio .NET 中使这些页面工作,您必须设置一个项目并添加 CS 文件和两个 ASPX 文件(如果您喜欢,可以将 <script> 代码移到代码隐藏中),然后编译。

链接

历史

  • 2004-06-30: CodeProject 上的版本 1
  • 2004-07-03: CodeProject 上的版本 2。
© . All rights reserved.