F#でインターネットエクスプローラーをつまりは IE を操作してみた！

この記事は[F# Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fsharp)の２日目の記事です




# Summary

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149269/3b591102-7182-cf1f-773e-e7377c3c273f.png)





インターネットエクスプローラーとはホームページを閲覧するソフトウェアです

一昔前までは超はやりのソフトでしたが（だったらしい）、今はクロムがいいらしいです

そんなインターネットエクスプローラー略して IE を

Seleniumを使用してF#で操作してみました



## 環境と事前準備

使用しているパソコン

```
> ver
Microsoft Windows [Version 10.0.19042.630]
```

ドットネット

```
> dotnet --version
5.0.100
```

webブラウザー

```
Internet Explorer11
version 11.630.19041.0
```

## 事前設定

安定的に動作させるためにはレジストリの設定が必要です

管理者権限がある場合は`local machine`を、なければ`current user`を設定します

設定すべきレジストリ項目

```
1. countinuousBrowsing -> 0 にする（無効にする）
2. secondaryStartPage  -> 削除する
3. startPage           -> about:blankにする
4. protectionMode      -> 0 にする（すべてのゾーンで有効にする）
5. isolation           -> PMIL
6. isolation64Bit      -> 0 にする（無効にする）
7. featuerBfCache      -> 0 にする（key nameをiexplore.exeにする -> type is DWORD）
```

see also 

```
Required Configuration
https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver#required-configuration
```

たとえば自分的に、`F#`で設定するのに下記のような便利関数をつくったりしてます

```fsharp

  // Registry config for seleniumIE
  module private IeRegistrySettings =

    module LocalMachine =

      let private setRegSz szName szValue query =
        let regkey:Microsoft.Win32.RegistryKey  =
            Microsoft.Win32.Registry.LocalMachine.CreateSubKey( query)
        regkey.SetValue(szName, szValue,Microsoft.Win32.RegistryValueKind.String)
        regkey.Close()

      let private deleteRegSz szName query =
        let regkey:Microsoft.Win32.RegistryKey  =
            Microsoft.Win32.Registry.LocalMachine.CreateSubKey( query )
        if isNull (regkey.GetValue(szName,null)) |> not then
          regkey.DeleteValue(szName)
        else
          ()

      let private setRegDWord dwName dwValue query =
        let regkey:Microsoft.Win32.RegistryKey  =
            Microsoft.Win32.Registry.LocalMachine.CreateSubKey( query)
        regkey.SetValue(dwName, dwValue,Microsoft.Win32.RegistryValueKind.DWord)
        regkey.Close()


      // スタートアップ（インターネットオプション > 全般） => ホームページから開始する
      let public countinuousBrowsing () =

        let dwName = "Enabled"
        let dwValue = 0 |> int32 // disable is 0, enable is 1

        let query = @"SOFTWARE\Microsoft\Internet Explorer\ContinuousBrowsing"
        setRegDWord dwName dwValue query

        let querybit32 = @"SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\ContinuousBrowsing"
        setRegDWord dwName dwValue querybit32
```

## サンプル

こんな感じで動作させます

まず立ち上げ設定

```fsharp
    // ie-driverの設定
    // IE option: https://www.selenium.dev/selenium/docs/api/dotnet/

    let public myDrivier (waitTime:int) =

      (DriverManager()).SetUpDriver(InternetExplorerConfig(),"Latest", Architecture.X32)

      let driverOptions = InternetExplorerOptions()
      driverOptions.IgnoreZoomLevel <- true
      driverOptions.PageLoadStrategy <- PageLoadStrategy.Eager
      driverOptions.RequireWindowFocus <- false

      let driver = new InternetExplorerDriver(driverOptions)

      driver.Manage().Timeouts().PageLoad <- System.TimeSpan.FromSeconds(waitTime |> float)
      driver.Manage().Timeouts().ImplicitWait <- System.TimeSpan.FromSeconds(waitTime |> float)
      driver.Manage().Window.Position <- System.Drawing.Point(0, 0)
      driver.Manage().Window.Size <- System.Drawing.Size(1500,1000)

      driver
```

リトライは下記な感じで

```fsharp
namespace MyUtil

(*

  util_retry.fs
  -------------------------------------

  Functional backoff/retry
    Backoff/retry with injected mapping from call result to success criterion,
    and backoff times as a parameter. (New version with the backoff in the right place!)
    http://www.fssnip.net/re/title/Functional-backoffretry

*)

module Retry =

  type MyResult = | Ok | NotOk

  let rec private retry (f : unit -> 'T) (didSucceed : 'T -> bool) (timesMs : int list) =
    let rec retry (times:int list) =
      match times with
      | [] ->
        f()
      | head::tail ->
        let result = f ()
        if didSucceed result then
          result
        else
          "リトライしてます..." |> printfn "\n    %s"
          async { do! Async.Sleep(head) } |> Async.RunSynchronously
          retry tail
    retry timesMs


  let private errorlog e fpErrorLog =
    let curdir = System.IO.Directory.GetCurrentDirectory()
    let logfile = System.IO.Path.Combine(curdir,fpErrorLog)
    System.IO.File.AppendAllLines(logfile
      ,[|
        System.DateTime.Now.ToString("yyyyMMdd_HHmmss")
        e.ToString()
        "\n"
      |]
    )


  let public Retry3 filepathErrorlog func =
    retry
      (fun () ->
        try
          func ()
          Ok
        with
        | e ->
          errorlog e filepathErrorlog
          NotOk
      )
      (fun result -> result = Ok)
      [1; 2; 4]
```

上記のコードを使用して例えば下記みたいにコード書いたりしてみます（一例）

```fsharp
// 一般的な設定をグローバル的な感じで
module GlobalParams =

  open OpenQA.Selenium.IE
  open MyUtil.DB
  open MyLocal

  let _fpErrLog = "./../myFiles/errlog/logfile.txt"
  let _conSqlite = sqliteConnection MyDB.MySqlite.dbSetting
  let _driverIE:(ref<InternetExplorerDriver>) = ref null
  let _driverWaitTime = 30
  let _iRetry:(ref<int>) = ref 0



module mainImpl =

    try
     // doSomething

    with e ->

      printfn "%A" (e.ToString())

      // エラーの場合Webブラウザを再起動する
      (!_driverIE).Quit()
      MyUtil.SeleniumIE.UtilIe.forceQuitIe()
      _driverIE := // ここで上記の myDriver的なのを差し込む

      // エラー内容をメインテーブルに出力する
      if !_iRetry = 3 then
        MyLocal.MyDB.MySqlite.update1 _conSqlite
          {|
             // DBにエラーログを書く
          |}

      _iRetry := !_iRetry + 1
      reraise ()


module main =
   _driverIE := // ここで上記の myDriver的なのを差し込む
   // mainImplで実際の処理

```


## その他

### document ready 的な

```fsharp

    open System.Text.RegularExpressions
    let public verifyPageIsLoaded(driver:InternetExplorerDriver) (titlePattern:string) =

      let sw = System.Diagnostics.Stopwatch()
      sw.Start()

      let fst = sw.Elapsed
      let waitTimeSec = driver.Manage().Timeouts().PageLoad |> fun t -> t.TotalSeconds
      let mutable pageLoaded = false
      let mutable cnt = 1

      while not pageLoaded do

        if (sw.Elapsed - fst).TotalSeconds > waitTimeSec then
          raise <| LoadPageException "verifyPageIsLoaded - timeout"
        else
          if (driver.ExecuteScript("return document.readyState;").Equals("complete") ) && (Regex.IsMatch(driver.Title,titlePattern)   ) then
            printfn "\n    問合せ%i回目: %sのページの読込完了！" cnt driver.Title
            pageLoaded <- true
          else
            // debug時はここをコメントアウトする
            // System.Console.SetCursorPosition(0, System.Console.CursorTop)
            printf "    問合せ%i回目: ページの応答を待ってます..." cnt
            ()

        cnt <- cnt + 1
        System.Threading.Thread.Sleep 100
```

### data picker

```fsharp

let cssYMD = "input.xxx.hasDatepicker"

let id = "123456789"
let value = "2020/10/20"

let elm = driver.FindElementByCssSelector(cssYMD)

driver.ExecuteScript(
    "arguments[0].removeAttribute('readonly')"
  , elm
) |> ignore

    elm.SendKeys("2020/10/20")
```

### 非表示ボタンを押す

```fsharp
// 非表示のボタンを表示ボタンにする
let css = "xxx"
let scripts = $"""
  arguments[0].style.visibility = 'visible';
"""
ieDriver.ExecuteScript(scripts,ieDriver.FindElementByCssSelector(css)) |> ignore

let cssButton = "xxx"
ieDriver.FindElementByCssSelector(cssButton)
|> fun x -> x.SendKeys(Keys.Control) ; x.Click()

```

## 参考

```
    Selenium downloads
    https://www.selenium.dev/downloads/

    Selenium Client & WebDriver Language Bindings
    https://www.selenium.dev/downloads/

    Selenium.WebDriver ( use alpha version )
      dotnet add package Selenium.WebDriver --version 4.0.0-alpha07
      dotnet add package Selenium.Support --version 4.0.0-alpha07
```

# 現場からは以上です