C#：SeleniumでWeb解析


##執筆のきっかけ・他
・WebスクレイピングをするためにSeleniumを使ったが色々ハマった＋C#は情報が少ない
・環境はWin10, VS2017, Google Chrome 83.0.4103.116 (Firefoxでもほぼ変わらず書ける)
・初投稿のため、お手柔らかにご指摘いただけましたら幸いです

2020.10.11更新
・この記事を読めばChromeDriberを新規プロジェクトから実行できるよう追記
　「VSC#のインストール」や「ウィンドウズフォームアプリの作成」はGoogleで検索を
2021.05.08
・属性値：例(a href="http:...")などのテキスト部分取得する方法について追記
  「取得した要素の属性値を取得する」
2021.08.11
・公式ページを見つけたのでリンクを張っておきます

https://www.selenium.dev/ja/documentation/webdriver/web_element/

・他言語ですが辞書的に使えるページがありますので貼っておきます

https://www.seleniumqref.com/

##前提
ChromeDriverのインスタンス名はchromeとした
文中のHTMLはぼかして書いているので、実際の挙動と齟齬がある可能性がありますがご容赦を・・・

##目次
右ペインの一覧をご参照ください

##ドライバのダウンロード
ChromeDriverは「[https://chromedriver.chromium.org/downloads](https://chromedriver.chromium.org/downloads)」よりダウンロードできる。
バーションはChromeのバージョンと合わせる必要があり、以下の方法で確認できる

1.Chromeを起動する
2.右上の点3つのボタンを押す
3.設定をクリック
4.設定画面＞Chromeについてをクリック
5.バージョンを確認する
　※公開されているChromeDriverのバージョンと一致するものがない場合、
　　バージョンが85.0.4183.121なら、85.0の最新版のChromeDriverを使えば大丈夫　　

＜手順1-3＞
![3.設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433821/90c288cb-5643-3313-da6b-2df4b7acd26c.png)
<手順4-5>
![4.確認.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433821/4520e4d4-2c81-297c-7e2f-91b881280c27.png)

##ドライバを置く場所
プロジェクトがおいてあるフォルダに置いてあればよい (なくても良い時があるが、理由は不明)
![5.ドライバを置く場所.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433821/0e9bbdf8-a14f-72a9-8ae0-5b1c0013959b.png)




##ライブラリ

NugetからChromeDriverのNugetパッケージをインストールする必要がある。
Selenium.WebDriverとSelenium.WebDriver.ChromeDriverをインストールする。

＜Nuget管理マネージャを開く＞
![6.Nuget1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433821/c9e30b0b-4dbc-9bbb-b9a9-53395ba12c5d.png)

＜必要なNugetパッケージをインストールする＞
![7.Nuget2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433821/cd56f934-4ca9-cd7a-4c4c-8328b3df6397.png)

##インクルード

下記をインクルードする

```csharp
using OpenQA.Selenium.Chrome;
```

##Chromeを起動する
実行するとコマンドプロンプトが立ち上がり、空っぽのウィンドウが立ち上がる

```csharp:cs
ChromeDriver chrome = new ChromeDriver();
```

<img width="376" alt="Chromeを起動する.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433821/fe9ffb50-54bd-a03c-a70b-a8d7f999b946.png">

##Webページを開く

```csharp:cs

chrome.Url = "https://www.google.com/";
```

<img width="481" alt="2.Webページを開く.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433821/ada5af72-18e6-eaa9-6b3f-240907a915dd.png">
##新しくタブを開く
他にもページを印刷したり色々できるらしい

```csharp:cs
chrome.ExecuteScript("window.open()");
``` 

##タブを閉じる

```csharp:cs
chrome.Close();
```

##タブを切り替える
例では末尾のタブに切り替えている

```cs:cs

chrome.SwitchTo().Window(driver.WindowHandles[GetTabIdxMax()]);
int GetTabIdxMax() { return driver.WindowHandles.Count - 1; }
```


##タイムアウト時間を設定する
webページが開けない場合のタイムアウト時間を設定

```cs:cs

chrome.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(20);
```

##クラス名から要素を取得する
・FindElementsByClassNameを使う
・FindElement"s"にするとぶら下がっている要素すべてをListで受け取れる
・FindElementsで見つからない場合は要素が0個のListが生成される
・FindElementにすると、最初に見つかった要素を拾えて、要素が見つからない場合例外を吐く


例)クラス名List_itemの要素を取得
・itemListにList_itemの要素2つが追加される
・Tag名"li"の要素を拾いたい場合はFindElementsByTagNameを使う

``` HTML:HTML
<ul class="SearchResultList-box">
<li class="List_item">Text1</li>
<li class="List_item">Text2</li>
</ul>
```


``` cs:cs
ReadOnlyCollection<IWebElement> itemList= chrome.FindElementsByClassName("List_item");
```

##クラス名から要素を取得する(クラス名にスペースが有る)
スペースが有るとダメなことに気づくまでかなりハマった

```HTML:HTML
<ul class="SearchResultList-box">
<li class="List item">Text1</li>
<li class="List item">Text2</li>
</ul>
```

```cs:cs
ReadOnlyCollection<IWebElement> itemList= chrome.FindElementsByCssSelector("[class='List item']");
```

##他の要素を取得する方法
ライブラリのメタデータ"RemoteWebDriver"から引用
FindElementsも同じものが用意されている
FindElementとFindElementByCssSelectorについては後述する

```cs:cs
        public IWebElement FindElement(By by);
        public IWebElement FindElementByClassName(string className);
        public IWebElement FindElementByCssSelector(string cssSelector);
        public IWebElement FindElementByLinkText(string linkText);
        public IWebElement FindElementByName(string name);
        public IWebElement FindElementByPartialLinkText(string partialLinkText);
        public IWebElement FindElementByTagName(string tagName);
        public IWebElement FindElementByXPath(string xpath);
```

##”FindElementByClassNameで取得した要素”の下にある要素を取得する
FindElementByClassNameで取得した要素に対して、さらにFindElementByClassNameで要素を取得することができない
これにはFindElement/FindElementsを使う。

```HTML:HTML
<ul class="SearchResultList-box">
<li class="List_item">Text1</li>
<li class="List_item">Text2</li>
</ul>
```

```cs:cs
//クラス名SearchResultList-boxの要素を取得
ReadOnlyCollection<IWebElement> itemList_sb = chrome.FindElementsByClassName("SearchResultList-box");
//クラス名SearchResultList-boxの下にある
//クラス名List_itemの要素を取得
//ここでSearchResultList-boxを検索した場合は、自身は含まれないので要素ゼロのListが生成される
ReadOnlyCollection<IWebElement> itemList_li = itemList_sb[0].FindElements(By.ClassName("List_item"));
```

##すべての要素を取得する

すべての要素を取得したい場合、ワイルドカードが使える
例ではli,liの2要素が取得できる

```HTML:HTML
<ul class="SearchResultList-box">
<li class="List_item">Text1</li>
<li class="List_item">Text2</li>
</ul>
```

```cs:cs
//クラス名SearchResultList-boxの要素を取得
ReadOnlyCollection<IWebElement> itemList_sb = chrome.FindElementsByClassName("SearchResultList-box");
//クラス名SearchResultList-boxの下にあるすべての要素を取得
ReadOnlyCollection<IWebElement> itemList_wc = itemList_sb[0].FindElements(By.CssSelector("*"));
```

##取得した要素のタグで囲われたテキストを取得
例の"Text1"を拾いたい場合についてFindElementsで取得した要素のTextプロパティより取得できる

```HTML:HTML
<ul class="SearchResultList-box">
<li class="List_item">Text1</li>
<li class="List_item">Text2</li>
</ul>
```

```cs:cs
//クラス名List_itemの要素を取得
ReadOnlyCollection<IWebElement> itemList = chrome.FindElementsByClassName("List_item");
//List_item1個めのテキスト"Text1"を取得
string text = itemList[0].Text;

```

##取得した要素のクラス名を取得する
例の"List_item"というクラス名を拾いたい場合、FindElementsで取得した要素に対してGetAttributeを使って取得できる

```HTML:HTML
<ul class="SearchResultList-box">
<li class="List_item">Text1</li>
<li class="List_item">Text2</li>
</ul>
```

```cs:cs
//Tag名"li"の要素を取得
ReadOnlyCollection<IWebElement> itemList = chrome.FindElementsByTagName("li");
//Tag名liの1個めのクラス名"List_item"を取得
string text = itemList[0].GetAttribute("class");
```

##取得した要素のHTMLを取得する
例では、textの中身が
`<li class="List_item">Text1</li>
 <li class="List_item">Text2</li>`
になる。
改行コードがLFの場合がほとんどなので、メモ帳に貼り付けると1行に。。。


```HTML:HTML
<ul class="SearchResultList-box">
<li class="List_item">Text1</li>
<li class="List_item">Text2</li>
</ul>
```

```cs:cs
//Tag名"ul"の要素を取得
ReadOnlyCollection<IWebElement> itemList = chrome.FindElementsByTagName("ul");
//HTMLを取得
string text = itemList[0].GetAttribute("innerHTML");

```

##取得した要素のTag名を取得する
例の"ul"というTag名を拾いたい場合、FindElementsで取得した要素のTagNameプロパティが使える
クラス名が`GetAttribute("class");`で拾えるのでTagNameとすれば拾えるかと思いきやダメだった

```HTML:HTML
<ul class="SearchResultList-box">
<li class="List_item">Text1</li>
<li class="List_item">Text2</li>
</ul>
```

```cs:cs
//class名SearchResultList-boxの要素を取得
ReadOnlyCollection<IWebElement> itemList = chrome.FindElementsByClassName("SearchResultList-box");//TagName
//Tag名uiを取得
string text = itemList[0].TagName;

```

##取得した要素の属性値を取得する
属性値を取得するGetAttribute(string)を使えば属性値を取得できる
リンクの場合はhrefが属性値になるので例ではGetAttribute("href")としている。
GetAttribute("target")とすれば、"_blank"を取得することができる

```HTML:HTML
<div class="URL-box">
<a class="List_item" href="https://www.google.co.jp/" target="_blank">Google</li>
<a class="List_item" href="https://www.yahoo.co.jp/" target="_blank">Yahoo</li>
</div>
```

```cs:cs
//class名SearchResultList-boxの要素を取得
ReadOnlyCollection<IWebElement> itemList = chrome.FindElementsByClassName("URL-box");
//URLを取得
string text = itemList[0].GetAttribute("href");//textには"https://www.google.co.jp/"が代入される

```

以上で解説終わり！
これで簡単なWebページ解析ができるようになるはずです！

##その他・ハマったこと
###①ウィンドウに対するキー操作が送れない！(Ctrl+sを送りたい)
　Chromeでページを完全ページとして保存したかったため、Ctrl+sキーを送信して
　Webページを保存しようとした。htmlだけの保存だと中途半端なので。。。
　がしかし、ウィンドウに対するキー操作は受け付けてくれない模様。
　※Ctrl+aで全選択はできるのにWhy!? もちろんFirefoxもダメ!
 例)
　`chrome.FindElementByTagName("html").SendKeys(Keys.LeftControl + "s" );`

　継ぎ接ぎだらけではあるが、下記を参考に保存できたのでメモしておく
　要約すると、Firefoxのプロセスを探して強制的にActiveにして、外部からCtrl+sを送っている
　＜参考URL＞
　　感謝！
　　https://qiita.com/yaju/items/af308376f04ef2ff1325

　※他にwebブラウザを開いているとそっちを拾ってしまうので全て閉じておいてください
    button5_Clickは適当に作ったボタン

```cs:cs
        [DllImport("User32.dll")]
        static extern int SetForegroundWindow(IntPtr point);
        [DllImport("user32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int x, int y, int cx, int cy, int uFlags);

        // ウィンドウをアクティブにする
        public static void SetActiveWindow(IntPtr hWnd)
        {
            const int SWP_NOSIZE = 0x0001;
            const int SWP_NOMOVE = 0x0002;
            const int SWP_SHOWWINDOW = 0x0040;

            const int HWND_TOPMOST = -1;
            const int HWND_NOTOPMOST = -2;

            SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
            SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOMOVE | SWP_NOSIZE);
        }

        [DllImport("user32.dll")]
        private static extern bool IsIconic(IntPtr hWnd);
        [DllImport("user32.dll")]
        private static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
        [DllImport("user32.dll")]
        private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
        [DllImport("user32.dll")]
        private static extern IntPtr GetForegroundWindow();
        [DllImport("user32.dll")]
        private static extern bool AttachThreadInput(int idAttach, int idAttachTo, bool fAttach);

        [DllImport("user32.dll")]
        public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
        [DllImport("user32.dll")]
        public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
        [DllImport("user32.dll")]
        public static extern IntPtr PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll", EntryPoint = "FindWindowEx")]
        public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);

        // ウィンドウを強制的にアクティブにする
        public static void ForceActive(IntPtr handle)
        {
            const int SW_RESTORE = 9;

            // 最小化状態なら元に戻す
            if (IsIconic(handle))
                ShowWindowAsync(handle, SW_RESTORE);

            int processId;
            // フォアグラウンドウィンドウを作成したスレッドのIDを取得         
            int foregroundID = GetWindowThreadProcessId(GetForegroundWindow(), out processId);
            // 目的のウィンドウを作成したスレッドのIDを取得
            int targetID = GetWindowThreadProcessId(handle, out processId);

            // スレッドのインプット状態を結び付ける   
            AttachThreadInput(targetID, foregroundID, true);
            // ウィンドウをフォアグラウンドに持ってくる
            SetForegroundWindow(handle);
            // スレッドのインプット状態を切り離す
            AttachThreadInput(targetID, foregroundID, false);
        }

        private void button5_Click(object sender, EventArgs e)
        {
            SaveFireFox();
        }

        public void SaveFireFox()
        {
            System.Diagnostics.Process pFireFox = null;
            foreach (System.Diagnostics.Process p in System.Diagnostics.Process.GetProcesses())
            {
                //メインウィンドウのタイトルがある時だけ列挙する
                if (p.MainWindowTitle.Length != 0)
                {
                    if (p.ProcessName == "firefox")
                    {
                        pFireFox = p;
                    }
                 }
            }
            if (pFireFox != null)
            {
                //FireFoxをアクティブに
                ForceActive(pFireFox.MainWindowHandle);
                //Ctrl+sを送る→保存画面が出る
                SendKeys.SendWait("^s");
                Thread.Sleep(3000);
                //Enterを送って保存
                SendKeys.SendWait("{Enter}");
                Thread.Sleep(3000);
            }
        }

```

### ②上記コードをタイマーで定期的に実行したいけど動かない
Task実行しないとウィンドウが固まってしまうため、timerで定期的に実行するようにしたら、なぜか動かない。未解決。
DoEventsを途中で挟むとそれ以降保存できなくなるのでそのあたりになにかあると思われる。