实现方案
- 利用WKWebView打开一个待爬取的网页
- 在webView渲染完成之后注入一段爬虫脚本
- 在脚本回调里面获取爬取的数据
代码
以天猫的商品爬取为例
先打印网页内容
注入脚本document.body.innerHTML
- (void)viewDidLoad {
[super viewDidLoad];
self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 100.f, FULL_WIDTH, 200.f)];
self.webView.navigationDelegate = self;
[self.view addSubview:self.webView];
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://detail.tmall.com/item.htm?id=578502467835&ali_refid=a3_430406_1007:1121266184:N:1060515764_0_100:61033457550edeff91391950420fef46&ali_trackid=1_61033457550edeff91391950420fef46&spm=a21bo.2017.201874-sales.57"]]];
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
[self.webView evaluateJavaScript:@"document.body.innerHTML" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"抓取结果:%@", result);
}];
}
格式化之后
写脚本
商品图获取:
document.getElementsByClassName('item')[0].getElementsByTagName('img')[0].src
价格获取:
document.getElementsByClassName('real-price')[0].getElementsByClassName('price')[0].textContent
商品名获取:
document.getElementsByClassName('main')[0].textContent
组合成字典的形式返回(完整脚本)
(function() {
var init = function() {
return {
imgSrc: document.getElementsByClassName('item')[0].getElementsByTagName('img')[0].src,
price: document.getElementsByClassName('real-price')[0].getElementsByClassName('price')[0].textContent,
title: document.getElementsByClassName('main')[0].textContent
};
};
return init();
})()
注入新的脚本
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
[self.webView evaluateJavaScript:@"(function(){var init = function(){return {imgSrc:document.getElementsByClassName('item')[0].getElementsByTagName('img')[0].src,price:document.getElementsByClassName('real-price')[0].getElementsByClassName('price')[0].textContent,title:document.getElementsByClassName('main')[0].textContent};}; return init();})()" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"抓取结果:%@", result);
}];
}
注意点
(1) html的解析一定要以客户端返回的为准, 与浏览器打开看到的html是不一样的
(2) 脚本有问题的时候error会提示Error Domain=WKErrorDomain Code=4 "A JavaScript exception occurred" 根据提示修改脚本即可
(3) 服务端的脚本可以通过下面的方法转成string
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://xxxxx.js"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
2019.02.16更新
因为网页数据大多数是异步返回的, 在didFinishNavigation回调触发的时候, 页面上想被抓取的数据并没有返回
增加一个dom变更的监听, 利用一个debounce防止调用过于频繁
var timer = null;
var body = document.getElementsByTagName("body")[0];
body.addEventListener("DOMSubtreeModified", function(evt) {
clearTimeout(timer);
timer = setTimeout(function(){
spider();
}, 1000);
}, false);
这个时候只能通过js去调用oc
初始化的时候去创建一个webView的config
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
//注册方法名
[configuration.userContentController addScriptMessageHandler:self name:@"spider"];
self.webview = [WKWebView initWithFrame:frame configuration:configuration];
实现WKScriptMessageHandler协议
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"spider"])
{
//js的传过来的数据
NSLog(@"%@",message.body);
}
}
js脚本
var spider = function() {
...
//window.webkit.messageHandlers..postMessage()
window.webkit.messageHandlers.spider.postMessage(spiderData);
...
}