最近在写一个爬虫项目,本来打算用C/C++来实现,在网上查找有关资料的时候发现了微软的这个MSHTML库,最后发现在解析动态页面的时候它的表现实在是太差:在项目中需要像浏览器那样,执行JavaScript等脚本然后形成静态的HTML页面,最后才分析这个静态页面。但是MSHTML在执行JavaScript等脚本时需要配合WebBroswer这个ActiveX控件,这个控件又必须在GUI程序中使用,但是我做的这个功能最终是嵌入到公司产品中发布,不可能为它专门生成一个GUI页面,所以这个方案就作废了。虽然最终没有采用这个方案,但是我在开始学习MSHTML并写Demo的过程中还是收益匪浅,所以在这记录下我的成果

解析Html页面

MSHTML是一个典型的DOM类型的解析库,它基于COM组件,在解析Html页面时需要一个IHTMLDocument2类型的接口。在GUI程序中很容易就获取这个接口,获取它的方法很容易就可以在网上找到,在这主要说一下如何通过一段HTML字符串来生成对应的IHTMLDocument2接口。至于如何生成这个HTML字符串,我们可以通过向web服务器发送http请求,并获取它的返回,解析这个返回的数据包即可获取到对应的HTML页面数据。
获取这个接口主要需要经过下面的几个步骤:
1. 使用CoCreateInstance创建一个接口,对于IHTMLDocument2接口一般是使用下面的语句:

HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER,IID_IHTMLDocument2, (void**)&m_spDoc);

2.创建一个COM中的数组,将HTML字符串写到数组中。这个数组主要用来进行VC与VB的交互,以便VB程序能够很方便的使用COM接口。在使用这个数组时不需要关注它的具体成员,VC提供了具体的接口来使用它,在初始化它的时候只需要调用下面几个:
a)SafeArrayCreateVector:这个函数用来创建一个对应的数组结构。函数有三个参数,第一个参数表示数组中元素类型,一般给VT_VARIANT表示它是一个自动类型,第二个参数数组元素起始位置的下标,对于VC来说,数组元素总是从0开始,所以这个位置一般给0,第三个参数是数组的维数,在这我们只是简单的将它作为一个字符数组,所以它是一个一维数组。
b)SafeArrayAccessData:允许用户操作这个数组,在需要读写这个数组时都需要调用这个函数,以便获取这个数组的操作权。它有两个参数,第一个参数是数组变量,第二个参数是一个输出参数,当调用这个函数成功,会提供一个缓冲区,我们操作这个缓冲区就相当于操作了这个数组。
c)SafeArrayUnaccessData:每当操作数组完成时需要调用这个函数,函数与SafeArrayAccessData配套使用,这个函数用来回收这个权限,并使我们对数组的操作生效
3. 调用接口的write方法,将接口与HTML字符串绑定
经过这样几步就可以利用这个接口来访问HTML中的元素了,下面是它的详细代码:

IHTMLDocument2* CreateIHTMLDocument2(const string &strHtml)
{IHTMLDocument2 *m_spDoc = NULL;HRESULT hr = CoCreateInstance(CLSID_HTMLDocument, NULL, CLSCTX_INPROC_SERVER,IID_IHTMLDocument2, (void**)&m_spDoc);HRESULT hresult = S_OK;VARIANT *param;SAFEARRAY *sfArray;// Creates a new one-dimensional arraysfArray = SafeArrayCreateVector(VT_VARIANT, 0, 1);if (sfArray == NULL || m_spDoc == NULL){return;}hresult = SafeArrayAccessData(sfArray,(LPVOID*) &param);param->vt = VT_BSTR;param->bstrVal = _com_util::ConvertStringToBSTR(strHtml.c_str());hresult = SafeArrayUnaccessData(sfArray);hresult = m_spDoc->write(sfArray);return m_spDoc;
}

HTML元素的遍历

MSHTML中,将元素的对应信息封装为IHTMLElement接口,得到对应元素的接口后可以使用它里面的get系列方法来获取它里面的各种信息,这些函数我没有一一列举,当需要时看看MSDN即可。
当获取到了HTML文档的IID_IHTMLDocument2接口时,可以使用下面的步骤进行元素的遍历:
1. 接口的get_all方法获取所有的标签节点。这个函数通过一个输出参数输出IHTMLElementCollection类型的接口指针
2. 然后通过IHTMLElementCollection接口的get_length方法获取标签的总数量,根据这个数量写一个循环,在循环进行元素的遍历
3. 在循环中使用IHTMLElementCollection接口的item方法进行迭代,依次获取各个元素对应的IDispatch接口指针
4. 调用IDispatch接口指针的QueryInterface方法生成对应的IHTMLElement接口。通过这个接口获取元素的各中信息。
它对应的代码如下:

void EnumElements(IHTMLDocument2* m_spDoc)
{CComPtr<IHTMLElementCollection> pCollec;m_spDoc->get_all(&pCollec);if (NULL == pCollec){return ;}VARIANT varName;long len = 0;pCollec->get_length(&len);for (int i = 0; i < len; i++){varName.vt = VT_I4;varName.llVal = i;CComPtr<IHTMLElement> pElement;CComPtr<IDispatch> pDisp;pCollec->item(varName, varName, &pDisp);if (NULL == pDisp){continue;}pDisp->QueryInterface(IID_IHTMLElement, (LPVOID*)&pElement);if (NULL != pElement){BSTR bstrTag;pElement->get_tagName(&bstrTag);string strTag = _com_util::ConvertBSTRToString(bstrTag);cout<<strTag.c_str()<<endl;}}
}

这个方法不能很好的体现各个元素的层次结构,它可以遍历所有的元素,但是默认将元素都作为同一层来表示,如果需要得到对应的子节点,可以调用get_children方法,它可以获取下面的所有子节点,使用方法与get_all类似

调用JavaScript方法

在这,调用JavaScript函数只能想调用普通的函数一样,根据函数名,给它参数,并获取返回值,但是不能得到它执行到中间的某个步骤,比如说这样一个函数

function add(a, b){window.location.href = "https://www.baidu.com";return a + b
}

调用这个函数,只能得到a + b的值,但是并不知道它会跳转到另一个页面,在编写爬虫时如果存在这样的跳转或者通过某条语句生成了一个链接,那么使用后面说的方法是获取不到的
言归正传,下面来说下如何实现调用JavaScript。
调用JavaScript方法一般是使用IDispatch接口中的Invoke方法,但是使用这个略显麻烦,我在网上找到了更简单的方法,就是使用CComDispatchDriver接口中的Invoke方法,这个接口中主要有Invoke0、Invoke1、Invoke2、InvokeN几个用于调用JavaScript函数的方法,分别表示传入0个参数、1个参数、2个参数、任意个参数。
一般使用如下步骤来调用:
1.调用IID_IHTMLDocument2的get_Script方法,获取CComDispatchDriver接口
2. 调用CComDispatchDriver接口的GetIDOfName,传入JavaScript函数名称,获取JS函数对应的元素接口,这个函数会通过一个输出参数输出一个DISPID类型的变量。这个主要是一个ID,用来唯一标识一个js函数
3. 调用CComDispatchDriver接口的invoke函数,传入对应的参数,并调用js函数。下面是一个例子代码:

bool CallJScript(IID_IHTMLDocument2* m_spDoc, const CString strFunc, CComVariant* paramArray,int nArgCnt,CComVariant* pVarResult)
{CComDispatchDriver spScript;GetJScript(spScript);if (NULL == spScript){return false;}DISPID pispid;BSTR bstrText = _com_util::ConvertStringToBSTR(strFunc);spScript.GetIDOfName(bstrText, &pispid);HRESULT hr = spScript.InvokeN(pispid, paramArray, nArgCnt, pVarResult);if(FAILED(hr)){ShowError(GetSystemErrorMessage(hr));return false;}return true;
}

在调用的时候需要组织一个CComVariant类型的数组,并提供一个数组元素个数作为参数。而对于Invoke0这样有确定函数参数的情况则要简单的多。

获取js函数返回值

js返回参数最终会被包装成一个VARIANT结构,在COM中为了方便操作这个结构,封装了一个CComVariant类。在操作返回值时就是围绕着CComVariant类来进行

返回确定值

当它返回一个确定值时很好解决,由于事先知道返回值得类型,只需要调用结构体的不同成员即可

CComVariant varResult;
parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult);
cout<<varResult.lVal<<endl;

当它返回一个数组时,一般需要经过这样几步的处理:
1. 创建一个CComDispatchDriver,并将返回值得pdispVal赋值给它
2. 调用CComDispatchDriver接口的GetPropertyByName方法,将它的第一个参数传入”length”字符串,让其返回数组元素的个数
3. 在循环中调用GetPropertyByName方法,传入索引,获取对应索引位置的CComVariant值。

CComVariant varResult;
parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult);CComVariant varArrayLen;
CComDispatchDriver spDisp = varResult.pdispVal;
spDisp.GetPropertyByName(L"length", &varArrayLen);
for (int i = 0; i < varArrayLen.intVal; i++)
{CComVariant varValue;CStringW csIndex;csIndex.Format(L"%d", i);spDisp.GetPropertyByName(csIndex, &varValue);cout<<varValue.intVal<<endl;
}

返回一个object对象

js的object对象中可以有不同的属性,不同的属性对应不同的值,类似于一个字典结构,当返回这个类型,并且我们知道这个对象中的相关属性名称的时候可以通过下面的方法来获取各个属性中的值:
1. 创建一个CComDispatchDriver,并将返回值得pdispVal赋值给它
2. 调用CComDispatchDriver接口的GetPropertyByName方法,将它的第一个参数传入对应属性名称的字符串,让其返回属性的值

//在这假设JavaScript方法返回一个object对象,其中有两个属性,str属性中保存字符串,value属性保存一个整型数据
CComVariant varResult;
parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult);CComVariant varValue;
CComDispatchDriver spDisp = varResult.pdispVal;
spDisp.GetPropertyByName(L"result", &varValue);
cout<<"result:"<<varValue.intVal<<endl;
spDisp.GetPropertyByName(L"str", &varValue);
string strValue = _com_util::ConvertBSTRToString(varValue.bstrVal);
cout<<"str:"<<strValue.c_str()<<endl;

返回类型不确定的object对象

上面这种情况只有当JavaScript代码由自己编写或者与他人进行过相关的约定的时候才可能非常清楚js函数中将会返回何种类型的值,但是大多数情况下,是不知道将会返回何种数据,比如像我们在编写爬虫的时候。这种情况下一般使用IDispatchEx接口来枚举返回对象中的属性名称然后再根据上面的方法来获取属性的值

CComVariant varResult;
parse.CallJScript("Add", CComVariant(1), CComVariant(2), &varResult);
CComQIPtr<IDispatchEx> pDispEx = varResult.pdispVal;
CComDispatchDriver spDisp = varResult.pdispVal;
DISPID dispid;
HRESULT hr = pDispEx->GetNextDispID(fdexEnumAll, DISPID_STARTENUM, &dispid);
//枚举返回对象中所有属性对应的值
while (hr == NOERROR)
{BSTR bstrName;pDispEx->GetMemberName(dispid, &bstrName);if (NULL != bstrName){DISPPARAMS params;CComVariant varVaule;cout<<_com_util::ConvertBSTRToString(bstrName)<<endl;spDisp.GetPropertyByName(bstrName, &varVaule);SysFreeString(bstrName);}hr = pDispEx->GetNextDispID(fdexEnumAll, dispid, &dispid);
}

这些差不多就是我当初学会的一些东西,当初在利用这个方案实现爬虫的时候还是有许多坑,也看到了它的许多局限性,以至于我最终放弃了它,采用其他的解决方案。目前在使用的时候的我发现这样几个问题:
1. 在调用js时,如果不知道函数的名称,目前为止没有方法可以调用,这样就需要我们在HTML中使用正则表达式等方法进行提取,但是在HTML中调用js的方法实在太多,而有的只有一个函数,并没有调用,这些情况给工作带来了很大的挑战
2. MSHTML提供的功能主要是用来与IE进行交互,以便很容易实现一个类似于IE的浏览器或者与IE进行交互,但是如果要在控制台下进行相关功能的编写,则显的力不从心
3. 在控制台下它没有提供一个很好的方式来进行HTML页面的渲染。
4. 在于js进行交互的时候,只能简单的获取到一个VARIANT结构,这个结构可以表示所有常见的类型,但是在很多情况下,我们并不知道它具体代表哪个类型
最后放上demo的下载地址:http://download.csdn.net/detail/lanuage/9857075

使用MSHTML解析HTML页面相关推荐

  1. Android开发探秘之三:利用jsoup解析HTML页面

    这节主要是讲解jsoup解析HTML页面.由于在android开发过程中,不可避免的涉及到web页面的抓取,解析,展示等等,所以,在这里我主要展示下利用jsoup jar包来抓取cnbeta.com网 ...

  2. Dns-prefetch DNS 预解析优化页面加载速度

    Dns-prefetch DNS 预解析优化页面加载速度 浏览器访问一个链接时并不是直接将请求到网页对应的服务器上,而是先要做域名解析--将域名解析到网页对应的服务器 ip 地址,然后浏览器才能和服务 ...

  3. Android开发系列十:使用Jsoup解析HTML页面

    在写Android程序时,有时需要解析HTML页面,特别是那类通过爬网站抓取数据的应用,比如:天气预报等应用.如果是桌面应用可以使用htmlparser这个强大的工具,但是在Android平台上使用会 ...

  4. 使用lxml+xpath解析html页面

    @待解析的页面 <!DOCTYPE html> <html lang="en"> <title>Title</title> < ...

  5. 纯前端JS实现文件上传解析渲染页面

    AI真的能代替前端吗? 回答:不会完全代替 能用吗?复制到项目中只会报错 爆红 --他完全不能理解你需要什么 JavaScript(简称JS)是一种轻量级的脚本语言,主要用于在Web页面上添加交互行为 ...

  6. oracle oaf结构,解析OAF页面元数据结构((转自Oracle 探索者)

    在 Oracle E-Business Suite的二次开发中,基于OAF的开发在JDeveloper OA Extension中进行,完成后使用XMLImporter工具导入页面定义到数据库中, 之 ...

  7. VC++如何使用微软提供的Mshtml库解析html页面元素

    1.创建Win32或MFC工程. 2.在预编译或需要使用MSHTML命名空间的头文件中添加以下语句: #include <atlbase.h>     #include <Mshtm ...

  8. html中加载解析,HTML页面加载和解析流程详细介绍

    序言: 我一直都认为"网页制作"这个词是一个不怎么高端的词,在我的印象中网页制作的词是没有生命力的一个制作,我喜欢用HTML 这样简单直接,这词凸显高端,有大气漂亮的UI.一套完美 ...

  9. beautifulsoup解析动态页面div未展开_两个资讯爬虫解析库的用法与对比

    " 阅读本文大概需要 10 分钟. " 舆情爬虫是网络爬虫一个比较重要的分支,舆情爬虫往往需要爬虫工程师爬取几百几千个新闻站点.比如一个新闻页面我们需要爬取其标题.正文.时间.作者 ...

  10. Python爬虫 教程: re正则表达式解析html页面

    正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为"元字符"). 正则表达式通常被用来匹配.检索.替换和 ...

最新文章

  1. golang 接口格式
  2. UA MATH567 高维统计II 随机向量10 Grothendieck不等式的证明 版本二:kernel trick
  3. h3csyslog_H3C Syslog简单配置
  4. 迭代终止准则的三种形式_一种经验模态分解筛选迭代过程终止准则的方法与流程...
  5. 郑州大学linux试题,郑州大学Linux讲义 PPT
  6. Spring Cloud Feign 1(声明式服务调用Feign 简介)
  7. 怎样将一个Long类型的数据转换成字节数组
  8. 获取listview当前滚动的高度
  9. windows mysql 和linux mysql解决乱码问题
  10. icem网格数和节点数_icem如何查看网格数量
  11. 免费PDF转换器注册码
  12. 移动设备支持方式-移动设备管理MDM
  13. Windows上的安全模式
  14. Codeforces1102F Elongated Matrix 【状压DP】
  15. matlab 变量上小尖尖,发动机最中间的那个小尖尖,你猜是什么?
  16. 解决 LaTeX 中的中文显示问题
  17. OpenCV——图像特征提取(颜色:HSV与形状)
  18. 电信诈骗为何如此难以根治?
  19. jemeter实现IP欺骗-性能测试必备
  20. 汇编语言中b和bl关键字的区别

热门文章

  1. 数据库MySQL学习教程(带你零基础入门MySQL)
  2. 《python学习手册》目录
  3. java数据结构银行叫号,数据结构实验二——队列(银行叫号系统)
  4. 【脑洞探究】等公交该站在哪儿比较合适?——关于减少吸入空气污染物(pm2.5 or 雾霾等)而选择合适等候公交车位置的探究
  5. 极路由HC5661a刷潘多拉固件后配置python环境运行脚本登陆dr.com校园网
  6. idea使用数据库连接工具
  7. 与孩子一起学编程09章
  8. (附源码)计算机毕业设计ssm大众点评管理系统
  9. jsp项目如何定位当前页面是哪个jsp
  10. java odbc 驱动_Java java.sql.SQLException: [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序...