智能 404 页面






4.74/5 (27投票s)
2004年12月6日
6分钟阅读

132855

1323
本文介绍如何通过提供与用户请求页面名称相似的页面链接来增强您的404页面。
引言
您是否注意到像 www.microsoft.com 这样的大型网站,当您访问一个不存在的页面时,它们不会简单地显示“抱歉,404”,而是会提供一个与您请求的页面相似的页面列表。这显然对您的用户非常有益,并且很容易集成到您的网站中。本文提供了源代码并解释了实现此功能的算法。注意:本文提出的方法的真正价值在于半智能的字符串比较。
背景
我的一个客户在更改内容管理系统时,网站上的所有 URL 都发生了变化,导致所有搜索引擎结果都显示 404 页面,这显然非常不便。因此,我创建了这个工具,以帮助用户在从搜索引擎访问时找到新网站的路径。
查看实际效果
访问 此页面(该页面不存在),404 页面应会列出名称非常相似的页面。
要求
- 您的网站必须设置为将 404 页面重定向到一个 .NET aspx 页面。
- 您必须能够获取您网站上所有要与 404 请求进行比较的页面 URL 数组。如果您有一个内容管理系统,很可能有一个存储了所有页面信息的 XML 文件或 JavaScript 数组(用于 DHTML 菜单等),或者您可以编写自己的查询从数据库中获取页面。如果不是,请使用内容管理系统,您可以在 404 页面的代码隐藏文件中硬编码一个字符串数组变量来包含页面名称,或者考虑某种方法来动态读取文件系统中的所有 .aspx 或 .html 页面。
- 当访问 404 页面时,您需要知道请求的是哪个页面。通过 web.config,您可以设置 404 错误代码定向到 /404.aspx,它会在查询字符串中附加请求的页面。此处的源代码假定您采用了此方法,但您可以根据自己的需求进行更改;只需修改
GetRequestedUrl()
函数即可。
为什么正则表达式不够用
要比较字符串,您可以使用 System.String.IndexOf
或者使用正则表达式来匹配相似性,但所有这些方法对于字符串中的细微差别都非常死板。在上面的示例 URL 中,页面名称是 December15-ISERCWorkshoponTesting.html,但在新的内容管理系统中,URL 是 December 15 - ISERC Workshop - Software Testing.html,这足以使传统的字符串比较技术失效。
因此,我寻找了一个模糊字符串比较例程,并偶然发现了一个由 Levenshtein 编写的算法。他的算法根据将一个字符串转换为另一个字符串所需的字符添加、删除和修改次数来计算两个字符串的差异。这被称为“编辑距离”,即要使两个字符串匹配所需的距离。这非常有用,因为它考虑了空格、标点符号和拼写上的细微差别。我在 这里 发现了这个算法,Lasse Johansen 好心地将其移植到了 C#。该网站解释了该算法,并且非常值得一读,以了解其工作原理。
标准化分数
起初,我发现该算法在某些情况下会产生令人惊讶的结果。如果 404 页面请求是“hello”,而存在一个名为“hello_new_version”的有效页面和另一个名为“abcde”的有效页面,那么“abcde”页面会获得更高的分数,因为将其更改为“hello”所需的更改更少(只需将“abcde”中的五个字符更改为“hello”)。这需要五次更改,即使“hello_new_version”在语义上是更好的匹配。幸运的是,一位名叫 Patrice 的新闻组参与者建议我将分数除以比较字符串的长度来标准化结果。这效果非常好,我发现分数在 0(完美匹配)和 0.6(良好匹配)之间值得包含为建议页面。您可以在 ComputeResults()
方法中更改此值,以使其更灵活或更不灵活。
代码摘要
private void Page_Load(object sender, System.EventArgs e)
{
GetRequestedUrl();
SetUpSiteUrls();
ComputeResults();
BindList();
}
上面的代码展示了构成此解决方案的四个关键任务。下面将解释每个方法
使用代码
GetRequestedUrl()
仅用于确定请求的页面。在此示例中,假定您的 web.config 包含以下内容<system.web> <customErrors mode="On"> <error statusCode="404" redirect="/404.aspx" /> </customErrors>
在此示例中,404.aspx 页面上的查询字符串包含请求的 URL。
private void GetRequestedUrl() { // assumes that web.config redirects 404 requests to 404.aspx, // putting the requested url // in the querystring: ?aspxerrorpath=/whatever.aspx this.requestedUrl = String.Concat(Request.QueryString["aspxerrorpath"],""); if(this.requestedUrl == "") // nothing to compare with return; try { this.requestedUrl = System.IO.Path.GetFileNameWithoutExtension(requestedUrl); } catch { return; // the referrer contained illegal characters } }
SetUpSiteUrls()
是加载您网站上所有页面的地方。在我的内容管理系统中,我有一个包含所有名称的 XML 文件,因此我执行 XPath 查询并将名称一个一个地添加到ArrayList
中。private void SetUpSiteUrls() { this.validUrls = new ArrayList(); /* * Insert code here to add the pages in your site to this arraylist */ }
ComputeResults()
遍历您在SetUpSiteUrls
中设置的 URL,并附加每个 URL 与请求 URL 的相似度分数。它还会对结果进行排序并丢弃任何不匹配的。private void ComputeResults() { ArrayList results = new ArrayList(); // used to store the results // build up an arraylist of the positive results foreach(string s in validUrls) { // don't waste time calculating the edit // distance of nothing with something if(s == "") continue; double distance = Levenshtein.CalcEditDistance(s, this.requestedUrl); // both in lower case double meanDistancePerLetter = (distance / s.Length); if(meanDistancePerLetter <= 0.60D) // anything between 0.0 and 0.6 is a good match. // The algorithm always returns a value >= 0 { // add this result to the list. NOTE: you will need some // way of inserting the correct url in the hyperlink below // (the url represented by 's' doesn't // have a file extension or its folder context) results.Add(new DictionaryEntry(meanDistancePerLetter, "<a href='" + s + ".html'>" + s + "</a>")); // use dictionary entries because we want to store the score // and the hyperlink. can't use sortedlist because they don't // allow duplicate keys and we have 2 hyperlinks // with the same edit distance. } } results.Sort(new ArrayListKeyAscend()); }
重要提示:上面代码中最里面的行绝对值得注意:
results.add(new DictionaryEntry(...)
。我正在添加一个 HTML 超链接,其名称为页面名称 + “.html”。这在您的网站上可能不是一个正确的链接,因为您可能在填充validUrls
ArrayList
时删除了 URL 的文件夹部分。您可能需要扩展此代码中使用的数据结构以包含每个页面的完整 URL。BindList()
仅将结果的ArrayList
绑定到DataGrid
,该DataGrid
配置为将它们显示为项目符号列表。private void BindList() { if(results.Count > 0) { this.lblHeader.Text = "The following pages have similar names to <i>" + this.requestedUrl + "</i>"; this.DataList1.DataSource = results; this.DataList1.DataBind(); } else { this.lblHeader.Text = "Unable to find any pages in this site that" + " have similar names to <i>" + this.requestedUrl + "</i>"; } }
代码中的“魔力”都来自于 Levenshtein.CalcEditDistance
方法,该方法返回两个字符串之间的距离。它包含在源代码中。
WinForms 测试应用程序
如果您有兴趣测试 Levenshtein 算法,我编写了一个 Windows Forms 应用程序,它允许您输入一个字符串(例如,页面 URL)以及一个要与之比较的字符串列表(例如,您网站上的所有页面 URL),并给出“编辑距离”分数。 在此下载 - 7.04 Kb。
注释
我认为这是一个很棒的功能,因为它极大地提升了网站的用户体验。如果您有任何问题、发现任何错误、改进建议,或者无法正常工作,或者以新颖的方式使用了它,请随时在下方评论。
尽情享用!