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






3.79/5 (12投票s)
2003年7月21日
4分钟阅读

162226
一篇关于如何使用 WINHTTP 库从网页 URL 提取数据以及如何提取抓取文档的 DOM 的文章。
引言
本文处理自动网页数据提取中的两个主要问题。
- 如何使用 WINHTTP 5 库进行抓取(读取网页数据)。
- 如何将从 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 提供的接口 IHTMLDocument
、IHTMLDocument2
、IHTMLDocument3
和 IHTMLDocument4
。以下代码从该缓冲区获取数据并从中创建一个 IHTMLDocument2
。然后,我们可以使用它的各种方法(getBody
、getInnerHTML
等)来访问 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 属性。使用我上面提供的代码可以轻松构建一个网站抓取器。您可以使用 IHTMLDocument2
的 get_anchors
方法来获取页面中的超链接,然后递归调用上面的代码,同时实现适当的链接循环检查。这样的程序可以抓取给定基础 URL 可访问的所有超链接页面,直到任意数量的级别。