我正在寻找从页面获取完整 html 的最直接的方法。页面加载后和 JS 完成工作后。

速度并不重要。优化并不重要。主线程的锁定并不重要。

只有结果才是重要的:获取页面的完整 HTML 代码。同步进行。


我尝试使用 WKWebView 获取 html:

class InternetLoader {
    private let webView = WKWebView(frame: .zero)
    
    private let script = """
    window.onload = function() {
        document.body.innerHTML;
    };
    """
    
    private var resultHtml: String? = nil
    
    func getHTML(from url: URL) -> String {
        webView.load(URLRequest(url: url))
        
        let semaphore = DispatchSemaphore(value: 0)
        
        webView.evaluateJavaScript(script) { (result, error) in
            if let error = error {
                print("Error executing JavaScript: \(error.localizedDescription)")
            } else if let htmlContent = result as? String {
                self.resultHtml = htmlContent
            }
            
            semaphore.signal()
        }
        
        semaphore.wait()
        
        return resultHtml!
    }
}

用法:

let il = InternetLoader()
let html = il.getHTML(from: URL(string: "https://stackoverflow.com/questions"))

但它却无休无止地卡住了。

我做错了什么?

14

  • 您能否展示一个没有获得“完整 html”的 URL 示例?显示你得到了什么,以及“完整的 html”应该是什么。


    – 


  • @Sweeper“Оригінальна назва:”(原始名称)阻止页面:“ ”,但我确信除了乌克兰之外的所有国家都无法访问它。


    – 


  • 这不是这些问题的重复。请投票重新开放。


    – 


  • 我可以正常访问该网站,但在该页面上找不到“Оригінальна назва”。如果我可以验证didFinish确实无法获取您想要的 HTML,我将重新打开。


    – 

  • @扫地机


    – 


2 个回答
2

感谢。修改此以获取html

@MainActor
class InternetLoader: NSObject {
    
    lazy var webView: WKWebView = {
        let webView: WKWebView = WKWebView()
        webView.navigationDelegate = self
        return webView
    }()
    
    private var resultHtml: String? = nil
        
    var continuation: UnsafeContinuation<String?, Error>?
    
    func getHTML(from url: URL) async throws -> String? {
        return try await withUnsafeThrowingContinuation { continuation in
            self.continuation = continuation
            webView.load(URLRequest(url: url))
        }
    }
    
}

extension InternetLoader: WKNavigationDelegate {

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        webView.evaluateJavaScript("document.body.innerHTML") { [weak self] (result, error) in
            if let error = error {
                print("Error executing JavaScript: \(error.localizedDescription)")
            } else if let htmlContent = result as? String {
                self?.resultHtml = htmlContent
            }
            self?.continuation?.resume(returning: self?.resultHtml)
        }
    }
    
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        continuation?.resume(throwing: error)
    }

    func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
        continuation?.resume(throwing: error)
    }
}


let loader = InternetLoader()
var html = ""
if let confirmedHtml = try? await loader.getHTML(from: URL(string: "https://stackoverflow.com/questions")!) {
    html = confirmedHtml
}
print(html)

3

  • 它工作得很好,它得到了非常准确的 html 代码。但这是async func而不是sync =(当你将其修复为同步代码时我会给你150个投票点。感谢你的帮助


    – 


  • @Andrew_STOP_RU_WAR_IN_UA Web 视图需要一些时间来加载内容。如果让confirmedHtml = 尝试一下? wait loader.getHTML(from: URL… 在 Web 视图加载或失败之前,此代码不会转到下一行。


    – 


  • 我知道什么意思sync,并且async:)这就是为什么我请求有关确切sync功能的帮助:)这就是为什么我尝试sync用以下方式编写 func semaphore.wait()🙂


    – 


我认为 Immanuel 使用 async/await 的答案很好。您可以创建一个命令行工具,其中结构体main()的方法@mainasync,然后使用他的解决方案。

然而,由于该方法的同步似乎很重要,因此我尝试使用 anOperationQueue和 a RunLoop

问题中的代码的问题是等待信号量导致死锁,因为 for 的完成处理程序evaluateJavascript始终在主线程上执行。由于semaphore.wait()阻塞了主线程,因此信号永远不会被调用。

在我的解决方案中,我创建了异步操作,然后使用运行循环让它们全部按正确的顺序执行。

enum OpStatus {
    case initial, executing, finished
}

class AsyncOp: Operation {
    var opStatus: OpStatus = .initial {
        willSet {
            willChangeValue(for: \.isExecuting)
            willChangeValue(for: \.isFinished)
        }
        didSet {
            didChangeValue(for: \.isExecuting)
            didChangeValue(for: \.isFinished)
        }
    }
    override var isAsynchronous: Bool { true }
    override var isExecuting: Bool { opStatus == .executing }
    override var isFinished: Bool { opStatus == .finished }
}

final class LoadOp: AsyncOp, WKNavigationDelegate {
    private let url: URL
    private let webView: WKWebView
    
    init(url: URL, webView: WKWebView) {
        self.url = url
        self.webView = webView
        super.init()
        self.webView.navigationDelegate = self
    }
    
    override func start() {
        webView.load(URLRequest(url: url))
        opStatus = .executing
    }
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        opStatus = .finished
    }
}

final class JSOp: AsyncOp {
    var resultHtml: String?
    private let webView: WKWebView
    private let script: String
    
    init(webView: WKWebView, script: String) {
        self.webView = webView
        self.script = script
        super.init()
    }
    
    override func start() {
        webView.evaluateJavaScript(script) { (result, error) in
            if let error = error {
                print("Error executing JavaScript: \(error.localizedDescription)")
            } else if let htmlContent = result as? String {
                self.resultHtml = htmlContent
            }
            self.opStatus = .finished
        }
        opStatus = .executing
    }
}

final class InternetLoader {
    private let webView = WKWebView(frame: .zero)
    private let script = "document.documentElement.outerHTML.toString();"
    
    func getHTML(from url: URL) -> String? {
        var jsLoaded = false
        var isQueueSetup = false
        
        let opQueue = OperationQueue.main
        opQueue.maxConcurrentOperationCount = 1
        
        let loadOp = LoadOp(url: url, webView: webView)
        let jsOp = JSOp(webView: webView, script: script)
        jsOp.addDependency(loadOp)
        jsOp.completionBlock = {
            jsLoaded = true
        }
        
        while !jsLoaded && RunLoop.main.run(mode: .default, before: .distantFuture) {
            if !isQueueSetup {
                opQueue.addOperations([loadOp, jsOp], waitUntilFinished: false)
                isQueueSetup = true
            }
        }
        
        return jsOp.resultHtml
    }
}

let il = InternetLoader()
let html = il.getHTML(from: URL(string: "https://stackoverflow.com/questions")!)
print(String(describing: html))

这是很多代码,但它有效。它仍然需要对 Web 视图进行更多错误处理,并且您应该确保这是 的正确使用RunLoop,虽然我对此没有那么丰富的经验,但它可能可以作为一个很好的起点。

2

  • 非常感谢!即使你给出了同步答案,我可以给以马内利 150 分吗?当您给出同步答案时,我将让您选择谁将获得赏金。


    – 


  • 这是你的问题,你决定!如果您选择其他答案并且它适用于您的用例,我不会感到不安,因为我支持在任何明智的地方采用 async/await。


    –