利用Webkit抓取动态网页和链接

做爬虫的时候最头疼的就是遇到一些动态加载的页面或者是一些动态生成的链接。

比如我们的博客园就是个例子:

凤凰网的评论链接也是一样:

今天我们就用Webkit来解决这个问题。

预备知识可以看一下我前面几篇文章,准备工作参照利用InjectedBundle定制自己的Webkit(二)中的客户端程序。

一切就绪之后我们开始!

首先介绍一些重要的函数和回调

在创建一个Page之后我们可以设置一些回调函数,其中有一个是:

WKPageLoaderClient::didFinishDocumentLoadForFrame

原型是:

typedef void (*WKPageDidFinishLoadForFrameCallback)(WKPageRef page, WKFrameRef frame, WKTypeRef userData, const void *clientInfo);

这个函数是在一个Frame加载完毕之后调用。由于每个Page都有一个mainFrame,而mianFrame又可能拥有若干个childFrame,只有在所有childFrame加载完毕之后mainFrame才算加载完毕,所以我们可以认为当mainFrame回调didFinishDocumentLoadForFrame的时候就是整个Page加载完毕的时候。换句话说这个时候所有的动态内容也都加载完毕了。我们可以在这个回调中获取我们需要的页面内容。

一个简单的例子:

WKContextRef context = WKContextGetSharedProcessContext();
RECT webViewRect = { 0, 0, 0, 0};
WKViewRef view = WKViewCreate(webViewRect, context, 0, m_window);
WKPageLoaderClient loaderClient = { 0 };
loaderClient.version = kWKPageLoaderClientCurrentVersion;
loaderClient.didFinishLoadForFrame = didFinishLoadForFrame;
WKPageSetPageLoaderClient(WKViewGetPage(view), &loaderClient);

大家都知道JavaScript功能强大,而Webkit给我们提供了运行自己的JS的接口,所以要提取我们想要的内容并非难事。调用方法如下:

WKStringRef script = WKStringCreateWithUTF8CString("1.1 + 1.5");
WKPageRunJavaScriptInMainFrame(page, script, 0, scriptResultCallback);

这是一个简单的例子运行1.1 + 1.5简单的浮点计算,这里面用到的page就是回调得到的WKPageRef,scriptResultCallback是执行完毕之后的回调。接下来编写回调函数处理执行结果:

void scriptResultCallback(WKSerializedScriptValueRef value, WKErrorRef, void* context)
{
  JSGlobalContextRef scriptContext = JSGlobalContextCreate(0);
  JSValueRef exc;
  JSValueRef var = WKSerializedScriptValueDeserialize(value, scriptContext, &exc);
  double dd = JSValueToNumber(scriptContext, var, &exc);
  wchar_t info[1024];
  swprintf(info, L"result is: %f", dd);
  ::MessageBox(NULL, info, L"Script", MB_OK);
  JSGlobalContextRelease(scriptContext);
}

运行结果如下:

既然能够得到正确的结果那我们开始解决第一个问题:提取动态内容

下面的例子是用JS把页面body部分的代码提取出来

var wholeHtmlString = '';    // 存放HTML

function myPrintTag(node)
{
  if (node.nodeName == '#text')  // 文本块直接打印内容
  {
    wholeHtmlString += node.textContent;
    wholeHtmlString += '\n';
    return 'text';
  }
  else if (node.nodeName == '#comment')  // 过滤注释
  {
    return 'comment';
  }
  else if (node.nodeName == 'SCRIPT')  // 过滤JS
  {
    return 'script';
  }

  wholeHtmlString += '<';
  wholeHtmlString += node.nodeName;
  wholeHtmlString += ' ';
  if (node.hasAttributes())
  {
    for (var i = 0; i < node.attributes.length; i++)  // 输出节点属性
    {
      var attr = node.attributes.item(i);
      wholeHtmlString += attr.name;
      wholeHtmlString += '=\'';
      wholeHtmlString += attr.value;
      wholeHtmlString += '\' ';
    }
  }
  wholeHtmlString += '>\n';
  return 'normal';
}

function myProcessNode(parent)
{
  var nodeType = myPrintTag(parent);  // 输出当前节点
  if (nodeType == 'normal')
  {
    if (parent.hasChildNodes())
    {
      for (var i = 0; i < parent.childNodes.length; i++)  // 输出孩子节点
      {
        myProcessNode(parent.childNodes.item(i));
      }
    }
    wholeHtmlString += '</';
    wholeHtmlString += parent.nodeName;
    wholeHtmlString += '>\n';
  }
}

function myPrintHtml()
{
  myProcessNode(document.body);  // 输出body部分
  return wholeHtmlString;
}

myPrintHtml();

要注意的地方是注释和文本节点转成HTML的时候需要特殊处理,利用这种方式可以轻松地自定义需要得到的部分。

接下来解决第二个问题:提取动态链接

我们浏览网页的时候要跳转到一个新的链接通常都是用鼠标点击一下即可,我们就可以模拟这一过程来提取出动态生成的链接。先看JS代码:

var clickEvt = document.createEvent('Event');
clickEvt.initEvent('click', true, true);
myObject.dispatchEvent(clickEvt);

myObject是我们想要获取链接的DOM节点,利用给目标DOM节点发送一个click消息就能够模拟鼠标点击事件,然后要做的就是捕获跳转的请求。

在页面将要导航到新的URL的时候,会调用一个回调:

WKPagePolicyClient::decidePolicyForNavigationAction

在将要创建一个新的页面的时候,会调用一个回调:

WKPagePolicyClient::decidePolicyForNewWindowAction

利用这两个回调,就能捕获到上文提到的跳转请求。下面看一个例子:

首先注册一下回调函数

WKPagePolicyClient policyClient = { 0 };
policyClient.version = kWKPagePolicyClientCurrentVersion;
policyClient.decidePolicyForNavigationAction = decidePolicyForNavigationAction; 
policyClient.decidePolicyForNewWindowAction = decidePolicyForNewWindowAction;
WKPageSetPagePolicyClient(page), &policyClient);

然后编写回调函数

void decidePolicyForNavigationAction(WKPageRef page, WKFrameRef frame, 
  WKFrameNavigationType navigationType, WKEventModifiers modifiers, WKEventMouseButton mouseButton, 
  WKURLRequestRef request, WKFramePolicyListenerRef listener, WKTypeRef userData, 
  const void* clientInfo)
{
  didRecvNewNavigation(frame, request, listener);
}

void decidePolicyForNewWindowAction(WKPageRef page, WKFrameRef frame, 
  WKFrameNavigationType navigationType, WKEventModifiers modifiers, WKEventMouseButton mouseButton, 
  WKURLRequestRef request, WKStringRef frameName, WKFramePolicyListenerRef listener, WKTypeRef userData, 
  const void* clientInfo)
{
  didRecvNewNavigation(frame, request, listener);
}

之后我们在didRecvNewNavigation中统一处理

void didRecvNewNavigation(WKFrameRef frame, WKURLRequestRef request, WKFramePolicyListenerRef listener)
{
  if (页面加载完毕)
  {
    WKURLRef url = WKURLRequestCopyURL(request);
    // 处理获得的url
    WKFramePolicyListenerIgnore(listener);  // 不加载该url
  }
  else
  {
    if (WKFrameIsMainFrame(frame))  // 如果是主frame
    {
      WKFramePolicyListenerUse(listener);  // 加载url
    }
    else
    {
      WKFramePolicyListenerIgnore(listener);  // 不加载该url
    }
  }
}

判断页面加载完成的时机之前已经说了,只要用一个状态变量记录即可。这里主要讲一下WKFramePolicyListenerRef,这个可以设置Webkit是否加载指定的URL,也就是可以过滤掉不需要的加载,提高效率。因为一般我们需要的内容都处于mainFrame中,所以这里只加载了mainFrame的内容。

至此我们就完成了动态内容和链接的提取,通过适当的改造就可以变成自己定义的多功能爬虫了。

 
分类: Webkit
标签: Webkit爬虫
原文地址:https://www.cnblogs.com/Leo_wl/p/2815023.html