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

使用 U++ 的简单多请求 Web Crawler。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2016 年 1 月 1 日

CPOL

5分钟阅读

viewsIcon

44103

downloadIcon

1545

使用 U++ 库 HttpRequest 类的异步特性来实现带 GUI 的并行 Web Crawler。

引言

本文使用 U++ 框架。请参阅 Ultimate++ 入门指南 以了解该环境的介绍。

U++ 框架提供了一个能够异步操作的 HttpRequest 类。在本例中,我们将利用此功能来构建一个简单的单线程 Web 爬虫,最多支持 60 个并行 HTTP 连接。

设计 GUI

我们将提供一个简单的 GUI 来显示爬取进度

Click to enlarge image

首先,我们将为我们的应用程序设计一个简单的 GUI 布局。这里的 GUI 非常简单,但仍然值得使用布局设计器

布局由 3 个 ArrayCtrl 小部件组成,它们基本上是表格。我们将使用 'work' 来显示单个 HTTP 请求的进度,'finished' 来显示已完成的 HTTP 请求的结果,并且为了好玩,还有一个 'path' 列,它将显示从种子 URL 到已完成 URL 的 URL 路径。

现在,让我们使用此布局并在代码中设置一些内容

#define LAYOUTFILE <GuiWebCrawler/GuiWebCrawler.lay>
#include <CtrlCore/lay.h>

struct WebCrawler : public WithCrawlerLayout<TopWindow> {
    WebCrawler();
};

WebCrawler 将是我们的主应用程序类。它前面的奇怪的 #include 会将设计的布局“导入”到代码中,即它定义了 WithCrawlerLayout 模板类,代表我们的布局。通过继承它,我们将 workfinishedpath ArrayCtrl 小部件添加为 WebCrawler 的成员变量。我们将在 WebCrawler 构造函数中完成设置。

WebCrawler::WebCrawler()
{
    CtrlLayout(*this, "WebCrawler");
    work.AddColumn("URL");
    work.AddColumn("Status");
    finished.AddColumn("Finished");
    finished.AddColumn("Response");
    finished.WhenCursor = [=] { ShowPath(); };    // when cursor is changed in finished, 
                                                  // show the path
    finished.WhenLeftDouble = [=] { OpenURL(finished); };
    path.AddColumn("Path");
    path.WhenLeftDouble = [=] { OpenURL(path); }; // double-click opens url in browser
    total = 0;
    Zoomable().Sizeable();
}

CtrlLayoutWithCrawlerLayout 方法,它将小部件放置在设计好的位置。其余代码设置列表的列,并将小部件上的一些用户操作连接到 WebCrawler 中的相应方法(我们稍后会添加这些方法)。

Data Model

现在,无聊的 GUI 部分已经完成,我们将专注于有趣的部分——webcrawler 代码。首先,我们需要一些结构来跟踪事物。

struct WebCrawler : public WithCrawlerLayout<TopWindow> {
    VectorMap<String, int> url;        // maps url to the index of source url
    BiVector<int>          todo;       // queue of url indices to process
    
    struct Work {                      // processing record
        HttpRequest http;              // request
        int         urli;              // url index
    };
    Array<Work>      http;             // work records
    int64            total;            // total bytes downloaded
    

VectorMap 是 U++ 的独特容器,可以认为是数组和映射的混合体。它提供基于索引的键和值访问,以及一种快速查找键索引的方法。我们将使用 url 来避免重复的 URL 请求(将 URL 作为键),并将“父” URL 的索引作为值,以便稍后显示从种子 URL 开始的路径。

接下来是我们待处理 URL 的队列。从 HTML 中提取 URL 时,我们会将它们放入 url VectorMap 中。这意味着每个 URL 在 url 中都有唯一的索引,所以我们只需要一个索引队列,todo

最后,我们需要一些缓冲区来保存我们的并发请求。处理记录 Work 简单地将 HttpRequest 与 URL 索引(仅用于知道我们正在尝试处理哪个 URL)结合起来。Array 是 U++ 的容器,能够存储没有复制形式的对象。

主循环

我们有了数据模型,让我们开始编写代码。先从简单的事情开始,询问用户种子 URL。

void WebCrawler::Run()
{   // query the seed url, then do the show
    String seed = "www.codeproject.com";            // predefined seed url
    if(!EditText(seed, "GuiWebSpider", "Seed URL")) // query the seed url
        return;
    todo.AddTail(0);                                // first url to process index is 0
    url.Add(seed, 0);                               // add to database

Seed 是第一个 URL,所以我们知道它将具有索引 0。我们只需将其添加到 urltodo。现在,真正的繁重工作开始了。

    Open();              // open the main window
    while(IsOpen()) {    // run until user closes the window
        ProcessEvents(); // process GUI events

我们将一直运行循环,直到用户关闭窗口。在此循环中,我们需要处理 GUI 事件。循环的其余部分将处理实际的事务。

        while(todo.GetCount() && http.GetCount() < 60) 
        { // we have something to do and have less than 60 active requests
            int i = todo.Head();                     // pop url index from the queue
            todo.DropHead();
            Work& w = http.Add();                    // create a new http request
            w.urli = i;                              // need to know source url index
            w.http.Url(url.GetKey(i))                // setup request url
                  .UserAgent("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:11.0) 
                  Gecko/20100101 Firefox/11.0")      // lie a little :)
                  .Timeout(0);                       // asynchronous mode
            work.Add(url.GetKey(i));                 // show processed URL in GUI
            work.HeaderTab(0).SetText
              (Format("URL (%d)", work.GetCount())); // update list header
        }

如果我们有待处理的 todo 并且并发请求少于 60 个,我们会添加一个新的并发请求。
接下来要做的就是推进所有活动的 HTTP 请求。HttpRequest 类通过 Do 方法来实现这一点。此方法在非阻塞模式下尝试推进连接请求。我们所要做的就是为所有活动请求调用此方法,然后读取状态。

然而,即使在“活动”模式下可以不等待实际套接字事件就做到这一点,一个行为良好的程序也应该先等待直到套接字可以写入或读取,以节省系统资源。U++ 提供了 SocketWaitEvent 类正是为此目的。

        SocketWaitEvent we; // we shall wait for something to happen to our request sockets
        for(int i = 0; i < http.GetCount(); i++)
            we.Add(http[i].http);
        we.Wait(10);        // wait at most 10ms (to keep GUI running)

这里唯一的问题是 SocketWaitEvent 只等待套接字,而我们还需要运行 GUI。我们通过将最大等待时间限制为 10 毫秒来解决此问题(我们知道此时至少会发生一个周期性定时器事件,该事件应该由 ProcessEvents 处理)。
解决了这个问题,我们就可以继续处理请求了。

        int i = 0;
        while(i < http.GetCount()) {                       // scan through active requests
            Work& w = http[i];
            w.http.Do();                                   // run request
            String u = url.GetKey(w.urli);                 // get the url from index
            int q = work.Find(u);                          // find line of url in GUI work list
            if(w.http.InProgress()) {                      // request still in progress
                if(q >= 0)
                    work.Set(q, 1, w.http.GetPhaseName()); // set GUI to inform user 
                                                           // about request phase
                i++;
            }
            else { // request finished
                String html = w.http.GetContent();         // read request content
                total += html.GetCount();      // just keep track about total content length
                finished.Add(u, w.http.IsError() ? String().Cat() << w.http.GetErrorDesc()
                                                 : String().Cat() << w.http.GetStatusCode()
                                                   << ' ' << w.http.GetReasonPhrase()
                                                   << " (" << html.GetCount() << " bytes)",
                             w.urli);          // GUI info about finished url status, 
                                               // with url index as last parameter
                finished.HeaderTab(0).SetText(Format("Finished (%d)", finished.GetCount()));
                finished.HeaderTab(1).SetText(Format("Response (%` KB)", total >> 10));
                if(w.http.IsSuccess()) {       // request ended OK
                    ExtractUrls(html, w.urli); // extact new urls
                    Title(AsString(url.GetCount()) + " URLs found"); // update window title
                }
                http.Remove(i);                // remove from active requests
                work.Remove(q);                // remove from GUI list of active requests
            }
        }

这个循环看起来很复杂,但大部分代码用于更新 GUI。HttpRequest 类有一个方便的 GetPhaseName 方法来描述请求中正在发生的事情。InProgress 在请求完成(成功或某种失败)之前一直为 true。如果请求成功,我们将使用 ExtractUrls 从 HTML 代码中获取新的 URL 进行测试。

获取新 URL

为了简单起见,ExtractUrls 的实现相当朴素,我们只是扫描“http://”或 “https://”字符串,然后读取下一个看起来像 url 的字符。

bool IsUrlChar(int c)
{// characters allowed
    return c == ':' || c == '.' || IsAlNum(c) || c == '_' || c == '%' || c == '/';
}

void WebCrawler::ExtractUrls(const String& html, int srci)
{// extract urls from html text and add new urls to database, srci is source url
    int q = 0;
    while(q < html.GetCount()) {
        int http = html.Find("http://", q); // .Find returns next position of pattern
        int https = html.Find("https://", q); // or -1 if not found
        q = min(http < 0 ? https : http, https < 0 ? http : https);
        if(q < 0) // not found
            return;
        int b = q;
        while(q < html.GetCount() && IsUrlChar(html[q]))
            q++;
        String u = html.Mid(b, q - b);
        if(url.Find(u) < 0) {             // do we know about this url?
            todo.AddTail(url.GetCount()); // add its (future) index to todo
            url.Add(u, srci);             // add it to main url database
        }
    }
}

我们将所有候选 URL 添加到 urltodo 中,供主循环处理。

最后润色

此时,所有繁重的工作都已完成。其余代码只是两个方便的函数,一个是在双击 finishedpath 列表时打开 URL。

void WebCrawler::OpenURL(ArrayCtrl& a)
{
    String u = a.GetKey(); // read url from GUI list
    WriteClipboardText(u); // put it to clipboard
    LaunchWebBrowser(u);   // launch web browser
}

(我们还额外将 URL 复制到剪贴板。)
另一个函数填充 path 列表,以显示从种子 URL 到 finished 列表中 URL 的路径。

void WebCrawler::ShowPath()
{   // shows the path from seed url to finished url
    path.Clear();
    if(!finished.IsCursor())
        return;
    int i = finished.Get(2);  // get the index of finished
    Vector<String> p;
    for(;;) {
        p.Add(url.GetKey(i)); // add url index to list
        if(i == 0)            // seed url added
            break;
        i = url[i];           // get parent url index
    }
    for(int i = p.GetCount() - 1; i >= 0; i--) // display in reverted order, with seed first
        path.Add(p[i]);
}

在这里,我们使用 VectorMap 的“双重性质”通过索引从子 URL 遍历回种子 URL。
现在唯一缺少的小块代码是 MAIN

GUI_APP_MAIN
{
    WebCrawler().Run();
}

好了,就是这样,一个带有 GUI 的简单并行 Web 爬虫,大约 150 行代码。

有用链接

历史

  • 2020年5月5日:初始版本
  • 2020 年 5 月 7 日:ExtractUrls 现在也扫描“https”
  • 2020 年 9 月 20 日:与“入门”文章进行交叉链接
© . All rights reserved.