如何在F#中使用async从WebBrowser返回HtmlDocument



在DOM加载之前,我正在尝试抓取一系列在DOM上运行大量javascript的网站。这意味着我使用的是WebBrowser,而不是更友好的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:TempUntitled-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

请求的帮助

  • 此处使用Async.SwitchToContext(browserContext)是否正确
  • 整体方法是否可以简化
  • 有没有一个我似乎一无所知的概念
  • 如何获取WebBrowser.Document

问题出在这一行:

let browserContext = SynchronizationContext()

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

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

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

最新更新