VB.NetでSeleniumを使用中にファイルをダウンロードする処理を入れていたのですが、その部分の処理が正常に動かない状態になってしまったのでその時の対応内容を纏めておきます。
- 目次 -
状況
まずはその部分の処理について。
今回VB.Netでダウンロードを行うにあたり使用した処理は、WebClientクラスを使用した以下の様なものです。
Sub fileDownload(url As String, path As String) Dim wc As New System.Net.WebClient() wc.DownloadFile(url, path) wc.Dispose() End Sub
動かなくなった箇所は「wc.DownloadFile(url, path)」で、具体的なエラーの内容は接続タイムアウトです。
もちろんですが指定しているURLと保存先のパスに誤りは無い事は確認済みです。
また「リモート サーバに接続できません」とは出ていますが、通信回線自体に問題はありません。該当のドメインサイトにもアクセス出来ていますし、ブラウザ上では対象のファイル(今回は画像)も問題無く表示できています。
ちなみにこのエラー・・・
特定のタイミングで呼び出すと発生する様で、再現確率は100%でした。
エラーが発生するタイミングを検証
Seleniumでブラウザ操作中に呼び出したダウンロード処理が動かなくなったという事で、どの部分に問題があるのかを切り分ける為、以下の①~④のタイミングでそれぞれダウンロード処理を呼び出してみました。
※ここで呼び出している「fileDownload」は、冒頭で記述したものを使用しています。
'===========【サンプル1】=========== fileDownload(url, path) '① Dim chrome as New ChromeDriver() fileDownload(url, path) '② With chrome ' 読み込み対象のプロファイルパスをセット .AddArgument("user-data-dir=" & userProfile) .Start() .Get("https://www.yahoo.co.jp/") End With fileDownload(url, path) '③
'===========【サンプル2】=========== Dim chrome as New ChromeDriver() With chrome ' 読み込み対象のプロファイルパスをセット .AddArgument("user-data-dir=" & userProfile) .Start() .Get("https://www.yahoo.co.jp/") End With Threading.Thread.Sleep(5000) chromeDriver.Quit() chromeDriver.Dispose() Threading.Thread.Sleep(5000) fileDownload(url, path) '④
①、②はChromeDriverのインスタンス化前後。
③はブラウザ立ち上げ後。
④は立ち上げたブラウザを終了後、リソースを解放して更に5秒待機した後。
という条件でそれぞれ実行してみた所、
①、②は共に成功し、ファイルがダウンロードできていた事を確認。
③、④は何度試してもタイムアウトとなりました。
③の時点ではChromeが起動している事が原因なのかと思いましたが、④でもエラーのままという事で余計謎が深まってしまいました。
ただし、一度処理を停止させた後、最初から起動してみると①、②の処理で正常にダウンロードする事ができます。
ブラウザを起動した時点で何らかの要素が占有されてしまい、ブラウザを閉じてもそれが解放されないまま残ってしまい、それとWebClientの処理が何か競合してしまっている・・・という事なのかな?
対策を考えてみる
「SeleniumからChromeブラウザを起動した」という事がタイムアウトエラーのきっかけになっている事は何となく分かりましたが、ブラウザを閉じてリソース解放してもダメ・・・となると、お手上げです。そこまで追えない。
という訳で
「エラーを解消する」という対策では無く、
「別の形で動く様にする」という方針に切り替えて幾つか方法を考えてみました。
WebRequest、WebResponseクラスを使ってみる
「System.Net.WebClient」と同様に、WebRequest、WebResponseを使用した以下の処理でもファイルをダウンロードする事が可能です。
Private Sub fileDownload(url As String, path As String) 'WebRequestを作成 Dim webreq As System.Net.HttpWebRequest = DirectCast( System.Net.WebRequest.Create(url), System.Net.HttpWebRequest) 'サーバーからの応答を受信するためのWebResponseを取得 Dim webres As System.Net.HttpWebResponse = DirectCast( webreq.GetResponse(), System.Net.HttpWebResponse) '応答データを受信するためのStreamを取得 Dim strm As System.IO.Stream = webres.GetResponseStream() 'ファイルに書き込むためのFileStreamを作成 Dim fs As New System.IO.FileStream(path, System.IO.FileMode.Create, System.IO.FileAccess.Write) '応答データをファイルに書き込む Dim readData As Byte() = New Byte(1023) {} While True 'データを読み込む Dim readSize As Integer = strm.Read(readData, 0, readData.Length) If readSize = 0 Then 'すべてのデータを読み込んだ時 Exit While End If '読み込んだデータをファイルに書き込む fs.Write(readData, 0, readSize) End While '閉じる fs.Close() strm.Close() End Sub
参考サイト:WebRequest、WebResponseクラスを使ってファイルをダウンロードし保存する(dobon.net 様)
https://dobon.net/vb/dotnet/internet/webrequestsavefile.html
試してみた結果は・・・・
やっぱりダメでした。
webres.GetResponseStream()
の部分で同じく処理が停止してしまい、そのままタイムアウトとなりました。
JavaScriptでローカルにファイルをダウンロードする
chromeDriverクラスにあるExecuteScriptでJavaScriptを実行できるので、これを使ってファイルをダウンロードする事ができないかと思い調べてみましたが・・・出てくるのはhtmlを修正してダウンロードリンクを生成する処理ばかり。
違うそうじゃない。
ただ調べているとこんな感じの書き込みが。
「それ(ローカルへの直接保存)ができてしまったらサイト開くだけでウイルス仕込み放題になってしまう。」
「セキュリティ的に無理」
うーん確かに。
まぁでも出来る事は出来るみたいな内容も見つけました。
ただし保存先やファイル名を自由に設定できない様で、それでは意味が無いので断念。(ダウンロード後にファイルの場所を動かしたり改名する事もできますが、手間が掛かり過ぎるので×)
ダウンロード機能だけを別出しする
引数でURLと保存先のパスを渡して実行すると、色々やってファイルをダウンロードしてくれるスクリプト(VBSとか)を用意する方法です。
途中まで作業を進めた段階で、ふと
「できれば本体のファイルのみで処理を完結させたい・・・」
と思い中断。
という訳で最後まで試してはいませんが、本体とは独立した処理で動作するはずなのでこれはこれで成功したんじゃないかと思う。
URLDownloadToFileを使用する
「そういえば・・・VBAでファイルダウンロードしてた時はまた別の処理を使ってたな・・・」
と、唐突に思い出したのでこちらも実践。
VBAではdllをインポートして使う「URLDownloadToFile」というメソッドでファイルを保存していたので、駄目元で試してみる事に。
' ======= 宣言部分 ======= ' DllImportを行うためにインポート Imports System.Runtime.InteropServices ' 適当なモジュールを用意し、インポートを宣言 Module Common <DllImport("urlmon.dll", CharSet:=CharSet.Unicode)> Public Function URLDownloadToFile( ByVal pCaller As Integer, ByVal szURL As String, ByVal szFileName As String, ByVal dwReserved As Integer, ByVal lpfnCB As Integer ) As Long End Function End Module ' ======= 実装部分 ======= ※ 前後の処理は省略 Dim chrome as New ChromeDriver() URLDownloadToFile(0, url, path, 0, 0) With chrome ' 読み込み対象のプロファイルパスをセット .AddArgument("user-data-dir=" & userProfile) .Start() .Get("https://www.yahoo.co.jp/") End With URLDownloadToFile(0, url, path, 0, 0)
今回はChromeブラウザの立ち上げ前後にダウンロード処理を挟んでテストしてみます。
こちらを実行してみた所、
Chromeブラウザ起動前後のどちらの処理においても、問題無くファイルをダウンロードする事が出来ました!
・・・ただし、本来ダウンロード成功時は戻り値として「0」が返ってくるはずなのになぜか数値が返って来ているのが謎。何かがエラーになっているのだとは思いますが、その割にはファイルは正常に保存できている・・・
まぁ対象のファイルがしっかり保存出来ているのでこの際戻り値は無視しても良いとは思うんですが、何か引っかかるなぁ。
結論
「URLDownloadToFile」なら動く。という結果が出ました。
同じ現象で悩んでいる方がいれば、こういった解決策もあるという事で紹介しておきます。
というかこんな現象があるならどこかで話題に挙がっててもおかしくないはずなんだけど・・・やっぱり環境的な問題という事か・・・。
もしくは本当にSeleniumとWebClientクラスが相性的に悪いとかそういう話になるのかな。
気が向いたら調べてみる事にします。