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

通过爬行进行 Web 数据提取,使用 WINHTTP 和文档对象 (DOM) 实例化

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.79/5 (12投票s)

2003年7月21日

4分钟阅读

viewsIcon

162226

一篇关于如何使用 WINHTTP 库从网页 URL 提取数据以及如何提取抓取文档的 DOM 的文章。

引言

本文处理自动网页数据提取中的两个主要问题。

  1. 如何使用 WINHTTP 5 库进行抓取(读取网页数据)。
  2. 如何将从 WINHTTP 提取的数据提取/实例化出 DOM。

最后,我们讨论如何创建一个递归爬虫。

背景

WINHTTP 库符合基于持久(keep alive)协议模型的 HTTP 1.0/1.1 模型,这意味着我们首先连接到 Web 服务器,然后向其请求文档。对同一 Web 服务器(在本例中为主机名)的后续请求不涉及建立和断开连接。我们在这里讨论如何给定一个字符串 URL 来提取 HTML 数据。我遇到的主要问题是,抓取时你可能会得到一个很长的 URL。现在需要将其分解为主机名(用于连接)和 URL 路径的其余部分(用于请求)。当然,你会说 WINHTTPCrackUrl 方法可以完成这项工作,但它不行。它会给你正确的 URL 路径,但不是连接服务器的正确主机名。

对于 DOM 的操作,最广泛使用的接口系列是 IHTMLDocument,但这种类型的对象通常由 Browser 对象(使用 get_document 方法)实例化和填充。这里的问题是如何用从 WINHTTP 获取的纯文本 HTML 填充此对象。

这两个步骤为构建一个能够抓取网络并操作网页 DOM 模型而非进行纯字符串后处理(大多数工具都这样做)的工具奠定了基础。通过调用 IE 上的 Navigate 方法并分析 DOM 可以获得类似的壮举,但预估加载整个文档(包括图像)并在获取 DOM 之前在浏览器中渲染它会有多大的效率低下是微不足道的。

为什么选择 WINHTP 而不是 WinInet

传统开发者会说,既然有 WinInet,为什么还要使用 WinHTTP,而且微软也大力推广它用于 HTTP(以及 ftp 和 Gopher)客户端应用程序。但是 WinInet 对完全自动化造成了很大的阻碍。当通过 WinInet 进行任何身份验证和其他操作时,它会显示用户界面。然而,WinHTTP 以编程方式处理这些操作。

程序的外观

例如,如果待遍历的 URL 是 http://news.yahoo.com/fc?tmpl=fc&cid=34&in=world&cat=iraq,那么 WINHTTP 需要连接到 news.yahoo.com(这是 Web 服务器的主机名),然后发出对 /fc?tmpl=fc&cid=34&in=world&cat=iraq 的请求。

下面将详细介绍如何获取 URL 并进行拆分(使用 WinHttpCrackUrl),之后进行更改,因为拆分并不能给我们想要的结果,然后将这些数据馈送给 WINHTTP 调用。完成这些之后,就轮到从 URL 中提取数据了,为此,我们首先连接到 URL,并使用 WinHttpQueryDataAvailable 获取该 URL 上可用数据的大小。诀窍在于,我们无法一次性获取网页的所有数据,因此我们初始化一个缓冲区,我们将不断地将从 WinHttpReadData 获取的数据追加到该缓冲区,并在读取完所有数据后(由可用数据大小等于零指示)获取网页。这正是 Java 中等效的 URLReader 类的工作方式。下面是使用明确注释的每一步完成此壮举的完整代码。

USES_CONVERSION;

    // First, split up the URL
    URL_COMPONENTS urlComp;    // a structure that would contain the
                               // individual components of the URL
    LPCWSTR varURL;            // ***** varURL is the URL to be
                               // traversed
    DWORD dwUrlLen = 0;
    LPCWSTR hostname, optional;

    // Initialize the URL_COMPONENTS structure.
    ZeroMemory(&urlComp, sizeof(urlComp));
    urlComp.dwStructSize = sizeof(urlComp);

    //MessageBox(NULL,OLE2T(varURL),"the url to be traversed", 1);

    // Set required component lengths to non-zero so that they
    // are cracked.
    urlComp.dwSchemeLength    = -1;
    urlComp.dwHostNameLength  = -1;
    urlComp.dwUrlPathLength   = -1;
    urlComp.dwExtraInfoLength = -1;

    // Split the URL (varURL) into hostname and URL path
    if (!WinHttpCrackUrl( varURL, wcslen(pwszUrl1), 0, &urlComp))
    {
        printf("Error %u in WinHttpCrackUrl.\n", GetLastError());
    }
    
    // You can inspect the cracked URL here
    // For our example of varURL =
    // http://news.yahoo.com/fc?tmpl=fc&cid=34&in=world&cat=iraq
    // MessageBox(NULL,W2T(urlComp.lpszHostName),
    //            "INTERPRETER-> hostname",MB_OK);
    // We get the hostname as
    // "news.yahoo.com/fc?tmpl=fc&cid=34&in=world&cat=iraq"
    //  MessageBox(NULL,W2T(urlComp.lpszUrlPath),
    //             "INTERPRETER-> urlpath",MB_OK);
    // We get the urlPath as "/fc?tmpl=fc&cid=34&in=world&cat=iraq"
    // MessageBox(NULL,W2T(urlComp.lpszExtraInfo),
    //            "INTERPRETER->extrainfo",MB_OK);
    // We get the extrainfo as ""
    // MessageBox(NULL,W2T(urlComp.lpszScheme),
    //            "INTERPRETER->Scheme",MB_OK);
    // We get the scheme as
    // "http://news.yahoo.com/fc?tmpl=fc&cid=34&in=world&cat=iraq"
    
    // Compute the correct hostname
    String myhostname(W2T(urlComp.lpszHostName));
    String myurlpath(W2T(urlComp.lpszUrlPath));
    int strindex = myhostname.IndexOf(myurlpath);
    String newhostname(myhostname.SubString(0,strindex));

    strindex = 0;


    DWORD dwSize        = 0;
    DWORD dwDownloaded  = 0;
    LPSTR pszOutBuffer;
    BOOL  bResults      = FALSE;
    HINTERNET  hSession = NULL,
               hConnect = NULL,
               hRequest = NULL;

    // Use WinHttpOpen to obtain a session handle.
    hSession = WinHttpOpen( L"WinHTTP Example/1.0",
                            WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                            WINHTTP_NO_PROXY_NAME,
                            WINHTTP_NO_PROXY_BYPASS, 0);

    // Specify an HTTP server.
    // In our examples, it expects just "news.yahoo.com"
    if (hSession)
        hConnect = WinHttpConnect( hSession, T2W(newhostname),
                                   INTERNET_DEFAULT_HTTP_PORT, 0);

    // Create an HTTP request handle.
    // In our example, it expects
    // "/fc?tmpl=fc&cid=34&in=world&cat=iraq"
    if (hConnect)
        hRequest = WinHttpOpenRequest( hConnect, L"GET",
                                       urlComp.lpszUrlPath,
                                       NULL, WINHTTP_NO_REFERER,
                                       WINHTTP_DEFAULT_ACCEPT_TYPES,
                                       WINHTTP_FLAG_REFRESH);
    // Send a request.
    if (hRequest)
        bResults = WinHttpSendRequest( hRequest,
                                       WINHTTP_NO_ADDITIONAL_
                                              HEADERS, 0,
                                       WINHTTP_NO_REQUEST_DATA, 0,
                                              0, 0);

    // End the request.
    if (bResults)
        bResults = WinHttpReceiveResponse( hRequest, NULL);
        String respage="";    // The buffer that'll contain the
                              // extracted Web page data

    // Keep checking for data until there is nothing left.
    if (bResults)
        do
        {

            // Check for available data.
            dwSize = 0;
            if (!WinHttpQueryDataAvailable( hRequest, &dwSize))
                printf("Error %u in WinHttpQueryDataAvailable.\n",
                        GetLastError());

            // Allocate space for the buffer.
            pszOutBuffer = new char[dwSize+1];
            if (!pszOutBuffer)
            {
                printf("Out of memory\n");
                dwSize=0;
            }
            else
            {
                // Read the Data.
                ZeroMemory(pszOutBuffer, dwSize+1);

                if (!WinHttpReadData( hRequest,
                                      (LPVOID)pszOutBuffer,
                                      dwSize, &dwDownloaded))
                    printf("Error %u in WinHttpReadData.\n",
                            GetLastError());
                else
                    respage.Append(pszOutBuffer);

                // Free the memory allocated to the buffer.
                delete [] pszOutBuffer;
            }

        } while (dwSize>0);
        // MessageBox(NULL,respage,"fetched page from
        // crawler",1);

完成此操作后,我们将 HTML 页面作为字符串存储在 respage 缓冲区中。因此,现在的目标是获得它的 DOM 模型,以便我们可以以编程方式操作数据,例如查询节点、访问特定元素等。进行 DOM 操作的最佳方法是通过 Microsoft 提供的接口 IHTMLDocumentIHTMLDocument2IHTMLDocument3IHTMLDocument4。以下代码从该缓冲区获取数据并从中创建一个 IHTMLDocument2。然后,我们可以使用它的各种方法(getBodygetInnerHTML 等)来访问 DOM,或者将其类型转换为相关的接口,如 IHTMLDocument3,并查询 DOM 树中的节点。

        // Declare an IHTMLDocument structure
        IHTMLDocument2Ptr myDocument; // Declared earlier in the code
        HRESULT hr = CoCreateInstance(CLSID_HTMLDocument,NULL,
          CLSCTX_INPROC_SERVER,IID_IHTMLDocument2, (void **)&myDocument);
        HRESULT hresult = S_OK;
        VARIANT *param;
        SAFEARRAY *tmpArray;
        
        // Creates a new one-dimensional array
        // for holding the webpage data 
        tmpArray = SafeArrayCreateVector(VT_VARIANT, 0, 1);
        // Convert the buffer into binary string
        bstr_t bsData = (LPCTSTR) respage;
        hresult = SafeArrayAccessData(sfArray,(LPVOID*) & param);
        param->vt = VT_BSTR;
        param->bstrVal = bsData;
        hresult = myDocument->write(tmpArray); 
               // injected code in document structure
        hresult = SafeArrayUnaccessData(tmpArray);
        SysFreeString(bsData);
        if (tmpArray != NULL) {
            SafeArrayDestroy(tmpArray);
        }    

进一步增强

我在这里强调了抓取的基础知识,完整的爬虫设计由用户自行决定。对于一个完整的爬虫,我们需要从特定网页中提取链接,并从这些链接中提取数据。传统工具进行字符串处理以查找锚点或 href 标签并提取超链接字符串,这显然是一种效率低下的方法,因为我们需要解析所有页面数据。为此目的查询 DOM 要高效得多;我们可以直接查找所有带有锚点标签的节点并提取 href 属性。使用我上面提供的代码可以轻松构建一个网站抓取器。您可以使用 IHTMLDocument2get_anchors 方法来获取页面中的超链接,然后递归调用上面的代码,同时实现适当的链接循环检查。这样的程序可以抓取给定基础 URL 可访问的所有超链接页面,直到任意数量的级别。

© . All rights reserved.