首页 > 解决方案 > 如何在 F# 中使用异步从 WebBrowser 返回 HtmlDocument?

问题描述

我试图在加载完成之前抓取一系列在 DOM 上运行大量 javascript 的网站。这意味着我使用的是 aWebBrowser而不是友好的WebClient。我想解决的问题是等到WebBrowser.DocumentCompleted事件触发然后返回WebBrowser.Document。然后我对它进行一些后期处理,HtmlDocument但还不能让它返回。

我拥有的代码

let downloadWebSite (address : string) = 
    let browser = new WebBrowser()
    let browserContext = SynchronizationContext()
    browser.DocumentCompleted.Add (fun _ ->
        printfn "Document Loaded")

    async {
        do browser.Navigate(address)
        let! a = Async.AwaitEvent browser.DocumentCompleted
        do! Async.SwitchToContext(browserContext)
        return browser.Document)
    }


[downloadWebSite "https://www.google.com"]
|> Async.Parallel // there will be more addresses when working
|> Async.RunSynchronously

错误

System.InvalidCastException: Specified cast is not valid.
   at System.Windows.Forms.UnsafeNativeMethods.IHTMLDocument2.GetLocation()
   at System.Windows.Forms.WebBrowser.get_Document()
   at FSI_0058.downloadWebSite@209-41.Invoke(Unit _arg2) in C:\Temp\Untitled-1.fsx:line 209
   at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvokeNoHijackCheck[a,b](AsyncActivation`1 ctxt, FSharpFunc`2 userCode, b result1)
   at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.FSharp.Control.AsyncResult`1.Commit()
   at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronouslyInAnotherThread[a](CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout)
   at Microsoft.FSharp.Control.AsyncPrimitives.RunSynchronously[T](CancellationToken cancellationToken, FSharpAsync`1 computation, FSharpOption`1 timeout)
   at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously[T](FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken)
   at <StartupCode$FSI_0058>.$FSI_0058.main@()
Stopped due to error

我认为正在发生的事情

有几个问题让我相信我是WebBrowser从错误的线程访问的。1 2 3

请求帮助

标签: asynchronousf#

解决方案


问题出在这一行:

let browserContext = SynchronizationContext()

您手动创建了一个新实例,SynchronizationContext但没有将其与 UI 线程或任何线程相关联。这就是当您访问browser.Document必须在 UI 线程上访问的程序时程序崩溃的原因。

要解决这个问题,只需使用SynchronizationContext已经与 UI 线程关联的现有:

let browserContext = SynchronizationContext.Current

我假设该downloadWebSite函数是在 UI 线程上调用的。如果不是,您可以将上下文从某处传递到函数中,或使用全局变量。

更好的设计

尽管Async.SwitchToContext您可以确保下一行在 UI 线程中访问并返回文档,但接收文档的客户端代码可能在非 UI 线程上运行。更好的设计是使用延续函数。您可以返回由作为参数SomeType传入的延续函数产生的值,而不是直接返回文档。downloadWebSite通过这种方式,可以确保继续函数在 UI 线程上运行:

let downloadWebSite (address : string) cont =
    let browser = new WebBrowser()
    let browserContext = SynchronizationContext.Current
    browser.DocumentCompleted.Add (fun _ ->
        printfn "Document Loaded")

    async {
        do browser.Navigate(address)
        let! a = Async.AwaitEvent browser.DocumentCompleted
        do! Async.SwitchToContext(browserContext)
        // the cont function is ensured to be run on UI thread:
        return cont browser.Document }

[downloadWebSite "https://www.google.com" (fun document -> (*safely access document*))]
|> Async.Parallel
|> Async.RunSynchronously

推荐阅读