Swift WKWebView、JSを利用したネイティブアプリ内で完結させるスクレイピング

個人開発したアプリの宣伝
目的地が設定できる手帳のような使い心地のTODOアプリを公開しています。
Todo with Location

Todo with Location

  • Yoshiko Ichikawa
  • Productivity
  • Free

サーバを持ちたくなくてネイティブアプリ側で外部サイトの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)

のような感じにしておけば良いかと。


ちなみにJS からネイティブへの値の受け渡しは、こんなことでも少しハマってしまった。

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の使い方を指摘されたりするかもしれないね。

あくまで実験のアウトプットなので利用するしないは各々の判断で。ということで。

サンプルコード

github.com