colinardo
Goto Top

PowerShell: Einführung in die Webbrowser Automation mit Selenium WebDriver

article-picture

back-to-topEinleitung


Diese Anleitung soll eine Einführung in den Einsatz der Browser Automatisierung mittels Selenium WebDriver und Powershell geben und anhand von kleinen Beispielen den Einsatz der gebotenen Funktionen aufzeigen. Mit der Selenium WebDriver Technik ist das Automatisieren von unterschiedlichen Browsern möglich darunter auch die drei Schwergewichte Google Chrome, Microsoft Edge, Mozilla Firefox auf die ich mich hier primär beziehen werde. Selenium Webdriver gibt es für die Nutzung unter diversen Skript- und Programmiersprachen darunter auch als .NET Bibliothek die in diesem Beitrag Verwendung findet.

back-to-topGründe für die Nutzung einer Browserautomatisierung


back-to-topExtrahieren von Daten

In der Regel ist es ja so das man Aufgaben wie das Extrahieren von Daten auf Webseiten schneller und einfacher mittels der CMDLets Invoke-WebRequest oder Invoke-Restmethod extrahieren lassen. Aber es gibt immer mehr Webseiten die Ihre Daten dynamisch zusammenstellen und diese bspw. nur mit aktivem JavaScript Ihre Inhalte auch preisgeben. Wenn es also weder eine API zum Abfragen von Daten gibt, oder man keine direkten Ajax-Pfade mit den Developer-Tools der Browser ausfindig machen kann, oder sich deren Umgang schwierig bis unmöglich gestaltet, bleibt einem oft nur noch die Browser-Automatisierung bei der der Browser quasi mit Befehlen ferngesteuert wird.
Lange benutzte man dazu unter Windows den Internet-Explorer, da dieser ein COM-Objekt zur Verfügung stellt mit dem sich der Browser mit etlichen Skript- und Programmiersprachen fernsteuern lässt. Da aber mittlerweile viele Webseiten einfach keine vernünftige Unterstützung mehr für den IE bieten, dieser nicht mehr alle aktuellen Techniken einsetzt und der IE in naher Zukunft sowieso abgekündigt (R.I.P) ist, sollte nun wirklich die Zeit gekommen sein das man diesbezüglich auf aktuellere Pferde setzen sollte. Das war auch einer der Gründe dafür das ich diesen Beitrag hier schreibe, es soll den Umstieg von der IE Automation hin zu Selenium etwas erleichtern.

back-to-topSimulieren von realen Usern

Ein anderes Feld ist das Simulieren von realen Usern und das automatisierte Testen von Webseiten. Das kann bspw. dazu dienen ob die eigene Webseite auch unter der Last von vielen Usern noch so funktioniert wie vorgesehen, wie das UI reagiert, etc. pp. Dafür bietet sich Selenium Webdriver ebenfalls an.

back-to-topVoraussetzungen und benötigte Komponenten für die Nutzung in der Powershell

Selenium Webdriver besteht aus mehreren Komponenten. Einmal benötigen wir die Selenium.WebDriver .NET Bibliothek als DLL und dazu den passenden Driver (eine EXE) abhängig von dem zu automatisierenden Browser. Der Driver ist oftmals versionsspezifisch, so dass ein Driver der für Chrome 92 geschaffen wurde kein installiertes Chrome 94 fernsteuern kann.

Die in diesem Beitrag verwendeten Driver sind: ChromeDriver, MSEdgeDriver, FirefoxDriver

Der Browser den man fernsteuern möchte muss zudem auf dem System installiert sein.

Um euch das Herunterladen und laden der Assemblies für den Anfang zu erleichtern habe ich all das in eine Funktion Namens Create-Browser gepackt welche je nachdem welchen Browser man nutzen möchte die benötigten DLLs bei der ersten Verwendung entweder in das Skriptverzeichnis (wenn das Skript schon gespeichert ist) oder in das TEMP-Verzeichnis (wenn z.B. in der ISE nur ausprobiert wird) herunterlädt und die Bibliothek via Add-Type in die Session importiert. Sind die Driver und die DLL bereits im Skript- oder TEMP-Verzeichnis wird natürlich kein erneuter Download angestoßen!

back-to-topCreate-Browser: Grundfunktion zum Erzeugen einer Browser-Instanz


Für den Anfang müssen wir ein WebDriver-Objekt des jeweiligen Browsers erzeugen auf dem anschließend alles andere aufbaut. Das ist für alle Browser ähnlich, es gibt aber ein paar kleine Detailunterschiede. (Mit den Details will ich euch jetzt nicht behelligen, das lässt sich im folgenden CMDLet selbst nachlesen.)
Die Erzeugung des Webdriver-Services und des Driver-Objekts stelle ich euch mittels folgender Funktion zur Verfügung. Sie unterstützt die zum Zeitpunkt der Erstellung dieses Beitrags aktuellen Major Versionen der o.g. Browser. Sollte eine ältere oder neuere Version zum Einsatz kommen die nicht in der Funktion hinterlegt ist kann der Funktion die zu nutzende Driver-Version die auf Nuget unter den oben genannte Links zu den Drivern als verfügbar gelistet wird mit dem Parameter -driverversion <Versionstring> mitgegeben werden.
function Create-Browser {
    param(
        [Parameter(mandatory=$true)][ValidateSet('Chrome','Edge','Firefox')][string]$browser,    
        [Parameter(mandatory=$false)][bool]$HideCommandPrompt = $true,
        [Parameter(mandatory=$false)][string]$driverversion = '',    
        [Parameter(mandatory=$false)][object]$options = $null
    )
    $driver = $null

    function Load-NugetAssembly {
	    [CmdletBinding()]
	    param(
		    [string]$url,
		    [string]$name,
		    [string]$zipinternalpath,
		    [switch]$downloadonly
	    )
	    if($psscriptroot -ne ''){    
		    $localpath = join-path $psscriptroot $name
	    }else{
		    $localpath = join-path $env:TEMP $name
	    }
	    $tmp = "$env:TEMP\$([IO.Path]::GetRandomFileName())"    
	    $zip = $null
	    try{
		    if(!(Test-Path $localpath)){
			    Add-Type -A System.IO.Compression.FileSystem
			    write-host "Downloading and extracting required library '$name' ... " -F Green -NoNewline    
			    (New-Object System.Net.WebClient).DownloadFile($url, $tmp)
			    $zip = [System.IO.Compression.ZipFile]::OpenRead($tmp)
			    $zip.Entries | ?{$_.Fullname -eq $zipinternalpath} | %{
				    [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_,$localpath)
			    }
			    write-host "OK" -F Green    
		    }
		    if (Get-Item $localpath -Stream zone.identifier -ea SilentlyContinue){
			    Unblock-File -Path $localpath
		    }
		    if(!$downloadonly.IsPresent){
			    Add-Type -Path $localpath -EA Stop
		    }
	    }catch{
		    throw "Error: $($_.Exception.Message)"    
	    }finally{
		    if ($zip){$zip.Dispose()}
		    if(Test-Path $tmp){del $tmp -Force -EA 0}
	    }
    }

    # Load Selenium Webdriver .NET Assembly and dependencies
    Load-NugetAssembly 'https://www.nuget.org/api/v2/package/Newtonsoft.Json' -name 'Newtonsoft.Json.dll' -zipinternalpath 'lib/net45/Newtonsoft.Json.dll' -EA Stop  
    Load-NugetAssembly 'https://www.nuget.org/api/v2/package/Selenium.WebDriver' -name 'WebDriver.dll' -zipinternalpath 'lib/netstandard2.0/WebDriver.dll' -EA Stop  
    
    if($psscriptroot -ne ''){    
        $driverpath = $psscriptroot
    }else{
        $driverpath = $env:TEMP
    }
    switch($browser){
        'Chrome' {    
            $chrome = Get-Package -Name 'Google Chrome' -EA SilentlyContinue | select -F 1    
            if (!$chrome){
                throw "Google Chrome Browser not installed."    
                return
            }
            Load-NugetAssembly "https://www.nuget.org/api/v2/package/Selenium.WebDriver.ChromeDriver/$driverversion" -name 'chromedriver.exe' -zipinternalpath 'driver/win32/chromedriver.exe' -downloadonly -EA Stop    
            # create driver service
            $dService = [OpenQA.Selenium.Chrome.ChromeDriverService]::CreateDefaultService($driverpath)
            # hide command prompt window
            $dService.HideCommandPromptWindow = $HideCommandPrompt
            # create driver object
            if ($options){
                $driver = New-Object OpenQA.Selenium.Chrome.ChromeDriver $dService,$options
            }else{
                $driver = New-Object OpenQA.Selenium.Chrome.ChromeDriver $dService
            }
        }
        'Edge' {    
            $edge = Get-Package -Name 'Microsoft Edge' -EA SilentlyContinue | select -F 1    
            if (!$edge){
                throw "Microsoft Edge Browser not installed."    
                return
            }
            Load-NugetAssembly "https://www.nuget.org/api/v2/package/Selenium.WebDriver.MSEdgeDriver/$driverversion" -name 'msedgedriver.exe' -zipinternalpath 'driver/win64/msedgedriver.exe' -downloadonly -EA Stop    
            # create driver service
            $dService = [OpenQA.Selenium.Edge.EdgeDriverService]::CreateDefaultService($driverpath)
            # hide command prompt window
            $dService.HideCommandPromptWindow = $HideCommandPrompt
            # create driver object
            if ($options){
                $driver = New-Object OpenQA.Selenium.Edge.EdgeDriver $dService,$options
            }else{
                $driver = New-Object OpenQA.Selenium.Edge.EdgeDriver $dService
            }
        }
        'Firefox' {    
            $ff = Get-Package -Name "Mozilla Firefox*" -EA SilentlyContinue | select -F 1    
            if (!$ff){
                throw "Mozilla Firefox Browser not installed."    
                return
            }
            Load-NugetAssembly "https://www.nuget.org/api/v2/package/Selenium.WebDriver.GeckoDriver/$driverversion" -name 'geckodriver.exe' -zipinternalpath 'driver/win64/geckodriver.exe' -downloadonly -EA Stop    
            # create driver service
            $dService = [OpenQA.Selenium.Firefox.FirefoxDriverService]::CreateDefaultService($driverpath)
            # hide command prompt window
            $dService.HideCommandPromptWindow = $HideCommandPrompt
            # create driver object
            if($options){
                $driver = New-Object OpenQA.Selenium.Firefox.FirefoxDriver $dService, $options
            }else{
                $driver = New-Object OpenQA.Selenium.Firefox.FirefoxDriver $dService
            }
        }
    }
    return $driver
}

back-to-topErzeugen eines Browser-Objekts


Der erste Schritt ist nun das Erzeugen des jeweiligen Browser-Objekts. Man muss das obige CMDLet immer als erstes im Skript platzieren damit man sie nutzen kann. Um nun den jeweiligen Browser anzugeben besitzt die Create-Browser]] Funktion den Parameter -browser.Eingebaut sind die folgenden Browser Google Chrome, Microsoft Edge (chromium based) und Mozilla Firefox.Valide Parameter-Werte für den Parameter lauten: {{Chrome, Edge, Firefox.

Beispiel für das Erzeugen eines Chrome-Browsers:
$browser = Create-Browser -browser Chrome

Hat das geklappt war das eigentlich schon die halbe Miete und das Browser-Fenster sollte auf dem Desktop erscheinen.

Die Funktion hat noch einen weiteren Boolean-Parameter namens -HideCommandPrompt, welcher das normalerweise erscheinende Kommandozeilenfenster des Webdrivers per Default unterdrückt, möchte man es sehen wenn man bspw. Debug-Ausgaben sehen möchte setzt man diesen Parameter auf $false mittels -HideCommandPrompt $false.
Ebenso kann eine spezielle WebDriver-Version mittel -driverversion <Versionstring> angeben werden die genutzt werden soll, sollte das nötig sein, siehe Hinweis weiter oben.

In den nun folgenden Code-Schnippseln werde ich der Verständlichkeit halber die Variable $browser weiterhin beibehalten und das Erzeugen des Browser-Objekts nicht erneut wiederholen, ich gehe also davon aus das Ihr das $browser Objekt in euren Skripten bereits vor dem Ausprobieren der folgenden Funktionen bereits erzeugt habt.

back-to-topBrowser-Fenster anpassen


Die Anpassung des Browser-Fensters lässt sich mit den folgenden Funktionen und Eigenschaften steuern:

back-to-topx/y Position festlegen


$browser.Manage().window.position = '0,0'  

back-to-topGröße des Fensters in Pixeln festlegen (Breite,Höhe)


$browser.Manage().window.Size = '1024,768'  

back-to-topFenster maximieren, minimieren, Fullscreen


# maximize Window
$browser.Manage().Window.Maximize() 
# minimize Window
$browser.Manage().Window.Minimize()
# Fullscreen
$browser.Manage().Window.FullScreen()

back-to-topNavigieren zu URLs


Das Öffnen von Webseiten bzw. URLs lässt sich nun folgendermaßen erledigen:

$browser.Url = 'https://administrator.de'  
$navi = $browser.Navigate() 

Man legt die URL in der Eigenschaft URL fest. Das zurückgegebene Objekt der Navigate() Funktion bietet seinerseits weitere Funktionen um im Fenster zu vor und zurück zu navigieren oder die aktuelle Seite zu aktualisieren, es ist also meist eine gute Idee das Ergebnis einer Variablen zuzuweisen wenn man es später wieder benötigt.
Name        MemberType Definition                                                                                                              
----        ---------- ----------                                                                                                              
Back        Method     void Back(), void INavigation.Back()                                                                                                                                                                           
Forward     Method     void Forward(), void INavigation.Forward()                                                                                                                                                                                                                                                                      
GoToUrl     Method     void GoToUrl(string url), void GoToUrl(uri url), void INavigation.GoToUrl(string url), void INavigation.GoToUrl(uri url)
Refresh     Method     void Refresh(), void INavigation.Refresh()                                                                              
Alternativ geht also auch so ein Konstrukt um zu einer neuen Seite zu navigieren
$browser.Navigate().GotoURL("https://google.com")

back-to-topSchließen/Beenden des Browsers


Das Beenden der Browser-Session erledigt sich mit der für sich sprechenden Funktion
$browser.Quit()

Wichtig! Das sollte man am Ende einer Session nicht vergessen auszuführen, da ansonsten der WebDriver im Hintergrund (unsichtbar) weiter läuft auch wenn die Powershell-Session beendet wird. Lässt sich die heruntergeladenen EXE des WebDriver nicht manuell löschen weil sie noch im Zugriff ist ist das ein Zeichen dafür das eine Instanz des WebDrivers noch im Hintergrund läuft oder die PS Session nicht richtig abgeschlossen wurde oder hängt.

back-to-topSeitenquelltext extrahieren


$browser.PageSource

back-to-topInformationen über Cookies


Cookies der aktuell aufgerufenen Seite erhält man mittels
$browser.Manage().Cookies.AllCookies

back-to-topScreenshot des aktuell sichtbaren Seiteninhalts erzeugen


Von dem aktuell sichtbaren Fenster lässt sich ein Screenshot erstellen, und lässt sich entweder direkt als Datei in diversen Dateiformaten (.NET Standardformate) speichern ...
$browser.GetScreenshot().SaveAsFile("C:\Pfad\screenshot.png",'png')
oder wenn man das Bild direkt als weiterverarbeiten möchte als ByteArray ...
[byte[]]$img = $browser.GetScreenshot().AsByteArray
oder Base64 enkodiert ...
$b64 = $browser.GetScreenshot().AsBase64EncodedString

back-to-topAusführen von JavaScript-Funktionen oder eigenem JavaScript-Code


Möchte man eine Funktion oder eigenen JavaScript Code im Kontext der Seite aufrufen lässt sich dies so erreichen
 $browser.ExecuteScript('alert("Test");')  

back-to-topSuchen/Finden von bestimmten Elementen auf Webseiten


Ein wichtiges Thema ist natürlich das referenzieren von bestimmten Elementen auf Webseiten. Bevor man etwas mit einem Objekt macht muss man es erst mal anhand unterschiedlicher Kriterien finden. Dazu bietet Selenium eine große Vielfalt an Funktionen mit der sich wirklich alles effektiv finden lässt.

Hier eine kleine Übersicht jeweils für das Finden von einem oder mehreren Elementen
[OpenQA.Selenium.By]::ClassName("meineKlasse")  
[OpenQA.Selenium.By]::CssSelector(".button")  
[OpenQA.Selenium.By]::Id("btnSearch")  
[OpenQA.Selenium.By]::Linktext("Suchen")  
[OpenQA.Selenium.By]::Name("Elementname")  
[OpenQA.Selenium.By]::PartitialLinkText("TeileinesLinks")  
[OpenQA.Selenium.By]::TagName("div")  
[OpenQA.Selenium.By]::XPath("//input")  

Die statischen Funktionen sollten in der Regel für sich sprechen und man übergibt ihnen jeweils einen String von dem Element und dem Typ das man sucht. Daher fasse ich diese hier in Beispielen zusammen

$items = $browser.FindElements([OpenQA.Selenium.By]::ClassName("meineKlasse"))  
$items = $browser.FindElements([OpenQA.Selenium.By]::CssSelector(".button"))  
$item = $browser.FindElement([OpenQA.Selenium.By]::Id("btnSearch"))  
$items = $browser.FindElements([OpenQA.Selenium.By]::TagName("img"))  

Tipp: Bei der Entwicklung seiner Skripte sind die DeveloperTools der Browser (meist erreichbar mittel F12) ein fast unverzichtbares Werkzeug um die Elemente im Quellcode der Seite ausfindig zu machen. Ein pratisches Tool dabei ist das "Locate" Tool das man in den Developer Tools oben ganz links in der Ecke findet:

screenshot

Dieses klickt man an und dann fährt man mit dem Mauszeiger auf das Element auf das man sich beziehen möchte. Daraufhin wird automatisch die Stelle des Elements im Quellcode markiert. Man braucht die Elemente also nicht lanngwierig manuell suchen.

[OpenQA.Selenium.By]::XPath()

Nicht jeder wird wohl etwas mit dem Begriff XPath anfangen können. Wer aber schon mal mit XPath gearbeitet hat weiß die Selektionssprachensyntax zu schätzen.
Das sieht dann bspw. so aus:
$browser.FindElements([OpenQA.Selenium.By]::XPath('//div[@class = "teaser-question-list-row"]//span[@class = "teaser-question-autor"]')).Text
Der Filterstring sucht in dem Beispiel einen DIV Element mit dem Klassennamen teaser-question-list-row und sucht dann darauf folgend SPAN Elemente egal wie tief sie verschachtelt sind dessen Klassenname teaser-question-autor lautet. Von diesen gefundenen Elementen (sofern es welche gibt) wird dann der enthaltene Text ausgegeben. Damit lassen sich sehr effektiv verschachtelte Elemente sehr detailliert filtern.
Wer mehr über die XPath Syntax erfahren möchte kann sich z.B. auf folgender Seite schlau machen
https://www.w3schools.com/xml/xpath_syntax.asp

back-to-topEine Serie von Befehlen zusammenhängend ausführen


Oftmals müssen eine Reihe von Aktionen zusammenhängend oder auch wiederholend durchgeführt werden. Dafür bietet Selenium ein Objekt Namens Action an mit dem sich so eine Sequenz erstellen lässt und dann mit einem einzigen Befehl ausführen lässt, ähnlich wie eine Function in Powershell.
Das sieht dann bspw. so aus
# zu einer Seite navigieren
$browser.Url = 'https://administrator.de'  
$navi = $browser.Navigate()
# Irgendein Element suchen auf das später in den Actions verwiesen wird
$inputfield = $browser.FindElement([OpenQA.Selenium.By]::Id('global-search-field'))  
# Actions Objekt mit dem WebDriver Objekt als Parameter erstellen
$actions = [OpenQA.Selenium.Interactions.Actions]::new($browser)
# Erste Aktion: Text in ein Eingabefeld setzen
$actions.SendKeys($inputfield,"Testsuche")  
# Zweite Aktion: Enter Taste im Inputfeld drücken
$actions.SendKeys($inputfield,[OpenQA.Selenium.Keys]::Enter)
# Actions-Objekt bauen
$actions.Build()
# Finales Ausführen aller im Sequenzobjekt vorhandenen Aktionen
$actions.Perform()


Um noch zu verdeutlichen was das Actions-Objekt selbst an möglichen Aktionen bietet, nachfolgend eine Auflistung der verfügbaren Methoden der Klasse

    TypeName: OpenQA.Selenium.Interactions.Actions

Name                MemberType 
----                ---------- 
Build               Method     
Click               Method     
ClickAndHold        Method     
ContextClick        Method     
DoubleClick         Method     
DragAndDrop         Method     
DragAndDropToOffset Method     
GetType             Method     
KeyDown             Method     
KeyUp               Method     
MoveByOffset        Method     
MoveToElement       Method     
Perform             Method     
Release             Method     
SendKeys            Method    


back-to-topPraxis-Beispiel: Einloggen auf einer Webseite


Oftmals liegen Inhalte hinter Logins versteckt, es ist also oftmals eine der ersten Aufgaben dies abzuarbeiten. Da jeder Login anders aufgebaut ist kann es hier natürlich kein Rezept für alle Logins sein. Als Beispiel verwende hier der Einfachheit den Login in unserem Administrator Forum. Dies sähe dann etwa so aus (Funktionsfähig zum aktuellen Zeitpunkt beim Schreiben des Artikels):
# zur Login Seite navigieren
$browser.URL = 'https://administrator.de/login/'  
$navi = $browser.Navigate()
# Username, Passwort und Login Elemente referenzieren
$user = $browser.FindElement([OpenQA.Selenium.By]::Id('global-login-field'))  
$pass = $browser.FindElement([OpenQA.Selenium.By]::Id('global-login-pass'))  
$btnlogin = $browser.FindElement([OpenQA.Selenium.By]::TagName('button'))  
# Benutzername und Passwort in die Eingabefelder laden
$user.SendKeys('USERNAME')  
$pass.SendKeys('PASSWORD')  
# Login Button betätigen
$btnlogin.Click()
# Zu
$navi.GoToUrl('https://administrator.de/latest')  
# Nur fürs Beispiel 2 Sekunden warten
sleep 2
# Ausloggen
$navi.GoToUrl('https://administrator.de/members/logout.php?true')  

back-to-topPraxis-Beispiel: Extrahieren von sich wiederholenden Elementen


Um die Nutzung der Funktionen zum Suchen von Elementen zu demonstrieren und wie man damit sich wiederholende Elemente wie bspw. die hier im Forum gelisteten Posts extrahiert, nehme ich mal die Seite unter Ticker hier im Forum als Demonstrationsobjekt]

# Die "Aktuell" Seite öffnen 
$browser.Url = 'https://administrator.de/latest'  
$navi = $browser.Navigate()
# Finde alle Elemente mit dem Klassennamen **list-element-light-wrapper**
$browser.FindElements([OpenQA.Selenium.By]::ClassName('list-element-light-wrapper')) | %{  
    # ... und erzeuge für jedes dieser Elemente ein Custom-Object mit drei Eigenschaften die innerhalb des aktuellen Objekts die Details extrahieren
    [pscustomobject]@{
        Titel = $_.FindElement([OpenQA.Selenium.By]::ClassName("teaser-question-light-title")).Text  
        Link = $_.getAttribute('href')  
        Autor = $_.FindElement([OpenQA.Selenium.By]::ClassName("list-element-user-alias")).Text  
    }
}

Das Ergebnis ist dann ein Array aus Objekten das sich dann sehr gut weiterverarbeiten lässt
Titel                                                    Link                                                                                                  Autor     
-----                                                    ----                                                                                                  -----     
Word Benutzerdefinierter Druck funktioniert nicht        [content:1194406300]      ti-buh    
Brocade RSTP 802-1w berechnet sich immer neu             [content:1194372159]           alikber   
Upload nur 50 Prozent der möglichen Leistung             [content:1194161674]          supernicky
Android und Samsung Mail app - kein Empfang von CC Mails [content:1194097066] hausrocker
Microsoft Excel Namen über Batchdatei ändern             [content:1193997546]         Plxgot    
Windows Update KB5005033 lässt sich nicht installiere... https://administrator.de/forum/windows-update-kb5005033-laesst-sich-nicht-installieren-win-10-pro-... mecie21313

back-to-topAnhang


back-to-topEigenschaften welche Objekte haben die sich mit den "Find*" Funktionen referenzieren lassen


Das Ergebnis einer der oben aufgeführten Suchaktionen für Elemente auf einer Webseite ist/sind eines oder mehrere Objekte der Klasse OpenQA.Selenium.Remote.RemoteWebElement.

Übersicht welche Eigenschaften und Funktionen solche Elemente bieten:
   TypeName: OpenQA.Selenium.Remote.RemoteWebElement

Name                                 MemberType Definition                                                                                                                
----                                 ---------- ----------                                                                                                                
Clear                                Method     void Clear(), void IWebElement.Clear()                                                                                    
Click                                Method     void Click(), void IWebElement.Click()                                                                                                                                                                              
FindElement                          Method     OpenQA.Selenium.IWebElement FindElement(OpenQA.Selenium.By by), OpenQA.Selenium.IWebElement ISearchContext.FindElement(...
FindElementByClassName               Method     OpenQA.Selenium.IWebElement FindElementByClassName(string className), OpenQA.Selenium.IWebElement IFindsByClassName.Fin...
FindElementByCssSelector             Method     OpenQA.Selenium.IWebElement FindElementByCssSelector(string cssSelector), OpenQA.Selenium.IWebElement IFindsByCssSelect...
FindElementById                      Method     OpenQA.Selenium.IWebElement FindElementById(string id), OpenQA.Selenium.IWebElement IFindsById.FindElementById(string id) 
FindElementByLinkText                Method     OpenQA.Selenium.IWebElement FindElementByLinkText(string linkText), OpenQA.Selenium.IWebElement IFindsByLinkText.FindEl...
FindElementByName                    Method     OpenQA.Selenium.IWebElement FindElementByName(string name), OpenQA.Selenium.IWebElement IFindsByName.FindElementByName(...
FindElementByPartialLinkText         Method     OpenQA.Selenium.IWebElement FindElementByPartialLinkText(string partialLinkText), OpenQA.Selenium.IWebElement IFindsByP...
FindElementByTagName                 Method     OpenQA.Selenium.IWebElement FindElementByTagName(string tagName), OpenQA.Selenium.IWebElement IFindsByTagName.FindEleme...
FindElementByXPath                   Method     OpenQA.Selenium.IWebElement FindElementByXPath(string xpath), OpenQA.Selenium.IWebElement IFindsByXPath.FindElementByXP...
FindElements                         Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElements(OpenQA.Selenium.By by), Sys...
FindElementsByClassName              Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElementsByClassName(string className...
FindElementsByCssSelector            Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElementsByCssSelector(string cssSele...
FindElementsById                     Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElementsById(string id), System.Coll...
FindElementsByLinkText               Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElementsByLinkText(string linkText),...
FindElementsByName                   Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElementsByName(string name), System....
FindElementsByPartialLinkText        Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElementsByPartialLinkText(string par...
FindElementsByTagName                Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElementsByTagName(string tagName), S...
FindElementsByXPath                  Method     System.Collections.ObjectModel.ReadOnlyCollection[OpenQA.Selenium.IWebElement] FindElementsByXPath(string xpath), Syste...
GetAttribute                         Method     string GetAttribute(string attributeName), string IWebElement.GetAttribute(string attributeName)                          
GetCssValue                          Method     string GetCssValue(string propertyName), string IWebElement.GetCssValue(string propertyName)                              
GetHashCode                          Method     int GetHashCode()                                                                                                         
GetProperty                          Method     string GetProperty(string propertyName), string IWebElement.GetProperty(string propertyName)                              
GetScreenshot                        Method     OpenQA.Selenium.Screenshot GetScreenshot(), OpenQA.Selenium.Screenshot ITakesScreenshot.GetScreenshot()                                                                                                                           
SendKeys                             Method     void SendKeys(string text), void IWebElement.SendKeys(string text)                                                        
Submit                               Method     void Submit(), void IWebElement.Submit()                                                                                  
ToString                             Method     string ToString()                                                                                                         
Coordinates                          Property   OpenQA.Selenium.Interactions.Internal.ICoordinates Coordinates {get;}                                                     
Displayed                            Property   bool Displayed {get;}                                                                                                     
Enabled                              Property   bool Enabled {get;}                                                                                                       
Location                             Property   System.Drawing.Point Location {get;}                                                                                      
LocationOnScreenOnceScrolledIntoView Property   System.Drawing.Point LocationOnScreenOnceScrolledIntoView {get;}                                                          
Selected                             Property   bool Selected {get;}                                                                                                      
Size                                 Property   System.Drawing.Size Size {get;}                                                                                           
TagName                              Property   string TagName {get;}                                                                                                     
Text                                 Property   string Text {get;}                                                                                                        

Jedes Element bietet also unter anderem erneut die oben aufgeführten "FindElement(s)" Funktionen um die Suche auf die Elemente zu beschränken die sich innerhalb des/der gefundenen Elemente befinden. Weitere wichtige Funktionen sind GetAttribute() mit der sich der Inhalt von Element-Attributen wie bspw. den Link aus dem "href" Attribut zu extrahieren lässt:
$browser.FindElements([OpenQA.Selenium.By]::TagName("a")).GetAttribute('href')
Oder auch Submit() zum übermitteln von Formularinhalten um nur zwei davon zu nennen.

back-to-topWeiterführende Links



Für Powershell Neulinge ein Powershell Link-Leitfaden für Anfänger


So, das war es für's erste. Sollte jemandem noch ein Topic zum Thema ins Gedächtnis rücken das er hier behandelt haben möchte kann er das gerne in den Kommentaren kund tun. Ich werden dann wenn die Zeit es zulässt, gerne Ergänzungen im Beitrag vornehmen.

Ich wünsche euch nun viel Spaß bei den ersten Ausflügen in die Browser-Automatisierung mit Selenium.

Gruß @colinardo

back-to-topUpdates


Datum Änderung
09.10.2023 Beschreibung für das Auffinden von Objekten auf den aktuellen Stand gebracht.
09.10.2023 Ergänzung der Funktion um den -options Parameter mit dem man benutzerdefinierte Driver-Optionen (proxy usw.) mitgeben kann
09.08.2022 Aktualisierung der Funktion (Anpassung Nuget Package interner Pfad)

Content-Key: 1197173647

Url: https://administrator.de/contentid/1197173647

Printed on: December 6, 2023 at 21:12 o'clock

Member: notmyjob
notmyjob Aug 27, 2021 at 04:00:42 (UTC)
Goto Top
Hi colinardo,

Aktuell verwende ich Selenium um vorher ausgezeichnete Tests automatisch per Pipeline nach jedem Commit auszuführen, um beispielsweise zu testen ob bestimmte Funktionen im UI beschädigt wurden. Aber die Verwendung mit der Powershell sieht sehr interessant aus, danke.

Wirst du in Zukunft noch etwas auf die Lasttests mit Selenium eingehen? Aktuell verwende ich dafür eine Kombination aus k6 (simulieren von virtuellen Usern z.B. 1000 User) und Selenium (z.B. automatisches einloggen in die Anwendung und messen wie lange dies dauert).


VG
Member: colinardo
colinardo Aug 27, 2021 updated at 05:23:11 (UTC)
Goto Top
Servus @notmyjob,
könnte man machen, mal sehen ob noch in diesem Beitrag, vielleicht doch ein Thema für einen neuen.

VG
Member: lifeadmin
lifeadmin Aug 09, 2022 at 15:25:24 (UTC)
Goto Top
Hallo Colinardo, Dein Beitrag hat mir sehr geholfen, um die Drucker- und Netzwerknamen unserer Ricoh Drucker zu ändern. Bei vierhundert Druckern das über die Weboberfläche manuell zu machen, war mir nichts.
Am Anfang hatte ich Probleme die Webdriver.dll mit diesem Skript runterzuladen bzw. finden. Dann habe ich in der PS install-module Selenium ausgeführt und dort wurde sie dann mit ins PS Modulverzeichnis von Selenium gepackt.

Vielen Dank !
Member: colinardo
colinardo Aug 09, 2022 updated at 15:52:46 (UTC)
Goto Top
Servus @lifeadmin .
Danke für deine Rückmeldung.
Zitat von @lifeadmin:
Am Anfang hatte ich Probleme die Webdriver.dll mit diesem Skript runterzuladen bzw. finden. Dann habe ich in der PS install-module Selenium ausgeführt und dort wurde sie dann mit ins PS Modulverzeichnis von Selenium gepackt.
Da hat sich mittlerweile der Pfad im Nuget-Package geändert, habe das Skript oben entsprechend korrigiert, läuft hier im Test jetzt wieder.
Vielen Dank !
Immer gerne face-smile.

Grüße Uwe
Member: Herbert
Herbert Apr 18, 2023 at 20:55:49 (UTC)
Goto Top
Hallo Colinardo,
zunächst einmal vielen Dank für diesen Post. Das ist mit Abstand das Beste, was ich zu Powershell und Selenium gefunden habe. Deine Arbeit hat mich sehr vorangebracht.
Ich bearbeite gerade eine mit einem CMS generierte Website mit Selenium.
Dazu habe ich zwei Fragen:
1.)
Da habe ich zwei unmittelbar aufeinander folgende Felder (Klassen), die ich mit FindElementsByClassName kombiniert abfragen will (wenn beide einen gewissen Zustand haben, geht es weiter).
Gibt es in Selenium außer, daß man die Y-Koordinaten miteinander vergleicht, die Möglichkeit sozusagen in der Nähe eines Feldes nach dem anderen Feld zu suchen? Ich löse das z.Zt. mit zwei geschachtelten foreach-Schleifen.
2.)
Auf der Website befindet sich rechts von dem Text ein Button, mit dem man eine Unterseite aufrufen kann.
Ich finde diesen Button mit FindElementByXPath. Wenn ich dann auf diesen Button einen Click auslöse, dauert es einen Moment und dann kommt die Fehlermeldung, daß der Button 8 Pixel tiefer als im Button-Objekt steht nicht gefunden wurde. Hast Du dafür eine Erklärung. In einem anderen Skriptteil läuft dieser Click() problemlos.

Vielen Dank für Deine Hilfe
Member: colinardo
colinardo Apr 18, 2023 updated at 21:15:54 (UTC)
Goto Top
Servus Herbert.
Danke für deine Nachricht.
Zitat von @Herbert:
Dazu habe ich zwei Fragen:
1.)
Da habe ich zwei unmittelbar aufeinander folgende Felder (Klassen), die ich mit FindElementsByClassName kombiniert abfragen will (wenn beide einen gewissen Zustand haben, geht es weiter).
Gibt es in Selenium außer, daß man die Y-Koordinaten miteinander vergleicht, die Möglichkeit sozusagen in der Nähe eines Feldes nach dem anderen Feld zu suchen? Ich löse das z.Zt. mit zwei geschachtelten foreach-Schleifen.
Du könntest bspw. die Funktion FindElementByCSSSelector() nutzen und dann über einen Siblings Selector deine Elemente auswählen.
https://www.w3schools.com/css/css_combinators.asp

2.)
Auf der Website befindet sich rechts von dem Text ein Button, mit dem man eine Unterseite aufrufen kann.
Ich finde diesen Button mit FindElementByXPath. Wenn ich dann auf diesen Button einen Click auslöse, dauert es einen Moment und dann kommt die Fehlermeldung, daß der Button 8 Pixel tiefer als im Button-Objekt steht nicht gefunden wurde. Hast Du dafür eine Erklärung. In einem anderen Skriptteil läuft dieser Click() problemlos.
Ohne den Quelltext vorliegen zu haben hört sich das vermutlich nach folgendem an
Selenium ChromeDriver how to click elements with negative x,y pixel locations? C#

Hth.

Grüße Uwe
Member: Herbert
Herbert Apr 19, 2023 at 08:53:35 (UTC)
Goto Top
Hallo Uwe,
danke für die schnelle Antwort.

Die Lösung zu 1 habe ich verworfen; meine doppelte foreach-Schleife funktioniert genauso gut.

Zu 2 habe ich jetzt eine sehr merkwürdige Lösung gefunden.

Ich benutze vor der Suche nach dem richtigen Button ein scrollintoView().
Damit befindet sich mein Button (je nach Anzahl der Aktionsblöcke) häufig auf y-Position 316.
Mit dieser Position sucht er beim Click auf Position 325 und findet nichts.

Ich mache jetzt einen erneuten scrollintoView() direkt auf den Button (dann hat er y-Position 0)
Damit funktioniert der Klick problemlos (der auch funktioniert hat, wenn ich das
Fenster manuell etwas verschoben habe und den Button neu gesucht habe; nur mit
316 geht es nicht).

Zwischendurch habe ich beim Programmieren das Gefühl, man muß nicht alles auf
der Welt verstehen.

Vielen Dank für Deine Hilfe
Herbert
Member: colinardo
colinardo Apr 19, 2023 updated at 09:07:13 (UTC)
Goto Top
Zitat von @Herbert:
Die Lösung zu 1 habe ich verworfen; meine doppelte foreach-Schleife funktioniert genauso gut.
Hauptsache du selbst kommst damit klar, am Ende musst du damit arbeiten face-smile, es gibt 1000 Wege nach Rom.
Zwischendurch habe ich beim Programmieren das Gefühl, man muß nicht alles auf
der Welt verstehen.
Tja die Browserautomation sollte auch nur das aller aller allerletzte Mittel sein wenn man es nicht anders umsetzen kann. Es lohnt sich auf jeden Fall per Developer Tools im Browser (F12) den Netzwerktraffic mitzuschneiden, denn 90% der Browserautomations lassen sich auf diesem Wege besser über low level POST/GET-Requests auf die Endpoints wesentlich effizienter bedienen also das Simulieren von Klicks. Die Browserautomation ist eher was für automatisierte User-Tests von GUIs.

Grüße Uwe
Member: SarekHL
SarekHL Aug 13, 2023 at 13:22:11 (UTC)
Goto Top
Ich möchte täglich die Lübecker Nachrichten herunterladen. Leider ist die Seite dynamisch aufgebaut, das heißt, der Link zum ePaper ändert sich täglich. Heute heißt er https://epaper.ln-online.de/webreader/997286.

Das einzig konstante scheint die CSS-Klasse zu sein:
ashampoo_snap_sonntag, 13. august 2023_15h12m59s_001_

Es scheint immer der Link zu sein, der mit "edition-cover__link" definiert wird. Kann man das irgendwie abbilden?

Der nächste Schritt, wenn das ePaper aufgerufen ist, ist dann vermutlich noch schwieriger. Wo ist hier die Download-URL:
ashampoo_snap_sonntag, 13. august 2023_15h20m44s_003_
Member: colinardo
colinardo Aug 13, 2023 updated at 14:01:48 (UTC)
Goto Top
Auch kein Hallo. 🤔
Die Seite benötigt ein Zugangskonto das habe ich nicht.
Der nächste Schritt, wenn das ePaper aufgerufen ist, ist dann vermutlich noch schwieriger. Wo ist hier die Download-URL:
Wird so wie ich das hier sehe über die Javascript Funktion "toggleDownload()" getriggert. Die musst du analysieren bzw. Mit F12 den Netzwerk-Traffic beobachten.

Bei Bedarf und Kontodatenstellung für die Seite zum Testen kann ich gegen entsprechende Aufwandsentschädigung gerne passenden Code bereitstellen. => PN

Das ist nichts für eine öffentliche Diskussion in diesem Beitrag hier, das würde dem Inhaber der Seite sicherlich nicht gefallen und dagegen vorgehen!

Schönen Sonntag
Gruß @colinardo
Member: PeterGyger
PeterGyger Sep 10, 2023 at 10:58:26 (UTC)
Goto Top
Hallo Uwe

Danke das Du die Ehre der Damen ("grüssen") eisern verteidigst.

Für viele Online Printmedien hilft das Projekt 12ft.io weiter...
12ft.io/

Einen schattigen Sonntag wünschend
Member: colinardo
colinardo Sep 10, 2023 updated at 11:10:12 (UTC)
Goto Top
Servus Peter.
Zitat von @PeterGyger:
Für viele Online Printmedien hilft das Projekt 12ft.io weiter...
12ft.io/
In diesem Fall aber leider nicht, da es sich hierbei nicht um ein Security-by-Obscurity System handelt bei dem der Beitrag nur durch plumpe JavaScript-Verschlüsselung oder überlagern von HTML-Tags besteht, wie es früher öfter der Fall war.
Hier sind echte Credentials für den Zugang nötig.
Einen schattigen Sonntag wünschend
Danke, ich spring in der Pause zur Abkühlung gleich ins Meer, like every day 😉🏊
Member: PeterGyger
PeterGyger Sep 10, 2023 at 11:30:43 (UTC)
Goto Top
Hallo Uwe

Don't feed the sharks 😈🍖

Ich gehöre zur anderen Fraktion: Berge 🤩

Wenn ein Online Printmedium in einer Internetsuchmaschine gefunden werden will (Indexierung), dann kann sie es nicht verhindern. So die Erklärung in einem Artikel den ich vor Jahren las.

Last but not least:
Vielen Dank für diesen und die vielen anderen hochkarätigen Posts die Du verschenkst!

Beste Grüsse
Member: colinardo
colinardo Sep 10, 2023 updated at 11:48:57 (UTC)
Goto Top
Zitat von @PeterGyger:
Don't feed the sharks 😈🍖
😂 Die bekommen meine Finne zu spüren 🏄‍♂️.
Wenn ein Online Printmedium in einer Internetsuchmaschine gefunden werden will (Indexierung), dann kann sie es nicht verhindern. So die Erklärung in einem Artikel den ich vor Jahren las.
Doch, die Teaser der Artikel sind ja das Lockmittel und die sind frei verfügbar und von Suchmaschinen indizierbar, den kompletten Artikel kannst du aber problemlos so abschotten das ihn nur Abonnenten zu Gesicht bekommen.

Last but not least:
Vielen Dank für diesen und die vielen anderen hochkarätigen Posts die Du verschenkst!
Immer gerne.
Member: Dani
Dani Oct 09, 2023 at 14:38:33 (UTC)
Goto Top
Hallo Uwe,
ich habe ich heute angefangen meine privaten PowerShell Skripte von Chrome auf Edge umzustellen.

Bis dato nutze unter Chrome die Methode AddArguments() um z.B. meinen Proxy explizit anzugeben. Bei Edge (aktuelle Version) gibt es diese Methode nicht. Daher dachte ich mir, dass evtl. über eine Funktion an die Methode komme -> Fehlanzeige.

Bin ich blind oder ist der Edge Driver noch nicht auf dem Stand wie der Chrome Driver?


Gruß,
Dani
Member: colinardo
colinardo Oct 09, 2023 updated at 16:07:05 (UTC)
Goto Top
Servus Dani,
Optionen kannst du dem Konstruktor des Webdriver Objektes mitgeben. Habe die Funktion dazu gerade noch dahingehend ergänzt, so dass man die Options dem CMDLet als Parameter mitgeben kann.
https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/edge ...

Aufruf dann bspw. folgendermaßen für den Edge, Proxy-Optionen (HttpProxy/SSLProxy/SocksProxy/...) natürlich anpassen:
$options = [OpenQA.Selenium.Edge.EdgeOptions]@{Proxy = [OpenQA.Selenium.Proxy]@{HttpProxy = "10.20.30.44:8080"}}  
$browser = Create-Browser -browser Edge -options $options

Grüße Uwe
Member: Dani
Dani Oct 10, 2023 at 11:59:51 (UTC)
Goto Top
Hallo Uwe,
auch laut deinem Link (https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/edge ..) muss es die Methode AddArguemts() geben. Daher bin ich nochmals auf die Ursachensuche gegangen.

In meinem Fall ist für das Verhalten das PowerShell Module Selenium (Install-Module Selenium) schuld. Sobald ich diese nicht mehr automatisch lade bzw. deinstalliere, steht die Methode AddArugments wieder zur Verfügung. Vermutlich ist es auch dem Umstand geschuldet, dass auf Powershell Gallery die Version 3.0.1 bereitgestellt ist. Hingegen auf NuGut bereits Version 4.14.x.

$item = $browser.FindElement([OpenQA.Selenium.By]::Id("btnSearch"))
Bei der Variable $item fehlt am Ende ein "s".

Load-NugetAssembly "https://www.nuget.org/api/v2/package/Selenium.WebDriver.MSEdgeDriver/$driverversion" -name 'msedgedriver.exe' -zipinternalpath 'driver/win64/msedgedriver.exe' -downloadonly -EA Stop
Den Namen von "msedgedriver.exe" in "MicrosoftWebDriver.exe" ändern. Anderenfalls kommt bei mir eine Fehlermeldung.

Load-NugetAssembly 'https://www.nuget.org/api/v2/package/Selenium.WebDriver/4.11.0' -name 'WebDriver.dll' -zipinternalpath 'lib/net45/WebDriver.dll' -EA Stop
Ist es Absicht, dass du eine Version im Downloadlink verankert hast. Alternativ die Versionsnummer weglassen. Damit wird automatisch die neuste Version heruntergeladen.


Gruß,
Dani
Member: colinardo
colinardo Oct 10, 2023 updated at 14:27:41 (UTC)
Goto Top
Zitat von @Dani:
$item = $browser.FindElement([OpenQA.Selenium.By]::Id("btnSearch"))
Bei der Variable $item fehlt am Ende ein "s".
Das war Absicht, FindElement ohne "s" liefert nur ein einzelnes Element daher habe ich die Variable in der Einzahl benannt $item ohne s face-smile.

Load-NugetAssembly "https://www.nuget.org/api/v2/package/Selenium.WebDriver.MSEdgeDriver/$driverversion" -name 'msedgedriver.exe' -zipinternalpath 'driver/win64/msedgedriver.exe' -downloadonly -EA Stop
Den Namen von "msedgedriver.exe" in "MicrosoftWebDriver.exe" ändern. Anderenfalls kommt bei mir eine Fehlermeldung.
Der Name ist schon OK. Funktioniert hier im Test auch einwandfrei mit aktuellem Edge unter Windows 11 ohne das es extern umbenannt werden müsste.

Man darf hier nicht mit Selenium-Modul und DLL mixen, sonst gibt es Durcheinander. Die Funktion ist rein ohne extra zu installierndes Selenium-Modul gedacht.

Load-NugetAssembly 'https://www.nuget.org/api/v2/package/Selenium.WebDriver/4.11.0' -name 'WebDriver.dll' -zipinternalpath 'lib/net45/WebDriver.dll' -EA Stop
Ist es Absicht, dass du eine Version im Downloadlink verankert hast. Alternativ die Versionsnummer weglassen. Damit wird automatisch die neuste Version heruntergeladen.
Ja das war Absicht, die Version hatte noch keine Abhängigkeiten deswegen hatte ich die Version genommen, werde das irgendwann noch anpassen wurde angepasst.

Grüße Uwe
Member: Dani
Dani Dec 02, 2023 at 10:28:35 (UTC)
Goto Top
Moin Uwe,
Man darf hier nicht mit Selenium-Modul und DLL mixen, sonst gibt es Durcheinander. Die Funktion ist rein ohne extra zu installierndes Selenium-Modul gedacht.
Danke für den Denkanstoß. Das war mir so nicht bewusst.

Ja das war Absicht, die Version hatte noch keine Abhängigkeiten deswegen hatte ich die Version genommen
Super, danke dir.


Gruß,
Dani