Webページ内のリンク画像を取得する

はじめに

まんがのしおりというアプリをリリースしました。

漫画を読むのが好きで日々たくさんの漫画を読んでいるのですが、物理本であれば「しおり」を挟めるので読んだ箇所がわからなくなることはありませんが、Web上で漫画を読んでいる際にどこまで読んでいたのかや、どの漫画を読んでいたのかわからなくなってしまい困っていました。

そこで上記のアプリをリリースするに至ったのですが、ここで問題なのは画像でした。

大量に漫画を読んでいるとタイトルではピントこないことが多々あり、絵柄やキャラクターをみて思い出すケースがあると思うのですが、画像をユーザに全て登録してもらうのはUX的に良くないですし画像を探すのは大変です、そこでリンクから画像をスクレイピングして取得すれば良いという結論に至りました💡

htmlを取得する

ググってすぐに出てくるのは以下だと思います、これで対象のhtmlを取得できるのであればOKだと思いますが取得できないケースにぶつかりました。

Task.detached {
    do {
        let html = try String(contentsOf: url)
        print(html)
    } catch {
        print("error")
    }
}

XXL対策をしているページのhtmlを取得

僕もちゃんと理解しているわけではないですが、セキュリティのためにWebページを開いたタイミングでtokenを発行してそのtokenを使ってwebページを開くものが存在するようです。

その場合上記のコードではhtmlを取得することはできず失敗してしまいます。

そこでアプローチとしてWKEebViewでページを開いてそこからhtmlを取得するという方法を取りました。これであれば基本的にURLを踏んで開けるページに関してはhtmlを取得することが可能です。

tips

httpsに対応していないhttpのリンクはATSでエラーになるので別途対応が必要です!

class ViewController: UIViewController {
    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let url = URL(string:"任意のURL")!

        self.webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
        let request = URLRequest(url: url)
        self.webView.load(request)

        let script = WKUserScript(source: "webkit.messageHandlers.didGetHTML.postMessage(document.documentElement.outerHTML.toString());", injectionTime: .atDocumentEnd, forMainFrameOnly: true)
        self.webView.configuration.userContentController.addUserScript(script)
        var handler = ScriptMessageHandler()
        self.webView.configuration.userContentController.add(handler, name: "didGetHTML")
    }
}

class ScriptMessageHandler: NSObject, WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "didGetHTML", let html = message.body as? String {
            print(html)
        }
    }
}

htmlからリンク画像をスクレイピングする

ライブラリを使うことも考えましたが、URLを取得できれば良いやということで正規表現でURLを取得するようにしました。

jpgとpng画像が取れれば良いやということで絞っていますが、含まない画像も存在するのでURLにアクセスして画像が取得できたものだけ表示するような形でも良いと思います。

private func scraypoingImageURL(html: String) -> [String] {
    // httpで始まり"で終わる文字列を抽出するための正規表現のパターン
    let pattern = "\"https://[^\"]*\""

    guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
    let results = regex.matches(in: html, options: [], range: NSRange(0..<html.count))

    let urls = results.compactMap { result in
        // resultの各範囲に対して、テキストを取得する
        let texts = (0..<result.numberOfRanges).map { i in
            let start = html.index(html.startIndex, offsetBy: result.range(at: i).location)
            let end = html.index(start, offsetBy: result.range(at: i).length)
            return String(html[start..<end])
        }.map(removeQuotes)

        return texts.first { text in
            text.contains("jpg") || text.contains("png")
        }
    }
    // 重複した値を取り除く
    return Array(Set(urls))
}

private func removeQuotes(_ string: String) -> String {
    return string.replacingOccurrences(of: "\"", with: "")
}

コード一覧

import UIKit
import WebKit

class ViewController: UIViewController {
    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let url = URL(string:"任意のURL")!

        Task.detached {

            do {
                let html = try String(contentsOf: url)
                print(html)
            } catch {
                print("取得失敗")
                await MainActor.run {
                    self.webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
                    let request = URLRequest(url: url)
                    self.webView.load(request)

                    let script = WKUserScript(source: "webkit.messageHandlers.didGetHTML.postMessage(document.documentElement.outerHTML.toString());", injectionTime: .atDocumentEnd, forMainFrameOnly: true)
                    self.webView.configuration.userContentController.addUserScript(script)
                    var handler = ScriptMessageHandler()
                    self.webView.configuration.userContentController.add(handler, name: "didGetHTML")
                }
            }
        }
    }
}

class ScriptMessageHandler: NSObject, WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "didGetHTML", let html = message.body as? String {
            print(scraypoingImageURL(html: html))
        }
    }

    private func scraypoingImageURL(html: String) -> [String] {
        // 正規表現のパターン
        let pattern = "\"https://[^\"]*\""

        guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] }
        let results = regex.matches(in: html, options: [], range: NSRange(0..<html.count))

        let urls = results.compactMap { result in
            // resultの各範囲に対して、テキストを取得する
            let texts = (0..<result.numberOfRanges).map { i in
                let start = html.index(html.startIndex, offsetBy: result.range(at: i).location)
                let end = html.index(start, offsetBy: result.range(at: i).length)
                return String(html[start..<end])
            }.map(removeQuotes)

            return texts.first { text in
                text.contains("jpg") || text.contains("png")
            }
        }
        // 重複した値を取り除く
        return Array(Set(urls))
    }

    private func removeQuotes(_ string: String) -> String {
        return string.replacingOccurrences(of: "\"", with: "")
    }
}