做爬蟲的時候最頭疼的就是遇到一些動態加載的頁面或者是一些動態生成的鏈接。
比如我們的博客園就是個例子:
鳳凰網的評論鏈接也是一樣:
今天我們就用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的內容。
至此我們就完成了動態內容和鏈接的提取,通過適當的改造就可以變成自己定義的多功能爬蟲了。