執筆のきっかけ・他
・WebスクレイピングをするためにSeleniumを使ったが色々ハマった+C#は情報が少ない
・環境はWin10, VS2017, Google Chrome 83.0.4103.116 (Firefoxでもほぼ変わらず書ける)
・初投稿のため、お手柔らかにご指摘いただけましたら幸いです
目次
・前提
・ライブラリ
<Chromeドライバの操作>
・Chromeを起動する
・Webページを開く
・新しくタブを開く
・タブを閉じる
・タブを切り替える
・タイムアウト時間を設定する
<要素の取得>
・クラス名から要素を取得する
・クラス名から要素を取得する(クラス名にスペースが有る)
・他の要素を取得する方法
・すべての要素を取得する
<要素の解析>
・取得した要素のタグで囲われたテキストを取得
・取得した要素のクラス名を取得する
・取得した要素のHTMLを取得する
・取得した要素のTag名を取得する
<他>
・その他・ハマったこと
・①ウィンドウに対するキー操作が送れない!(Ctrl+sを送りたい)
・②上記コードをタイマーで定期的に実行したいけど動かない
前提
ChromeDriverのインスタンス名はchromeとした
文中のHTMLはぼかして書いているので、実際の挙動と齟齬がある可能性があるがご容赦を
ライブラリ
usingOpenQA.Selenium.Chrome;Chromeを起動する
実行するとコマンドプロンプトが立ち上がり、空っぽのウィンドウが立ち上がる
ChromeDriverchrome=newChromeDriver();Webページを開く
chrome.Url="https://www.google.com/";新しくタブを開く
他にもページを印刷したり色々できるらしい
chrome.ExecuteScript("window.open()");タブを閉じる
chrome.Close();タブを切り替える
例では末尾のタブに切り替えている
chrome.SwitchTo().Window(driver.WindowHandles[GetTabIdxMax()]);intGetTabIdxMax(){returndriver.WindowHandles.Count-1;}タイムアウト時間を設定する
webページが開けいない場合のタイムアウト時間を設定
chrome.Manage().Timeouts().ImplicitWait=TimeSpan.FromSeconds(20);クラス名から要素を取得する
・FindElement"s"にするとぶら下がっている要素すべてをListで受け取れる
・FindElementにすると、最初に見つかった要素を拾えて、要素が見つからない場合例外を吐く
・FindElementsで見つからない場合は要素が0個のListが生成される
例)List_itemというクラス名の要素を取得
・listTextにList_itemの要素2つが追加される
・Tag名"li"の要素を拾いたい場合はFindElementsByTagNameを使う
<ulclass="SearchResultList-box"><liclass="List_item">Text1</li><liclass="List_item">Text2</li></ul>ReadOnlyCollection<IWebElement>itemList=chrome.FindElementsByClassName("List_item");クラス名から要素を取得する(クラス名にスペースが有る)
スペースが有るとダメなことに気づくまでかなりハマった
<ulclass="SearchResultList-box"><liclass="List item">Text1</li><liclass="List item">Text2</li></ul>ReadOnlyCollection<IWebElement>itemList=chrome.FindElementsByCssSelector("[class='List item']");他の要素を取得する方法
ライブラリのメタデータ"RemoteWebDriver"から引用
FindElementsも同じものが用意されている
FindElementとFindElementByCssSelectorについては後述する
publicIWebElementFindElement(Byby);publicIWebElementFindElementByClassName(stringclassName);publicIWebElementFindElementByCssSelector(stringcssSelector);publicIWebElementFindElementByLinkText(stringlinkText);publicIWebElementFindElementByName(stringname);publicIWebElementFindElementByPartialLinkText(stringpartialLinkText);publicIWebElementFindElementByTagName(stringtagName);publicIWebElementFindElementByXPath(stringxpath);”FindElementByClassNameで取得した要素”の下にある要素を取得する
FindElementByClassNameで取得した要素に対して、さらにFindElementByClassNameで要素を取得することができない
これにはFindElement/FindElementsを使う。
<ulclass="SearchResultList-box"><liclass="List_item">Text1</li><liclass="List_item">Text2</li></ul>//クラス名SearchResultList-boxの要素を取得ReadOnlyCollection<IWebElement>itemList_sb=chrome.FindElementsByClassName("SearchResultList-box");//クラス名SearchResultList-boxの下にある//クラス名List_itemの要素を取得//ここでSearchResultList-boxを検索した場合は、自身は含まれないので要素ゼロのListが生成されるReadOnlyCollection<IWebElement>itemList_li=tmpElements[0].FindElements(By.ClassName("List_item"));すべての要素を取得する
すべての要素を取得したい場合、ワイルドカードが使える
例ではli,liの2要素が取得できる
<ulclass="SearchResultList-box"><liclass="List_item">Text1</li><liclass="List_item">Text2</li></ul>//クラス名SearchResultList-boxの要素を取得ReadOnlyCollection<IWebElement>itemList_sb=chrome.FindElementsByClassName("SearchResultList-box");//クラス名SearchResultList-boxの下にあるすべての要素を取得ReadOnlyCollection<IWebElement>itemList_wc=chrome.FindElements(By.CssSelector("*"));取得した要素のタグで囲われたテキストを取得
例の"Text1"を拾いたい場合についてFindElementsで取得した要素のTextプロパティより取得できる
<ulclass="SearchResultList-box"><liclass="List_item">Text1</li><liclass="List_item">Text2</li></ul>//クラス名List_itemの要素を取得ReadOnlyCollection<IWebElement>itemList=chrome.FindElementsByClassName("List_item");//List_item1個めのテキスト"Text1"を取得stringtext=itemList[0].Text;取得した要素のクラス名を取得する
例の"List_item"というクラス名を拾いたい場合、FindElementsで取得した要素に対してGetAttributeを使って取得できる
<ulclass="SearchResultList-box"><liclass="List_item">Text1</li><liclass="List_item">Text2</li></ul>//Tag名"li"の要素を取得ReadOnlyCollection<IWebElement>itemList=chrome.FindElementsByTagName("li");//List_item1個めのクラス名"List_item"を取得stringtext=itemList[0].GetAttribute("class");取得した要素のHTMLを取得する
例では、textの中身が<li class="List_item">Text1</li>
<li class="List_item">Text2</li>
になる。
改行コードがLFの場合がほとんどなので、メモ帳に貼り付けると1行に。。。
<ulclass="SearchResultList-box"><liclass="List_item">Text1</li><liclass="List_item">Text2</li></ul>//Tag名"ul"の要素を取得ReadOnlyCollection<IWebElement>itemList=chrome.FindElementsByTagName("ul");//HTMLを取得stringtext=itemList[0].GetAttribute("innerHTML");取得した要素のTag名を取得する
例の"ul"というTag名を拾いたい場合、FindElementsで取得した要素のTagNameプロパティが使える
クラス名がGetAttribute("class");で拾えるのでTagNameとすれば拾えるかと思いきやダメだった
<ulclass="SearchResultList-box"><liclass="List_item">Text1</li><liclass="List_item">Text2</li></ul>//class名SearchResultList-boxの要素を取得ReadOnlyCollection<IWebElement>itemList=chrome.FindElementsByClassName("SearchResultList-box");//TagName//Tag名を取得stringtext=itemList[0].TagName;以上で解説終わり!
これで簡単な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は適当に作ったボタン
[DllImport("User32.dll")]staticexternintSetForegroundWindow(IntPtrpoint);[DllImport("user32.dll",SetLastError=true)][return:MarshalAs(UnmanagedType.Bool)]privatestaticexternboolSetWindowPos(IntPtrhWnd,inthWndInsertAfter,intx,inty,intcx,intcy,intuFlags);// ウィンドウをアクティブにするpublicstaticvoidSetActiveWindow(IntPtrhWnd){constintSWP_NOSIZE=0x0001;constintSWP_NOMOVE=0x0002;constintSWP_SHOWWINDOW=0x0040;constintHWND_TOPMOST=-1;constintHWND_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")]privatestaticexternboolIsIconic(IntPtrhWnd);[DllImport("user32.dll")]privatestaticexternboolShowWindowAsync(IntPtrhWnd,intnCmdShow);[DllImport("user32.dll")]privatestaticexternintGetWindowThreadProcessId(IntPtrhWnd,outintlpdwProcessId);[DllImport("user32.dll")]privatestaticexternIntPtrGetForegroundWindow();[DllImport("user32.dll")]privatestaticexternboolAttachThreadInput(intidAttach,intidAttachTo,boolfAttach);[DllImport("user32.dll")]publicstaticexternIntPtrFindWindow(stringlpClassName,stringlpWindowName);[DllImport("user32.dll")]publicstaticexternIntPtrSendMessage(IntPtrhWnd,uintMsg,IntPtrwParam,IntPtrlParam);[DllImport("user32.dll")]publicstaticexternIntPtrPostMessage(IntPtrhWnd,uintMsg,IntPtrwParam,IntPtrlParam);[DllImport("user32.dll",EntryPoint="FindWindowEx")]publicstaticexternIntPtrFindWindowEx(IntPtrhwndParent,IntPtrhwndChildAfter,stringlpszClass,stringlpszWindow);publicstaticvoidsendKeystroke(IntPtrhundle,ushortk){constuintWM_KEYDOWN=0x100;constuintWM_SYSCOMMAND=0x018;constuintSC_CLOSE=0x053;IntPtrresult3=SendMessage(hundle,WM_KEYDOWN,((IntPtr)k),(IntPtr)0);//IntPtr result3 = SendMessage(WindowToFind, WM_KEYUP, ((IntPtr)c), (IntPtr)0); }// ウィンドウを強制的にアクティブにするpublicstaticvoidForceActive(IntPtrhandle){constintSW_RESTORE=9;// 最小化状態なら元に戻すif(IsIconic(handle))ShowWindowAsync(handle,SW_RESTORE);intprocessId;// フォアグラウンドウィンドウを作成したスレッドのIDを取得 intforegroundID=GetWindowThreadProcessId(GetForegroundWindow(),outprocessId);// 目的のウィンドウを作成したスレッドのIDを取得inttargetID=GetWindowThreadProcessId(handle,outprocessId);// スレッドのインプット状態を結び付ける AttachThreadInput(targetID,foregroundID,true);// ウィンドウをフォアグラウンドに持ってくるSetForegroundWindow(handle);// スレッドのインプット状態を切り離すAttachThreadInput(targetID,foregroundID,false);}privatevoidbutton5_Click(objectsender,EventArgse){SaveFireFox();}publicvoidSaveFireFox(){System.Diagnostics.ProcesspFireFox=null;foreach(System.Diagnostics.ProcesspinSystem.Diagnostics.Process.GetProcesses()){//メインウィンドウのタイトルがある時だけ列挙するif(p.MainWindowTitle.Length!=0){if(p.ProcessName=="firefox"){pFireFox=p;}}}if(pFireFox!=null){ForceActive(pFireFox.MainWindowHandle);SendKeys.SendWait("^s");Thread.Sleep(3000);SendKeys.SendWait("{Enter}");Thread.Sleep(3000);}}②上記コードをタイマーで定期的に実行したいけど動かない
Task実行しないとウィンドウが固まってしまうため、timerで定期的に実行するようにしたら、なぜか動かない。未解決。
DoEventsを途中で挟むとそれ以降保存できなくなるのでそのあたりになにかあると思われる。

