Datenpulen mit PowerShell: HTML-Dateien bearbeiten
Daten aus einer HTML-Datei herauszupulen ist meistens sehr fehleranfällig und sollte immer nur Plan C sein. Die Funktion Get-MarkupTag von James Brundage macht es aber etwas leichter. Zu Get-MarkupTag
gehört auch noch Get-Web, um den Quelltext der Seite herunterzuladen, bevor man ihn mit Get-MarkupTag
parst. Da Get-Web
ein relativ kurzes Skript ist, das hauptsächlich um das .NET-Objekt Net.Webclient
gebaut wurde, kann man es sich auch leicht selber basteln. Das Skript Get-Web.ps1
von Brundage habe ich aber noch um folgende Zeilen ergänzt,
$webclient.Encoding = [System.Text.Encoding]::UTF8 # set UTF8 encoding $webclient.UseDefaultCredentials = $true # Default Credentials $webclient.Proxy.Credentials = $webclient.Credentials # Proxy Credentials |
damit die Umlaute von Websiten richtig dargestellt werden und damit sich das Skript mit den Anmeldeinformationen des gegenwärtig angemeldeten Benutzers authentifiziert, wenn der Server das verlangt.
Bei meiner Arbeit benutze ich Get-MarkupTag
um Informationen wie Zählerstände, Tonerstatus, Fehlermeldungen etc. von Netzwerkdruckern auf deren eigenen HTML-Seiten auszulesen, und um Daten aus SharePoint-Seiten herauszufiltern. Was ich bislang noch nicht genutzt habe, ist die Möglichkeit, die HTML-Ausgabe eines Cmdlts nach XML zu parsen, da mir dafür noch kein Anwendungsfall untergekommen ist:
$text = Get-ChildItem | Select Name, LastWriteTime | ConvertTo-HTML | Out-String Get-MarkupTag "tr" $text |
Das sind aber alles Skripte, die ich hier schlecht als Beispiele zeigen kann, um Get-MarkupTag
vorzustellen. Daher habe ich das kleine Cmdlet Get-Wetter
geschrieben, das die aktuelle Wetterlage in der PowerShell anzeigt. Die Daten dafür bekommt es von der Website Wetter.de.
Zum Starten lädt man den Quelltext der Website herunter, wofür man das Skript Get-Web.ps1
nimmt, oder man schreibt sich ein vergleichbares. Ich benutze hier Get-Web
:
$url = "http://www.wetter.de/deutschland/wetter-hannover-18219670/wetterbericht-aktuell.html" $html = Get-Web $url |
Die URL zeigt das aktuelle Wetter von Hannover an. Wen das nicht interessiert, sollte eine andere URL nehmen. Der Quelltext der Seite wird dann in die Variable $html
geschrieben. Um zu wissen, welche Daten man aus der Website herausziehen will, sollte man schon einen guten Überblick über die HTML-Syntax und den Aufbau der Seite haben. Dann kann man zum Beispiel schnell erkennen, dass ein paar sehr schöne Informationen zum Wetter im Tooltip einiger IMG-Tags stehen.
Also parse ich alle IMG-Tags des HTML-Dokuments nach XML, die das Attribut class="iconWeather tooltip"
enthalten:
Get-MarkupTag img $html | Where-Object { $_.Tag -like '*class="iconWeather tooltip"*' } | % { $_.Xml } src : http://cdn.static-fra.de/wetter11/css/images/icons.wetter.01/64x64.wetter/wolke_klein-sonne.png width : 64 height : 64 border : 0 alt : wetter-wolke_klein-sonne title : Wetter#@#Es ist sonnig, nur vorübergehend entstehen Quellwolken oder es ziehen lockere Wolkenfelder vorüber. Dabei bleibt es weitgehend trocken. class : iconWeather tooltip src : http://cdn.static-fra.de/wetter11/css/images/icons.wetter.01/64x64.wind/w.png width : 64 height : 64 border : 0 alt : wind-West title : Wind#@# Der Wind weht aus Westen class : iconWeather tooltip |
Wie man sehen kann, stehen die Tooltipps in den Title-Attributen der IMG-Tags, die ich auslesen möchte, z.B. „Wind#@# Der Wind weht aus Westen“. Deshalb werden auch nur die Title-Informationen geparst. Davon interessiert mich nur der zweite Teil des Strings, also alles was nach der Zeichenkette „#@#“ steht:
$out = Get-MarkupTag img $html | Where-Object { $_.Tag -like '*class="iconWeather tooltip"*' } | % { $_.Xml.title } $wetter = ($out[0].substring( ($out[0].lastIndexOf( "#@#" )) + 3)).trim() $wind = ($out[1].substring( ($out[1].lastIndexOf( "#@#" )) + 3)).trim() |
Anstelle von Substring
mit LastIndexOf
könnte man auch die Methode Split
aus der Klasse String
nehmen, ersteres soll aber ressourcenschonender sein.
Nachdem ich die Beschreibung von Wetter und Wind in Variablen verstaut habe, geht es um das Regenrisiko und die Sonnenstunden, die sich in einer Tabelle befinden:
<table> <tr> <td class="description">Regenrisiko</td> <td><span class="bold">2%</span></td> </tr> <tr> <td class="description">Sonnenstunden</td> <td><span class="bold">9</span></td> </tr> </table> |
Da es mehrere Tabellen im HTML-Quelltext gibt, sich die von mir gesuchte Information glücklicherweise in der einzigen Tabelle befindet, die keine zusätzlichen Attribute hat, parse ich also nur den Inhalt der Tabelle, die mit einem <table>
beginnt und speicher das Ergebnis aus den Tags, die ich mit Select
erweitere, in die Variable $tbl
:
$tbl = Get-MarkupTag table $html | Where-Object { $_.Tag -like '<table>*' } | Select-Object -expandProperty Tag |
Wird die Tabelle von Get-MarkupTag
verarbeitet, scheint aber etwas zu fehlen:
Get-MarkupTag "td" $tbl | % { $_.Xml } class #text ----- ----- description Regenrisiko description Sonnenstunden |
Die Werte von „Regenrisiko“ und „Sonnenstunden“ stehen zwischen SPAN-Tags und werden daher nicht berücksichtigt. Get-MarkupTag
hat Besonderheiten bei Tags wie SPAN und DIV, doch dazu später mehr. Damit auch die Werte dort stehen, braucht man einen kleinen Trick: Die SPAN-Tags werden mit dem Class-Attribut „value“ des TD-Tags getauscht:
$tbl = Get-MarkupTag table $html | Where-Object { $_.Tag -like '<table>*' } | Select-Object -expandProperty Tag $tbl = $tbl -replace '><span class="bold"', ' class="value"' $tbl = $tbl -replace '</span>', '' Get-MarkupTag "td" $tbl | % { $_.Xml } class #text ----- ----- description Regenrisiko value 2% description Sonnenstunden value 9 |
Im Skript selbst speichere ich alles in der Spalte „#text“ in der Listen-Variable $regen
. PowerShell macht automatisch eine Liste aus den Werten, ohne dass ich etwas dafür tun muss.
$regen = Get-MarkupTag "td" $tbl | % { $_.Xml.'#text' } |
Die Info über die Temperatur, also wie warm oder wie kalt es heute wird, liegt zwar eingebettet zwischen SPAN- und DIV-Tags, sie können aber mit Get-MarkupTag
nur schwer geparst werden. Das Cmdlet prüft nämlich die Anzahl der Start- und Endtags im Dokument, sind sie ungrade, nimmt es nur die Starttags und beachtet die Endtags nicht. Solche Tags werden dann wie nicht geschlossene Tags behandelt, also wie IMG- oder das BR-Tag, und in sich geschlossen:
Get-MarkupTag div $html | Select-Object -expandProperty Tag […] <div class="forecast-day-container light no-overview"/> <div class="forecast-date"/> <div class="forecast-day-overlay"/> <div class="overlay-container-left"/> <div class="forecast-day-temperature tooltip" title="Temperatur#@#MIN / MAX"/> <div class="forecast-day-additional"/> […] |
Im Fall von DIV- und SPAN-Tags scheint Get-MarkupTag
ungleiche Werte im HTML-Dokument zu ermitteln, weshalb sie als leere Elemente behandelt und die Starttag geschlossen werden. Bleibt also für die Suche nach den Gradzahlen im Dokument nur der alte, herkömmliche Weg mit einem Regulärer Ausdruck (RegEx):
$Temp = ([regex]‘.\d{1,2}°C’).matches($html) | foreach {$_.Groups[0].Value} $TempMin = ($Temp[0]).trim() $TempMax = ($Temp[1]).trim() |
Da der Reguläre Ausdruck ‘.\d{1,2}°C’
mehrfach vorkommt, gruppiere ich das Ergebnis und lese die Werte einzeln aus dem durch die Gruppierung gelieferten Array wieder aus. Gesucht wird eine Zahl von 0 – 9 (\d
), die eine oder 2 Stellen hat ({1,2}
), sollte davor exakt ein Zeichen (.
) stehen, wird das auch mitgenommen, z.B. ein Minus-Zeichen bei Minus-Temparaturen, und am Ende der Zahl muss °C
stehen. Falls Leerzeichen am Anfang oder am Ende der Zeichenkette stehen, werden sie mit der String.Trim
-Methode abgeschnitten.
Das vollständige Skript bindet die Cmdlets Get-Web
und Get-MarkupTag
als Skripte ein. Alternativ kann man sie natürlich auch direkt mit in das Skript aufnehmen:
Set-StrictMode -Version "2.0" Clear-Host $Scriptpath = "$env:USERPROFILE\Powershell" . $Scriptpath\Get-Web.ps1 . $Scriptpath\Get-MarkupTag.ps1 function Get-Wetter([string]$url){ $html = Get-Web $url # Get IMG-Tag title $out = Get-MarkupTag img $html | Where-Object { $_.Tag -like '*class="iconWeather tooltip"*' } | % { $_.Xml.title } $wetter = ($out[0].substring( ($out[0].lastIndexOf( "#@#" )) + 3)).trim() $wind = ($out[1].substring( ($out[1].lastIndexOf( "#@#" )) + 3)).trim() # Get table $tbl = Get-MarkupTag table $html | Where-Object { $_.Tag -like '<table>*' } | Select-Object -expandProperty Tag $tbl = $tbl -replace '><span class="bold"', ' class="value"' $tbl = $tbl -replace '</span>', '' $regen = Get-MarkupTag "td" $tbl | % { $_.Xml.'#text' } $Temp = ([regex]‘.\d{1,2}°C’).matches($html) | foreach {$_.Groups[0].Value} $TempMin = ($Temp[0]).trim() $TempMax = ($Temp[1]).trim() # create list $list = @() $list += "Wetter" $list += $wetter $list += "Wind" $list += $wind $list += "Min" $list += $TempMin $list += "Max" $list += $TempMax $list += $regen $hash = @{} ; while ($list) { $key, $value, $list = $list; $hash[$key]=$value } $hash } # End Get-Wetter $res = Get-Wetter "http://www.wetter.de/deutschland/wetter-hannover-18219670/wetterbericht-aktuell.html" echo "$($res.Wetter)`n$($res.Wind)." echo "Die Temparatur beträgt mind. $($res.Min) und höchstens $($res.Max).`nDas Regenrisiko liegt bei $($res.Regenrisiko) und die Sonne scheint etwa $($res.Sonnenstunden) Stunden." |
Alle ermittelten Variablen landen schließlich in der Liste mit dem originellem Namen $list
. Anschließend werden die Elemente der Liste in eine hash table „verschoben“ und als Hash zurückgegeben.
Statt der Ausgabe als Hash gebe ich das Wetter am Ende noch schön formatiert in der Shell aus.
Geschrieben in Powershell