使用U++的简单多请求Web爬网程序

本文使用的是U++框架。请参阅Ultimate++入门以获取有关环境的介绍。

U++框架提供了一个HttpRequest能够异步操作的类。在此示例中,我们将利用此功能使用多达60个并行HTTP连接来构造一个简单的单线程Web搜寻器。

设计GUI

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

首先,我们将为我们的应用程序设计一个简单的GUI布局。在这里,GUI相当简单,但是使用布局设计器仍然值得考虑:

布局包含3ArrayCtrl个小部件,它们基本上是表格。我们将使用工作来显示单个HTTP请求的进度,完成以显示已结束的HTTP请求的结果,并从中获得乐趣和路径,这将为完成的任何行显示从种子url到完成的url的url的“路径”。

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

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

struct WebCrawler : public WithCrawlerLayout<TopWindow> {
 WebCrawler();
};123456复制代码类型:[html]

WebCrawler将是我们应用程序的主要类别。#include在将设计的布局“导入”代码之前,这很奇怪,即它定义WithCrawlerLayout了代表我们布局的模板类。通过从中获取的,我们添加work,finished和pathArrayCtrl小部件为成员变量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();
}123456789101112131415复制代码类型:[html]

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

资料模型

现在,有了无聊的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 downloaded12345678910复制代码类型:[html]

VectorMap是一个独特的U++容器,可以将其视为数组和映射的组合。它提供了基于索引的键和值访问方式,以及一种快速找到键索引的方法。我们将使用url一种避免重复的url请求(将url放入密钥)的方法,并将'parent'url的索引作为值,以便以后可以显示种子url的路径。

接下来,我们要处理一系列的URL。从html提取网址时,我们会将其放入urlVectorMap。这意味着每个url在url中都有唯一的索引,因此我们只需要有索引队列todo。

最后,我们将需要一些缓冲区来保留并发请求。处理记录Work只需HttpRequest与url索引结合即可(只是知道我们要处理的url)。Array是U++容器,能够存储没有任何形式的副本的对象。

主循环

我们有数据模型,让我们开始编写代码。首先,让我们向用户询问种子网址:

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 database1234567复制代码类型:[html]

Seed是第一个网址,因此我们知道它将具有index0。我们将简单地将其添加到url和中todo。现在真正的工作开始了:

Open();  // open the main window
while(IsOpen()) { // run until user closes the window
 ProcessEvents(); // process GUI events123复制代码类型:[html]

我们将运行循环,直到用户关闭窗口。我们需要在此循环中处理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
}1234567891011121314复制代码类型:[html]

如果我们有东西todo并且少于60个并发请求,则添加一个新的并发请求。

下一步是处理所有活动的HTTP请求。HttpRequest类使用method来做到这一点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)1234复制代码类型:[html]

唯一的问题是SocketWaitEvent仅在套接字上等待,我们可以运行GUI。我们通过将最大等待限制指定为10ms来解决此问题(我们知道目前至少会发生周期性的计时器事件,应由处理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
 }
}12345678910111213141516171819202122232425262728293031复制代码类型:[html]

这个循环看起来很复杂,但是大多数代码都用于更新GUI。HttpRequest类具有方便的GetPhaseName方法来描述请求中发生的事情。InProgress是true直到请求完成(作为成功或某种故障)。如果请求成功,我们将使用ExtractUrls新的url从html代码进行测试。

获取新的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
  }
 }
}123456789101112131415161718192021222324复制代码类型:[html]

我们把所有候选的URLurl,并todo通过主循环处理。

最后的润色

至此,所有的辛苦工作已经完成。其余代码只是两个便捷功能,其中一个在双击finished或path列出时打开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
}123456复制代码类型:[html]

(我们也将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]);
}12345678910111213141516复制代码类型:[html]

在这里,我们使用“双重性质”VectorMap使用索引从子网址遍历回到种子。

现在缺少的唯一一小段代码是MAIN:

GUI_APP_MAIN
{
 WebCrawler().Run();
}1234复制代码类型:[html]

接下来,我们进行了大约150行的带有GUI的简单并行Web爬网程序。

(0)

相关推荐