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

创建您自己的 Googlebot

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (17投票s)

2011年8月20日

CPOL

10分钟阅读

viewsIcon

54582

downloadIcon

3763

在本文中,我将解释如何创建一个类似于 Googlebot 的应用程序——它通过超链接查找和索引网站。

引言

Googlebot 通过超链接查找和索引网络。它仍然通过旧站点中的超链接访问新页面。我的搜索机器人(Xearchbot)可以查找站点并将其 URL、标题、关键字元标记和描述元标记存储在数据库中。将来,它将存储转换为纯文本的文档正文。我不计算 PageRank,因为它非常耗时。

在 Googlebot 下载页面之前,它会下载文件 robots.txt。此文件中包含有关机器人可以去哪里以及不能去哪里的信息。这是此文件内容的示例

# All bots can't go to folder "nobots":
User-Agent: *
Disallow: /nobots

# But, ExampleBot can go to all folders:
User-Agent: ExampleBot
Disallow:

# BadBot can't go to "nobadbot1" and "nobadbot2" folders:
Disallow: /nobadbot1
Disallow: /nobadbot2

阻止 Googlebot 的第二种方法是:Robots 元标记。其 name 属性为“robots”,content 属性的值由逗号分隔。有“index”或“noindex”(文档是否可以索引)和“follow”或“nofollow”(是否跟踪超链接)。对于索引文档和跟踪超链接,元标记如下所示

<meta name="Robots" content="index, follow" />

也支持阻止单个链接的跟踪——为此,其 rel="nofollow"。恶意软件机器人和防病毒软件机器人会忽略 robots.txt、元标记和 rel="nofollow"。我们的机器人将是一个正常的搜索机器人,并且必须允许所有以下阻止程序。

有一个名为 User-Agent 的 HTTP 头。在此头中,客户端应用程序(例如,Internet Explorer 或 Googlebot)将进行呈现。例如,IE6 的 User-Agent 如下所示

User-Agent: Mozilla/4.0 (Compatible; Windows NT 5.1; MSIE 6.0)
(compatible; MSIE 6.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)

是的,Internet Explorer 的 HTTP 名称是 Mozilla... 这是 Googlebot 2.1 User-Agent 头

User-Agent: Googlebot/2.1 (+http://www.googlebot.com/bot.html)

括号中,加号字符后面的地址是有关机器人的信息。我们为我们的机器人在此头中放入类似的数据。

为了加快搜索机器人的速度,我们可以支持 GZIP 编码。我们可以添加值为“gzip”的 Accept-Encoding 头。有些网站允许 GZIP 编码,如果接受 gzip,它们会向我们发送压缩文档。如果内容已压缩,则服务器将在响应中添加值为“gzip”的 Content-Encoding 头。我们可以使用 System.IO.Compression.GZipStream 解压缩文档。

对于解析 robots.txt,我使用 string 函数(IndexOfSubstring...),对于解析 HTML,我使用正则表达式。在本文中,我们将使用 HttpWebRequestHttpWebResponse 下载文件。最初,我考虑使用 WebClient,因为它更容易,但在该类中我们无法设置下载超时。

对于本文,需要 SQL Server(可以是 Express)和基本的 SQL 知识(DataSets 等)。

基础知识已讨论,现在让我们编写一个搜索机器人!

数据库

首先我们必须创建一个新的 Windows Forms 项目。现在添加一个名为 SearchbotData 的本地数据库并创建其名为 SearchbotDataSet 的 DataSet。向数据库添加表 Results

列名 数据类型 长度 允许空值 主键 身份
id_result int 4
url_result nvarchar 500
title_result nvarchar 100
keywords_result nvarchar 500
description_result nvarchar 1000

在此表中,我们将存储结果。将此表添加到 SearchbotDataSet

准备工作

首先,我们必须添加以下 using 语句

using System.Net;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Compression;
using System.Text.RegularExpressions;

站点将在 Collection 中等待索引

Collection<string> waiting = new Collection<string>(); // Here sites wait for indexing

在网络中,有数十亿个页面,而且它们的数量还在不断增加。我们的机器人永远无法完成索引。所以我们必须有一个变量来停止机器人

bool doscan; 	// Do scanning. If doscan is false then main bot function will exit.

机器人函数将在开始索引下一页之前检查此变量。让我们添加 Scan 方法 - 我们机器人引擎的主要功能。

/// <summary>
/// Scans the web.
/// </summary>
void Scan()
{
    while (waiting.Count > 0 && doscan)
    {
        try
        {
            string url = waiting[0];
            waiting.RemoveAt(0);
            Uri _url = new Uri(url);
        }
        catch { }
    }
}

索引页面的代码将在循环中。开始时,等待队列中必须至少有一个包含超链接的页面。当页面被解析后,它将从等待队列中删除,但找到的超链接将被添加——因此我们有了循环。Scan 函数可以通过例如 BackgroundWorker 在其他线程中运行。

解析 robots.txt

在我们开始索引任何网站之前,我们必须检查 robots.txt 文件。让我们编写一个用于解析此文件的类。

在我的机器人中,我将这个类命名为 RFX - Robots.txt For Xearch。

/// <summary>
/// Robots.txt For Xearch.
/// </summary>
class RFX
{
    Collection<string> disallow = new Collection<string>();
    string u, data;

    /// <summary>
    /// Parses robots.txt of site.
    /// </summary>
    /// <param name="url">Base url of site</param>
    public RFX(string url)
    {
        try
        {
            u = url;
        }
        catch { }
    }
}

url 参数是不带“/robots.txt”的地址。构造函数将下载并解析文件。因此,下载文件:首先创建请求。所有内容都必须在内部 try 语句中,因为会抛出 HTTP 错误。

HttpWebRequest req = (HttpWebRequest)WebRequest.Create(u + "/robots.txt");
req.UserAgent = "Xearchbot/1.1 (+http://www.kmpp.neostrada.pl/xearch.htm)";
req.Timeout = 20000; // Maximal downloading time is 20 seconds.

现在获取响应及其 stream

HttpWebResponse res = (HttpWebResponse)req.GetResponse();
Stream s = res.GetResponseStream();

我们可以通过 StreamReaders 读取所有数据。

StreamReader sr = new StreamReader(s);
data = sr.ReadToEnd();

下载结束时,我们必须关闭所有内容

sr.Close();
res.Close();

现在下载的 robots.txt 存储在 data 变量中,因此我们可以对其进行解析。编写用于解析单个代理部分的功能。

/// <summary>
/// Parses agent in robots.txt.
/// </summary>
/// <param name="agent">Agent name</param>
/// <returns>True if agent exists, false if don't exists.</returns>
bool parseAgent(string agent)
{
}

如果存在针对我们机器人的特定部分,则我们必须仅解析该部分,否则我们必须解析所有机器人的部分。因此将以下行添加到构造函数中。

// If it's section for Xearchbot then don't parse section for all bots.
if (!parseAgent("Xearchbot"))
    parseAgent("*");

现在让我们编写 parseAgent 的代码。首先,我们必须找到指定用户代理部分的开头

int io = data.LastIndexOf("User-agent: " + agent); // Start of agent's section
if (io > -1)
{
    int start = io + 12 + agent.Length;
    return true;
}
else
    return false;

如果该节存在,则最后返回 true,否则返回 false。现在我们必须找到结束。将其添加到 start 行之后。

int count = data.IndexOf("User-agent:", start); // End of agent's section
if (count == -1)
    count = data.Length - start; // Section for agent is last
else
    count -= start + 1; // end - start = count

现在我们有了带有解析命令的区域。我不会再延长了,添加这些行

while ((io = data.IndexOf("Disallow: /", start, count)) >= 0) // Finding disallows
{
    count -= io + 10 - start; // Recalculating count
    start = io + 10; // Moving start
    string dis = data.Substring(io + 10); // The path
    io = dis.IndexOf("\n");
    if (io > -1)
        dis = dis.Substring(0, io).Replace("\r", ""); // Cutting to line end
    if (dis[dis.Length - 1] == '/')
        dis = dis.Substring(0, dis.Length - 1); // Deleting "/" at the end of path.
    disallow.Add(u + dis); // Adding disallow
}

这些行查找并解析 Disallow 语句。现在添加一个新方法来检查我们是否可以解析文档

/// <summary>
/// Checks we can allow the path.
/// </summary>
/// <param name="path">Path to check</param>
/// <returns>True if yes, false if no.</returns>
public bool Allow(string path)
{
    foreach (string dis in disallow)
    {
        if (path.StartsWith(dis))
            return false;
    }
    return true;
}

这将路径与每个不允许的路径进行比较。

现在回到主窗体类并添加机器人文件集合

Dictionary<string, RFX> robots = new Dictionary<string, RFX>(); // Parsed robots files

我们将在其中存储编译后的机器人文件。

检查 URL

我们的机器人将仅支持 HTTP 和 HTTPS 协议。因此,我们必须在 Scan 函数的 try 中检查 url 的方案

Uri _url = new Uri(url);
if (_url.Scheme.ToLower() == "http" || _url.Scheme.ToLower() == "https")
{
}

地址可以有不同的协议。为了消除歧义,让我们重新格式化 url

string bu = (_url.Scheme + "://" + _url.Host + ":" + _url.Port).ToLower();
url = bu + _url.AbsolutePath;

bu 变量是 RFXbaseUrl。现在每个协议都在路径中指定,默认协议 (80) 也是如此。

地址可以被索引,然后我们不应该第二次索引。因此,让我们向 ResultsTableAdapter 添加一个新查询(在将 Result 表添加到数据集时应创建它)。该查询将是 SELECT 类型,它返回一个单一值。它的代码

SELECT COUNT(url_result) FROM Results WHERE url_result = @url

将其命名为 CountOfUrls。它返回指定 URL 的计数。通过使用它,我们可以检查 URL 是否在数据库中。

在主窗体中,添加一个 resultsTableAdapter。如果要在 DataGrid 中显示结果并在扫描时刷新它,请使用 2 个表适配器 - 第一个用于显示,第二个用于 Scan 函数。所以我们必须检查 url 是否已索引

if (resultsTableAdapter2.CountOfUrls(url) == 0)
{
}

现在我们必须检查 robots.txt。我们已经为这些解析的文件声明了一个 Dictionary。如果我们有该站点的已解析 robots.txt,我们从 Dictionary 中获取它,否则我们必须解析 robots.txt 并将其添加到 Dictionary 中。

RFX rfx;
if (robots.ContainsKey(bu))
{
    rfx = robots[bu];
}
else
{
    rfx = new RFX(bu);
    robots.Add(bu, rfx);
}

现在解析后的文件在 rfx 中,我们可以检查 URL

if (rfx.Allow(url))
{
}

url 已检查。我们现在可以继续解析文件。

下载文档

如何下载文档,您已经从“解析 robots.txt”部分了解到。这里更复杂,因为我们必须检查文档类型。

HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.UserAgent = "Xearchbot/1.1 (+http://www.kmpp.neostrada.pl/xearch.htm)";
req.Timeout = 20000;
HttpWebResponse res = (HttpWebResponse)req.GetResponse();

有关文档类型的信息在 Content-Type 头中。此头的值的开头是文档的 MIME 类型。通常在 MIME 之后,还有其他信息,用分号分隔。所以解析 Content-Type

string ct = res.ContentType.ToLower();
int io = ct.IndexOf(";");
if (io != -1)
    ct = ct.Substring(0, io);

HTML 文档的 MIME 类型是 text/html,XHTML 文档也可以是 text/htmlapplication/xhtml+xml。因此,如果文档类型是 text/htmlapplication/xhtml+xml,我们可以处理该文档。

if (ct == "text/html" || ct == "application/xhtml+xml")
{
}
res.Close();

最后,我们必须关闭响应。现在我们必须从文档中读取所有数据(当然将其放在 if 内部)

Stream s = res.GetResponseStream();
StreamReader sr = new StreamReader(s);
string d = sr.ReadToEnd();
sr.Close();

解析元标记

是时候解析文档了。对于解析 HTML 元素,我选择了正则表达式的方式。在 .NET 中,我们可以通过 System.Text.RegularExpressions.Regex 类使用它们。正则表达式是用于比较 string 的强大工具。我不会解释语法。为了查找和解析元标记,我设计了以下正则表达式

<meta(?:\s+([a-zA-Z_\-]+)\s*\=\s*([a-zA-Z_\-]+|\"[^\"]*\"))*\s*\/?>

正则表达式可以存储匹配字符串的某些部分。此正则表达式存储属性的名称和值。

因此在类中声明此正则表达式

/// <summary>
/// Regular expression for getting and parsing Meta tags.
/// </summary>
public Regex parseMeta = new Regex(@"<meta(?:\s+([a-zA-Z_\-]+)\s*\=\s*([a-zA-Z_\-]+|\" +
    '"' + @"[^\" + '"' + @"]*\" + '"' + @"))*\s*\/?>",
    RegexOptions.Compiled | RegexOptions.IgnoreCase);

这里有转义的引号字符。这个正则表达式有 CompiledIgnoreCase 标志。我认为我不需要解释它们是什么意思。现在进入循环并声明这些变量

string title = "";
string keywords = "";
string description = "";
bool cIndex = true;
bool cFollow = true;

让我们解析元标签。使用正则表达式,这非常简单

MatchCollection mc = parseMeta.Matches(d);

mc 是找到的元标签的集合。现在我们必须处理所有元标签

foreach (Match m in mc)
{
}

m 中,我们找到了一个元标签。我们想读取属性。我们可以从 Groups 属性中获取捕获的名称和值。

CaptureCollection names = m.Groups[1].Captures;
CaptureCollection values = m.Groups[2].Captures;

names 中,我们有属性的名称,在 values 中,我们有相同顺序的属性值。如果元标签正确,则名称的数量等于值的数量

if (names.Count == values.Count)
{
}

现在让我们声明将包含元标记名称和内容的变量

string mName = "";
string mContent = "";

现在我们必须在 namesvalues 中找到 name 和 content 属性。

for (int i = 0; i < names.Count; i++)
{
    string name = names[i].Value.ToLower();
    string value = values[i].Value.Replace("\"", "");
    if (name == "name")
        mName = value.ToLower();
    else if (name == "content")
        mContent = value;
}

我们在 mName 中有元标记的名称,在 mContent 中有元标记的内容。我们的机器人将检查机器人元标记,但它也会存储关键字和描述元标记。所以解析它

switch (mName)
{
    case "robots":
        mContent = mContent.ToLower();
        if (mContent.Trim().ToLower().IndexOf("noindex") != -1)
            cIndex = false;
        else if (mContent.IndexOf("index") != -1)
            cIndex = true;
        if (mContent.IndexOf("nofollow") != -1)
            cFollow = false;
        else if (mContent.IndexOf("follow") != -1)
            cFollow = true;
        break;
    case "keywords":
        keywords = mContent;
        break;
    case "description":
        description = mContent;
        break;
}

关键字和描述很简单——我们不需要解析它们。Robots 元标签更复杂,因为它控制着机器人,我们必须解析它。元标签解析完成。

解析超链接和基本标签

为了解析超链接,我创建了另一个正则表达式

<a(?:\s+([a-zA-Z_\-]+)\s*\=\s*([a-zA-Z_\-]+|\"[^\"]*\"))*\s*>

在 HTML 中,有一个标签用于更改所有链接的基本路径。我们必须在我们的机器人中实现这一点。有一个用于解析它的正则表达式

<base(?:\s+([a-zA-Z_\-]+)\s*\=\s*([a-zA-Z_\-]+|\"[^\"]*\"))*\s*\/?>

我们必须声明它

/// <summary>
/// Regular expression for getting and parsing A tags.
/// </summary>
public Regex parseA = new Regex(@"<a(?:\s+([a-zA-Z_\-]+)\s*\=\s*([a-zA-Z_\-]+|\" +
    '"' + @"[^\" + '"' + @"]*\" + '"' + @"))*\s*>",
    RegexOptions.Compiled | RegexOptions.IgnoreCase);

/// <summary>
/// Regular expression for getting and parsing base tags.
/// </summary>
public Regex parseBase = new Regex(@"<base(?:\s+([a-zA-Z_\-]+)\s*\=\s*([a-zA-Z_\-]+|\" +
    '"' + @"[^\" + '"' + @"]*\" + '"' + @"))*\s*\/?>",
    RegexOptions.Compiled | RegexOptions.IgnoreCase);

让我们编写用于解析超链接并将其添加到等待队列的方法。

void follow(string d, Uri abs)
{
}

我们必须查找和解析标签

MatchCollection bases = parseBase.Matches(d);

mc = parseA.Matches(d);
foreach (Match m in mc)
{
    CaptureCollection names = m.Groups[1].Captures;
    CaptureCollection values = m.Groups[2].Captures;
    if (names.Count == values.Count)
    {
        string href = "";
        string rel = "";
        for (int i = 0; i < names.Count; i++)
        {
            string name = names[i].Value.ToLower();
            string value = values[i].Value.Replace("\"", "");
            if (name == "href")
                href = value;
            else if (name == "rel")
                rel = value;
        }
    }
}

我们必须检查 rel 属性。如果其值为 nofollow,则我们不需要将其添加到等待队列。如果 href 为空,则我们也不将 URL 添加到等待队列。href 可以是相对路径。因此我们必须将其与 abs 连接起来。

if (rel.IndexOf("nofollow") == -1 && href != "")
{
    Uri lurl = new Uri(abs, href);
    waiting.Add(lurl.ToString());
}

现在可以回到 Scan 方法。我们必须检查 cFollow 变量——我们在其中存储从机器人元标记中解析出的值。

if (cFollow)
{
}

让我们解析 base 标签

mc = parseBase.Matches(d);
Uri lastHref = _url;
for (int j = 0; j < mc.Count; j++)
{
    Match m = mc[j];
    CaptureCollection names = m.Groups[1].Captures;
    CaptureCollection values = m.Groups[2].Captures;
    if (names.Count == values.Count)
    {
        string href = "";
        for (int i = 0; i < names.Count; i++)
        {
            string name = names[i].Value.ToLower();
            string value = values[i].Value.Replace("\"", "");
            if (name == "href")
                href = value.ToLower();
        }
    }
}

超链接也可以在第一个 base 标签之前。因此,在循环之前,我们必须用默认的绝对路径(文档路径)解析超链接

string d2 = d;
if (mc.Count > 0)
    d2.Substring(0, mc[0].Index);
follow(d2, _url);

现在让我们解析每个 base 标签的链接部分

d2 = d.Substring(m.Index);
if (j < mc.Count - 1)
    d2.Substring(0, mc[j + 1].Index);
if (href != "")
    lastHref = new Uri(href);
follow(d2, lastHref);

base 标签没有 href 属性时,我们使用最近一个 base 标签的 href。

解析标题并添加到数据库

现在是标题和将索引文档添加到数据库的时候了。我们必须检查 index

if (cIndex)
{
}

现在我们必须声明用于解析标题的 regex

/// <summary>
/// Regular expression for getting title of HTML document.
/// </summary>
public Regex parseTitle = new Regex(@"<title(?:\s+(?:[a-zA-Z_\-]+)" +
    "\s*\=\s*(?:[a-zA-Z_\-]+|\" + '"' + @"[^\" + '"' + @"]*\" + '"' +
    @"))*\s*>([^<]*)</title>", RegexOptions.Compiled | RegexOptions.IgnoreCase);

所以让我们解析标题

mc = parseTitle.Matches(d);
if (mc.Count > 0)
{
    Match m = mc[mc.Count - 1];
    title = m.Groups[1].Captures[0].Value;
}

我们只考虑标题标签的最后一次出现。现在我们必须添加新的 SQL INSERT 命令,用于向表中添加一行。它的名称将是 InsertRow。这就是命令

INSERT INTO Results (url_result, title_result, keywords_result, description_result)
VALUES (@url, @title, @keywords, @description)

现在我们可以将结果添加到 Results 表中

resultsTableAdapter2.InsertRow(url, title.Trim(), keywords.Trim(), description.Trim());

这将在标题解析后添加。

GZIP 编码

我们可以使用 GZIP 编码来加快下载速度。有些网站支持此功能。

我们必须向请求添加 Accept-Encoding

req.Headers["Accept-Encoding"] = "gzip";

当我们获得 stream

Stream s = res.GetResponseStream();

...我们必须检查 stream 是否被 gzip 压缩,如果是,则解压缩

Stream s;
if (res.Headers["Content-Encoding"] != null)
{
    if (res.Headers["Content-Encoding"].ToLower() == "gzip")
        s = new GZipStream(res.GetResponseStream(), CompressionMode.Decompress);
    else
        s = res.GetResponseStream();
}
else
    s = res.GetResponseStream();

您可以将其添加到 Scan 方法和 RFX 中。

机器人已完成!

结论

网络中有数十亿个页面,而且它们的数量还在不断增加。为了阻止机器人,我们可以使用 robots.txt 文件、robots 元标记和 rel="nofollow" 属性。恶意机器人将忽略这些阻止程序。为了加快下载速度,我们可以使用 GZIP 编码。正则表达式是解析 string 的强大工具。

历史

  • 2011-08-20 - 首次发布
  • 2011-08-22 - 更正,支持 base 标签
© . All rights reserved.