使用MSHTML解析HTML页面
最近在写一个爬虫项目,本来打算用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 array
sfArray = SafeArrayCreateVector(VT_VARIANT, 0, 1);
if (sfArray == NULL || m_spDoc == NULL)
{
return;
}
hresult = SafeArrayAccessData(sfArray,(LPVOID*) ¶m);
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页面的更多相关文章
- Android开发探秘之三:利用jsoup解析HTML页面
这节主要是讲解jsoup解析HTML页面.由于在android开发过程中,不可避免的涉及到web页面的抓取,解析,展示等等,所以,在这里我主要展示下利用jsoup jar包来抓取cnbeta.com网 ...
- 前端优化:DNS预解析提升页面速度
在网页体验中我们常会遇到这种情况,即在调用百度联盟.谷歌联盟以及当前网页所在域名外的域名文件时会遇到请求延时非常严重的情况.那么有没有方法去解决这种请求严重延时的现象呢? 一般来说这种延时的原因不会是 ...
- python之urllib2简单解析HTML页面之篇一
一.urllib2简单获取html页面 #!/usr/bin/env python # -*- coding:utf-8 -*- import urllib2 response = urllib2.u ...
- Python—解析HTML页面(HTMLParser)
HTMLParser类的定义及常用方法 类的定义 HTMLParser主要是用来解析HTML文件(包括HTML中无效的标记). 参数convert_charrefs表示是否将所有的字符引用自动转化为U ...
- Python 通过lxml 解析html页面自动组合xpath实例
#coding:utf-8 ''' @author: li.liu ''' from selenium import webdriver from selenium.webdriver.common. ...
- Python爬虫 | re正则表达式解析html页面
正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,a 到 z 之间的字母)和特殊字符(称为"元字符"). 正则表达式通常被用来匹配.检索.替换和 ...
- Python爬虫 | Beautifulsoup解析html页面
引入 大多数情况下的需求,我们都会指定去使用聚焦爬虫,也就是爬取页面中指定部分的数据值,而不是整个页面的数据.因此,在聚焦爬虫中使用数据解析.所以,我们的数据爬取的流程为: 指定url 基于reque ...
- 使用jsoup解析html页面内容案例
public String getFaGuiKuTitles(String type, int page) { String href = "http://info.qd-n-tax.gov ...
- jsoup 解析html 页面数据
我html 页面元素: /html/body/table[2]/tbody/tr[1]/td/table/tbody/tr[1]/td[2]/font/html/body/table[2]/tbody ...
随机推荐
- 【蓝牙低功耗BLE】控制GPIO来点亮LED
这节讲一下最简单的,也是最基础的东西.CC2540的IO操作,把PORT口当做GPIO来用,废话不多说,往下看. 1.硬件电路 硬件电路时最简单的,用一根GPIO去控制LED灯.因为GPIO作为out ...
- hdu5304 Eastest Magical Day Seep Group's Summer 状压dp+生成树
题目:http://acm.hdu.edu.cn/showproblem.php?pid=5304 16个点的无向图,问能生成多少个n条边的连通图.(即多一条边的树) 先n^3 * 2^n 枚举全部的 ...
- AB串
题目: 给定n个A和2n个B.用这些字符拼成一个字符串.要求这个串的全部前缀和后缀B的个数始终不少于A. (一个字符串的前缀是仅仅从开头到某个位置为止的子串,后缀是仅仅从某个位置到结尾的子串). 输入 ...
- 爬取豆瓣电影储存到数据库MONGDB中以及反反爬虫
1.代码如下: doubanmoive.py # -*- coding: utf-8 -*- import scrapy from douban.items import DoubanItem cla ...
- 字符设备 Vs. 块设备 Character Device Vs. Block Device
字符设备是指驱动发送/接受单个字符(例如字节)的设备. 块设备是指驱动发送/接受整块数据(例如512个字节为一个块)的设备. 常见的字符设备:串口,并口,声卡. 常见的块设备:硬盘(最小读取单位为扇区 ...
- gunicorn Arbiter 源码解析
如前文所述,Arbiter是gunicorn master进程的核心.Arbiter主要负责管理worker进程,包括启动.监控.杀掉Worker进程:同时,Arbiter在某些信号发生的时候还可以热 ...
- 「mysql优化专题」单表查询优化的一些小总结,非索引设计(3)
单表查询优化:(关于索引,后面再开单章讲解) (0)可以先使用 EXPLAIN 关键字可以让你知道MySQL是如何处理你的SQL语句的.这可以帮我们分析是查询语句或是表结构的性能瓶颈. (1)写sql ...
- Spark术语
1.resilient distributed dataset (RDD) The core programming abstraction in Spark, consisting of a fau ...
- iOS js oc相互调用JavaScriptCore(一)
原址:http://blog.csdn.net/lwjok2007/article/details/47058101 1.普通调用 从iOS7开始 苹果公布了JavaScriptCore.framew ...
- iis 10 ftp 被动模式配置
第一步: 进入 Server Level 的FTP Firewall Support 第二步: 在 Data Channel Port Range 下配置 Passive mode 的端口号范围,注意 ...