サーバを持ちたくなくてネイティブアプリ側で外部サイトのHTMLをスクレイピング完結したい場合。
WKWebViewでページ取得するのが手取り早い。要はWKWebViewが読み込んだHTMLをスクレイピングしてしまおうという作戦。
ちなみに実験的に検証しただけなのでstoreの審査保証は全くできない。
WKWebViewで読み込んだページのHTMLを取得
import UIKit import WebKit class ViewController: UIViewController { var htmlBody = "" override func viewDidLoad() { super.viewDidLoad() let webView = WKWebView() webView.frame = CGRect(x: 0, y: 0, width: 0, height: 0) let url = URL(string: "https://example.jp") let urlRequest = URLRequest(url: url!) webView.load(urlRequest) webView.navigationDelegate = self //addSubviewしないとページロードしない view.addSubview(webView) } } extension ViewController:WKNavigationDelegate{ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { (html, error) -> Void in //htmlにロードしたHTMLが返される print(html) }) } }
要はWKWebViewでページロードしたHTML全体をevaluateJavaScript
で返すだけ。
もちろんWKWebViewのページロードが完了した契機で実行しないといけないので、didFinish delegate method内に記述することになる。
注意点はWKWebViewはaddSubviewしないとロード開始しない。なので対象をスクレイピングのみしたい場合は、
webView.frame = CGRect(x: 0, y: 0, width: 0, height: 0) view.addSubview(webView)
のような感じにしておけば良いかと。
あれ?WKWebViewって実際にaddSubView()しないと、ページロードしない?
— Fumiya Ichikawa (@LET__IT__RIDE) December 27, 2019
ちなみにJS からネイティブへの値の受け渡しは、こんなことでも少しハマってしまった。
Swift WKWebViewで
— Fumiya Ichikawa (@LET__IT__RIDE) December 27, 2019
webView.evaluateJavaScript("document.getElementsByClassName('classname'), completionHandler: { (html, error) -> Void in
})
でjs配列にアクセスする場合、htmlはnilになる
要素にアクセスするには
— Fumiya Ichikawa (@LET__IT__RIDE) December 27, 2019
webView.evaluateJavaScript("document.getElementsByClassName('classname')[0].innerHTML", completionHandler: { (html, error) -> Void in
})
な感じになる。
HTMLのスクレイピング
あとはロードしたHTMLをKannaとかでスクレイピングしちゃえばいいんだけど、ネイティブにロジックを持たせちゃうと対象ページの文書構造が変わった時にネイティブアップデートの必要が出てくる。
で、どうしたかと言うと差し替え可能なJS文字列として、スクレイピングロジックを記述しちゃえ。という作戦。
上記で示したevaluateJavaScriptメソッドはJSを文字列として実行できるので、そもそもがDOMアクセス可能なJSで楽にスクレイピングできる。
以下では例として、String型の変数let js
に、複数のclass product
の中に含まれるclass price
という金額が示す要素の一番引い値をネイティブ側に返すような文字列を定義している。
extension ViewController:WKNavigationDelegate{ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { let js = """ var boxes = document.getElementsByClassName('product') var minPrice = 0 for( i=0;i<boxes.length;i++ ){ var strPrice = boxes[i].getElementsByClassName('price')[0].innerText; var price = parseInt( strPrice.replace(/[,|¥]/g, '') ); if( minPrice == 0 || minPrice < price ){ minPrice = price } } minPrice; """ webView.evaluateJavaScript(js, completionHandler: { (html, error) -> Void in print(html) //self.htmlBody = html as! String }) } }
もうお分かりだと思うけど、文字列let js
をAPIなどでリモートから変更できる仕組みを作っておけば、対象HTMLの文書構造が変わってもアプリのアップデート自体はいらない。といった戦略を取ることが可能。
但し、外部からの変更可能ということはそれだけ、eval injectionの危険性があるということも把握しておく必要がある。
そして、Appleの審査でどこまで静的解析されるかは分からないけど、App Store Reviewガイドライン - Apple Developerのソフトウェア要件 2.5.2に引っかかったり、WKWebViewの使い方を指摘されたりするかもしれないね。
あくまで実験のアウトプットなので利用するしないは各々の判断で。ということで。
サンプルコード