在网络世界中,经常需要保存网页以便在没有网络的情况下查看。本文将介绍如何使用Visual C++创建一个离线浏览器,它可以下载网页及其所有资源,并将它们保存到本地硬盘上。将使用WinInet、URL Moniker和MSHTML等API来实现这一功能。
制作离线浏览器的动机源于一个需要记录用户与网页交互的模块。为了不使用浏览器的"另存为"选项,决定自己开发一个工具来保存网页到本地硬盘。以下是实现这一功能的简要算法描述:
1. 下载网页的HTML内容,例如www.google.com,并将其保存到指定的文件夹中。
2. 遍历HTML文档,查找每个标签的src属性,src属性的值是资源的URL。如果资源的URL是绝对的,例如www.google.com/images/logo.gif,那么没有问题。但如果URL是相对的,例如images/logo.gif,则需要使用主机名将其转换为绝对路径。
3. 更新src属性以反映资源URL的任何变化。相对URL将保持不变,但对于绝对地址,src属性现在将更改为相对路径。
4. 将原始src属性的值保存到srcdump中,仅供将来参考,以便原始src仍然可用。
在开发记录用户与网页交互的模块时,需要将网页保存到本地硬盘上。寻找了一些能够实现这一功能的代码,但没有找到有用的资料,因此决定自己开发。在这里分享这段代码,希望它能帮助其他从事相关工作的人,并希望得到一些反馈,指出可能犯的错误。没有使用MFC,以确保它与Win32应用程序以及MFC兼容。
以下是使用代码的示例:
LoadHtml()函数根据bDownload参数的值工作在两种模式下:
如果bDownload为true,则假定HTML已经使用SetHtml()函数加载,并且不会执行以下代码片段,只是从URL中填充主机名和端口字段。
如果bDownload为false,则首先从指定的URL下载HTML,然后填充主机名和端口字段。
HINTERNET hNet = InternetOpen("Offline Browser", INTERNET_OPEN_TYPE_PROXY, NULL, NULL, 0);
if (hNet == NULL) return;
HINTERNET hFile = InternetOpenUrl(hNet, sUrl.c_str(), NULL, 0, 0, 0);
if (hFile == NULL) return;
while (true) {
    const int MAX_BUFFER_SIZE = 65536;
    unsigned long nSize = 0;
    char szBuffer[MAX_BUFFER_SIZE+1];
    BOOL bRet = InternetReadFile(hFile, szBuffer, MAX_BUFFER_SIZE, &nSize);
    if (!bRet || nSize <= 0) break;
    szBuffer[nSize] = '\0';
    m_sHtml += szBuffer;
}
    
BrowseOffline()假设HTML已经加载。首先,它通过将HTML加载到MSHTML DOMDocument接口来构建HTML DOM树:
SAFEARRAY* psa = SafeArrayCreateVector(VT_VARIANT, 0, 1);
VARIANT *param;
bstr_t bsData = (LPCTSTR)m_sHtml.c_str();
hr = SafeArrayAccessData(psa, (LPVOID*)&m);
param->vt = VT_BSTR;
param->bstrVal = (BSTR)bsData;
hr = pDoc->write(psa);
hr = pDoc->close();
SafeArrayDestroy(psa);
    
一旦构建了DOM树,就可以遍历它并寻找需要下载的资源。目前,只寻找所有元素中的src属性,一旦找到src属性,就下载并保存到本地文件夹。
MSHTML::IHTMLElementCollectionPtr pCollection = pDoc->all;
for (long a = 0; a < pCollection->length; a++) {
    std::string sValue;
    IHTMLElementPtr pElem = pCollection->item(a);
    if (GetAttribute(pElem, L"src", sValue)) {
        if (!IsAbsolute(sValue)) {
            if (sValue[0] == '/') sValue = sValue.substr(1, sValue.length()-1);
            CreateDirectories(sValue, m_sDir);
            if (!DownloadResource(sValue, sValue)) {
                std::string sTemp = m_sScheme + m_sHost;
                sTemp += sValue;
                if (sTemp[0] == '/') sTemp = sTemp.substr(1, sTemp.length()-1);
                SetAttribute(pElem, L"src", sTemp);
                SetAttribute(pElem, L"srcdump", sValue);
            } else {
                SetAttribute(pElem, L"srcdump", sValue);
            }
        } else {
            std::string sTemp;
            sTemp = TrimHostName(sValue);
            CreateDirectories(sTemp, m_sDir);
            if (DownloadResource(sTemp, sTemp)) {
                if (sTemp[0] == '/') sTemp = sTemp.substr(1, sTemp.length()-1);
                SetAttribute(pElem, L"src", sTemp);
                SetAttribute(pElem, L"srcdump", sValue);
            }
        }
    }
}
    
由于src和srcdump属性的值发生了变化,原始HTML被更新并保存为[GUID].html,其中GUID是使用CoCreateGuid()生成的全局唯一标识符。
MSHTML::IHTMLDocument3Ptr pDoc3 = pDoc;
MSHTML::IHTMLElementPtr pDocElem;
pDoc3->get_documentElement(&pDocElem);
BSTR bstrHtml;
pDocElem->get_outerHTML(&bstrHtml);
std::string sNewHtml((const char*)OLE2T(bstrHtml));
SaveHtml(sNewHtml);
    
一旦有了资源的绝对URL,就可以直接下载并保存到适当的本地文件夹中。
if (URLDownloadToFile(NULL, sTemp.c_str(), sTemp2.c_str(), 0, NULL) == S_OK) return true;
else return false;
    
COfflineBrowser obj;
char szUrl[1024];
printf("Enter URL: ");
gets(szUrl);
obj.SetDir("c:\\MyTemp\\");
obj.LoadHtml(szUrl, true);
obj.BrowseOffline();