首页 > 编程知识 正文

delphi 解析html(html解析过程)

时间:2023-05-03 15:18:13 阅读:75882 作者:4899

我最近在写爬行动物项目。 本来是想通过抄送来实现的。 我在网上找相关资料的时候,找到了微软的这个MSHTML库。 最后解析动态页面时,发现它太差了。 项目需要像浏览器一样运行脚本(如JavaScript )以形成静态HTML页,最后解析此静态页。 但是,MSHTML在运行JavaScript等脚本时需要与名为WebBroswer的ActiveX控件配合使用,这个控件还必须在GUI程序中使用,而我创建的这个功能最终是公司产品虽然最终没有采用这个方案,但是我在学习MSHTML开始写Demo的过程中受益匪浅,所以在这里记录下我的成果

解析Html页面MSHTML是典型的DOM类型解析库,基于COM组件,解析Html页面需要IHTMLDocument2类型的接口。 通过GUI程序可以很容易地获得这个接口,获得方法也很容易在网上找到。 本节主要介绍如何从HTML字符串生成相应的IHTMLDocument2接口。 关于如何生成该HTML字符串,可通过向web服务器发送http请求并获取其回复,而通过分析该回复分组来获取对应的HTML页面数据。

要获得这个接口,主要需要以下步骤。

使用创建实例创建接口。 IHTMLDocument2接口通常使用以下语句:

hresult HR=cocreate instance (clsid _ html document,NULL,CLSCTX_INPROC_SERVER,IID_IHTMLDocument2,) void** 2 .在com中创建数组,并将HTML字符串写入数组。 该数组主要用于VC和VB交互,使VB程序可以方便地使用COM接口。 使用此数组时,不需要关注特定成员。 VC提供了使用它的特定接口,初始化时只需调用以下内容:

a )名为SafeArrayCreateVector:的函数用于创建相应的数组结构。 函数有三个参数。 第一个参数表示数组中元素的类型,通常表示VT_VARIANT为自动类型。 第二个参数数组元素开始位置的下标通常为0,第三个参数是数组的维数,因为对于VC而言,数组元素始终从0开始。 这里简单地作为字符数组。 因此,这是一维数组。

b ) SafeArrayAccessData :允许用户操作此数组。 如果需要读写数组,则必须调用此函数以获取数组的操作权。 有两个参数。 第一个参数是数组变量,第二个参数是输出参数,如果成功调用此函数,则会提供缓冲区。 操作这个缓冲区就像操作数组一样。

c ) SafeArrayUnaccessData :每次操作数组完成时都必须调用此函数。 函数与SafeArrayAccessData结合使用,该函数可回收此权限并启用对数组的操作

3 .调用接口的write方法并将接口绑定到HTML字符串

现在,您可以使用此接口访问HTML中的元素。 详细代码如下所示。

ihtml文档2 * createihtmldocument2(conststringstrhtml ) { ihtml文档2 * m _ sp doc=null; hresult HR=cocreate instance (clsid _ html document,NULL,CLSCTX_INPROC_SERVER,IID_IHTMLDocument2,) void** HRESULT hresult=S_OK; 变量*参数; 安全阵列* Sf阵列; //createsanewone-dimensionalarraysfarray=safearraycreatevector (vt _ variant,0,1 ); if (SF array==null|| m _ sp doc==null ) { return; } hresult=safearrayaccessdata (SF array,) lpvoid * (param ); param-vt=VT_BSTR; param-bstrval=_ com _ util :3360 convertstringtobstr (strhtml.c _ str (); hresult=safearrayunaccessdata (SF array ); Hresult=m_spdoc-write(SFarray ); return m_spDoc; }遍历html元素MSHTML将元素的对应信息封装在IHTMLElement接口中,得到对应元素的接口后,可以使用其中的get系列方法获取其中的各种信息。 因为没有列举这些函数,所以根据需要看MSDN即可。

获得HTML文档的IID_IHTMLDocument2接口后,可以使用以下过程遍历元素:

r> 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

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。