Die Grundidee für den Scatter-Baustein besteht darin, ein und denselben Enumerator in den beiden Output-Events zu verwenden. Und natürlich werden die beiden Output-Events auf je einem eigenen Thread ausgelöst.
Normalerweise wird beim Iterieren mit dem foreach-Sprachkonstrukt gearbeitet:
var ints = new[] { 1, 2, 3, 4 }; foreach(var i in ints) {...}
Hinter den Kulissen wird dies vom C#- Compiler in Aufrufe übersetzt, die in IEnumerable und IEnumerator definiert sind:
var enumerator = ints.GetEnumerator(); while(enumerator.MoveNext()) { var i = enumerator.Current; // Mache etwas mit i }
Wichtig ist hier festzuhalten, dass für die Schleife mit GetEnumerator eine neue Instanz eines Enumerators erzeugt wird. Jeder Aufruf von GetEnumerator liefert eine neue Instanz eines Enumerators! Insofern wäre es beim Scatter-Baustein schwierig, in jedem der beiden Ausgangsevents mit foreach über die Eingangsdaten zu iterieren, weil dann zwei Enumeratoren im Spiel wären. Beide würden jeweils von vorn nach hinten über die Eingangsdaten iterieren. Jedes Element der Eingangsdaten soll aber nur genau einmal an einem der beiden Ausgänge anstehen. Das wird erreicht, indem beide Ausgänge dieselbe Instanz des Enumerators verwenden, siehe Listing 11.
public class Scatter<T> { public void Process(IEnumerable<T> input) { var enumerator = input.GetEnumerator(); var thread1 = new Thread(() => Output1( GenerateOutput(enumerator))); var thread2 = new Thread(() => Output2( GenerateOutput(enumerator))); thread1.Start(); thread2.Start(); } private IEnumerable<T> GenerateOutput( IEnumerator<T> enumerator) { while (enumerator.MoveNext()) { yield return enumerator.Current; } } public event Action<IEnumerable<T>> Output1; public event Action<IEnumerable<T>> Output2; }
Der Enumerator wird einmal mit Get- Enumerator instanziert und dann beide Male an die Methode GenerateOutput übergeben. Diese iteriert mithilfe des Enumerators über die Eingangsdaten und liefert sie mit yield return als neue Aufzählung zurück. Auf diese Weise werden die einzelnen Elemente der Eingangsdaten nur jeweils einmal an einen der beiden Ausgänge verteilt.
Listing 12 zeigt die Tests für den Scatter- Baustein.
[TestFixture] public class ScatterTests { private Scatter<int> sut; private IEnumerable<int> result1; private IEnumerable<int> result2; [SetUp] public void Setup() { sut = new Scatter<int>(); sut.Output1 += x => result1 = x; sut.Output2 += x => result2 = x; } [Test] public void Jedes_Element_steht_genau_ einmal_an_einem_der_Ausgänge() { sut.Process(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); Assert.That(result1.Union(result2), Is.EquivalentTo(new[] {1,2,3,4,5,6,7,8,9,10})); } [Test] public void Die_beiden_Ausgänge_laufen_ nicht_auf_dem_Haupthread() { var mainThread = Thread.CurrentThread; sut.Output1 += _ => Assert.That(Thread.CurrentThread, Is.Not.SameAs(mainThread)); sut.Output2 += _ => Assert.That(Thread.CurrentThread, Is.Not.SameAs(mainThread)); sut.Process(new[]{1}); } [Test] public void Die_beiden_Ausgänge_laufen_ auf_unterschiedlichen_Threads() { Thread thread1 = null; Thread thread2 = null; sut.Output1 += _ => thread1 = Thread.CurrentThread; sut.Output2 += _ => thread2 = Thread.CurrentThread; sut.Process(new[]{1}); Assert.That(thread1, Is.Not.SameAs(thread2)); } }
Der erste Test überprüft, ob die beiden Aufzählungen, die an den Ausgängen des Scatter-Bausteins gebildet werden, zusammengenommen der Aufzählung des Eingangs entsprechen. Da es durch die Ausführung auf mehreren Threads zu Veränderungen der Reihenfolge kommen kann, erfolgt die überprüfung mit Is.EquivalentTo. Die beiden anderen Tests prüfen, ob die Ausgänge tatsächlich auf einem anderen als dem Mainthread ablaufen und ob wirklich beide Ausgänge einen eigenen Thread erhalten.
Aufgabe des Gather-Bausteins ist es, die Aufzählungen, die an den beiden Eingängen anstehen, zu einer Aufzählung für den Ausgang zusammenzufassen. Dabei wird der Ausgang auf einem eigenen Thread ausgeführt, damit der Baustein nicht blockiert. Bei der Implementation habe ich zunächst auf eine ConcurrentQueue und einen AutoResetEvent gesetzt. Doch dabei kam es sporadisch dazu, dass der Baustein blockierte. Die Synchronisierung der beiden Eingänge ist nicht so trivial, wie es zunächst den Anschein haben mag. Doch das .NET Framework hält eine weitere Datenstruktur bereit, die in diesem Fall die Lösung bedeutete: eine BlockingCollection. Die bewerkstelligt genau, was ich gebraucht habe: Auf der Eingangsseite sollen Daten in die BlockingCollection ergänzt werden können. Mit der Add-Methode ist das kein Problem. Und da diese Datenstruktur threadsicher ist, können auch beide Eingänge asynchron Einträge ergänzen, ohne dass es zu Problemen kommt.
Auf der Ausgangsseite liegt die Herausforderung. Hier können natürlich nur Elemente entnommen werden, wenn welche vorhanden sind. Wenn kein Eintrag vorhanden ist, muss der Thread, auf dem der Ausgangsevent des Gather-Bausteins läuft, angehalten werden. Erst wenn auf der Eingangsseite wieder ein Element ergänzt wird, soll der Thread weiterlaufen. Thread.Sleep kommt nicht infrage, so viel dürfte inzwischen klar sein. Ursprünglich hatte ich hier den AutoResetEvent verwendet, um auf neue Daten zu warten. Doch die BlockingCollection macht genau, was ich brauche: Sie blockiert, wenn keine Daten anliegen. Man kann sie daher gefahrlos iterieren. Im MoveNext des zugrunde liegenden Enumerators wird der Thread so lange angehalten, bis Daten anliegen. Damit sieht meine Implementation des Gather-Bausteins so wie in Listing 13 aus.
public class Gather<T> { private bool resultWasRaised; private readonly BlockingCollection<T> queue = new BlockingCollection<T>(); private bool intpu1IsEmpty; private bool intpu2IsEmpty; public void Input1(IEnumerable<T> input) { RaiseResultEvent(); Iterate(input); intpu1IsEmpty = true; if(intpu1IsEmpty && intpu2IsEmpty) { queue.CompleteAdding(); } } } public void Input2(IEnumerable<T> input) { RaiseResultEvent(); Iterate(input); intpu2IsEmpty = true; if (intpu1IsEmpty && intpu2IsEmpty) { queue.CompleteAdding(); } } private void Iterate(IEnumerable<T> input) { foreach (var t in input) { queue.Add(t); } } private void RaiseResultEvent() { if (resultWasRaised) { return; } resultWasRaised = true; var thread = new Thread(x => Result(EnumerateTheQueue())); thread.Start(); } private IEnumerable<T> EnumerateTheQueue() { foreach (var item in queue.GetConsumingEnumerable()) { yield return item; } } public event Action<IEnumerable<T>> Result; }
Es sei gleich erwähnt, dass die Implementation einen Haken hat: Der Flow kann nicht mehrfach durch den Gather- Baustein fließen. Die beiden Inputs können beide nur genau einmal aufgerufen werden. Beim zweiten Aufruf ist die BlockingCollection im Zustand AddingCompleted. Die Lösung dieses Problems ist deutlich aufwendiger, als es zunächst erscheinen mag. Prinzipiell müsste die BlockingCollection für jeden neuen Satz von Eingangsdaten erzeugt werden. Doch die Input1- und Input2-Aufrufe müssten korreliert werden: Es muss erkannt werden, welche Aufrufe zusammengehören. Wenn beispielsweise Input1 erneut aufgerufen wird, darf die BlockingCollection erst neu angelegt werden, wenn die bisherige abgearbeitet ist.
Das gleiche Problem stellt sich übrigens auch bei der asynchronen Verwendung des Join-Bausteins aus dem ebclang-Projekt [1]. Auch hier muss bei asynchroner Verwendung eine Korrelation und eine Eingangswarteschlange für die Inputs verwendet werden.
Diese Herausforderung geht deutlich über den Rahmen einer übung hinaus. Nichtsdestoweniger mag sich der geneigte Leser an dieser Herausforderung versuchen. Die asynchrone Programmierung wird mit steigender Zahl von Prozessorkernen einen größeren Stellenwert einnehmen. Da lohnt es, sich beizeiten damit zu befassen.
Die ersten Schritte in Richtung Parallelisierung, Multithreading und Asynchronizität sind schnell getan. Dank leistungsfähiger Datenstrukturen im .NET Framework müssen nicht alle Probleme selbst gelöst werden. Doch es bleiben auch zahlreiche Herausforderungen offen, die man als Softwareentwickler beherrschen sollte. Auch wenn das Ziel darin besteht, die Asynchronizität im Sinne eines Aspekts aus der Kernlogik einer Anwendung herauszuhalten, muss man sich damit auseinandersetzen. In den nächsten Ausgaben der dotnetpro werden Sie Weiteres zu diesem Thema lesen können.
[ml]
Viele Websites bieten ihre Daten und Services auch über eine Programmierschnittstelle (API) an. Wenn eine solche Schnittstelle nicht existiert, bleibt als Ausweg nur Screen-Scraping.
Die Idee beim Scraping besteht darin, die Webanwendung wie ein Browser über HTTP anzusprechen. Aus dem gelieferten HTML-Datenstrom liest man die benötigten Informationen heraus. Das Verfahren wird allgemein als Screen-Scraping bezeichnet. Ich kann mich noch an die „Green Screen“-Zeiten erinnern. Da hingen beispielsweise IBM-5250-Terminals an einer AS/400. Als die Windows-PCs in Mode kamen, konnte man 5250-Emulatoren unter Windows einsetzen, um aus dem 5250-Terminaldatenstrom Informationen herauszulesen. So wurde eine Bedienung der AS/400-Anwendungen mit der Maus unter Windows möglich. Über die Sinnhaftigkeit lässt sich streiten.
Heute spielen Terminals keine Rolle mehr. Aber eine vergleichbare Aufgabenstellung ergibt sich häufig für Webanwendungen. Existiert kein API, über das die Webanwendung angesprochen werden kann, bleibt nur der Ausweg über HTTP und HTML. Ein konkretes Beispiel, an dem ich zurzeit arbeite: Die Kölner Verkehrsbetriebe (KVB) stellen die aktuellen Abfahrtszeiten von Bussen und Bahnen zu fast allen Haltestellen im Web zur Verfügung [1]. An den Haltestellen befinden sich QR-Codes, die beim Scannen auf die jeweilige Webseite der Haltestelle führen, siehe Abbildung 1.
[Abb. 1]
Der KVB-Fahrplan im Browser des iPhones ..
Das ist eine tolle Sache! Allerdings habe ich mir eine App für mein iPhone gewünscht, in der ich die Haltestelle auswählen kann und dann die gleichen Informationen angezeigt bekomme. Eine solche App gab es nicht, also musste ich sie mir selbst bauen, siehe Abbildung 2.
[Abb. 2]
... und als native iPhone-App.
Die Daten werden von den KVB zurzeit nur in Form von HTML zur Verfügung gestellt, also blieb mir nichts anderes übrig, als die Daten aus dem HTML-Dokument auszulesen.
Glücklicherweise muss man für Web-Scraping nicht alles selbst bauen. Es gibt die Open-Source-.NET-Bibliothek Html Agility Pack [2]. Damit ist das Auslesen einzelner HTML-Elemente sehr komfortabel über XPath-Ausdrücke möglich. Und mit dem WebClient aus dem .NET Framework kann der HTML-Datenstrom besorgt werden. Das funktioniert auch asynchron.
Die Übung für diesen Monat besteht darin, mittels WebClient und Html Agility Pack eine Anwendung zu bauen, die asynchron Daten aus dem Web lädt und diese auf der Konsole oder in einem GUI anzeigt. Was Sie als Datenquelle nehmen, bleibt Ihnen überlassen. Folgende Beispiele mögen Ihnen als Anregung dienen:
Suchen eines Buches bei Amazon über den Titel oder die ISBN, um herauszufinden, ob es das Buch als E-Book für den Kindle gibt [3].
Anzeigen der aktuellen Abfahrten der Bahn AG an einem einzugebenden Bahnhof [4].
Anzeigen der aktuellen Abfahrtszeiten an einer KVB-Haltestelle [5].
Das Laden der HTML-Daten sollte in jedem Fall asynchron erfolgen. Andernfalls friert die Benutzerschnittstelle ein, das ist heutzutage nicht mehr zu entschuldigen. Experimentieren Sie mit der Frage, ob die HTML-Daten mit dem Html Agility Pack im Hintergrund verarbeitet werden sollten oder ob dies im Hauptthread passieren kann. Happy Scraping!
[1] KVB, Haltestellensuche, http://www.kvb-koeln.de/german/hst/search/ [2] Html Agility Pack, http://htmlagilitypack.codeplex.com/ [3] http://www.amazon.de [4] Deutsche Bahn, http://reiseauskunft.bahn.de/bin/bhftafel.exe/ [5] KVB, Haltestelle Kölner Dom, http://www.kvb-koeln.de/qr/8
Den Text im Browser kann man lesen – oder in eine eigene Applikation einfließen lassen. Mit Screen Scraping fischen Sie sich die gesuchten Angaben aus dem HTML-Code.
creen Sraping verwendete man in den 1990er Jahren, um Hostanwendungen unter Windows mit der Maus bedienbar zu machen. In der Aufgabenstellung skizzierte ich dazu das Beispiel der IBM-5250-Terminals. Aus dem Datenstrom, der eigentlich auf einem Terminal angezeigt werden sollte, wurden Textpassagen ermittelt, die auf eine Tastenkombination für einen Befehl hindeuteten. Stand dort beispielsweise F5: Neu, dann wurde dieser Text als Kommando interpretiert. In der Windows-Oberfläche wurde an dieser Stelle ein Button angezeigt, sodass die Anwendung nun zusätzlich zur Tastatur auch mit der Maus bedienbar wurde.
Das mag manchem Leser als Frickellösung anmuten. Immerhin haben diese Terminalemulatoren seinerzeit den Weg geebnet für grafische Benutzerschnittstellen im Frontend in Verbindung mit Hostanwendungen im Backend. Heute wird man auf dem Host ein geeignetes API anbieten und das Frontend in Java oder .NET entwickeln. Solange ein solches API jedoch nicht zur Verfügung steht, ist Screen Scraping eine Lösung, um an die benötigte Funktionalität eines anderen Rechnersystems heranzukommen. Das gilt heute für Webanwendungen gleichermaßen.
Bei Webanwendungen erfolgt das Scraping, das Herauskratzen der Daten, auf der Basis der HTML-Dokumente. Eine Anwendung, die an eine bestehende Webanwendung angebunden werden soll, lädt HTML-Dokumente ganz normal über HTTP-Requests. Für die Webanwendung unterscheidet sich dieser Request nicht von einem Request, der von einem Browser stammt. Somit ist an der Webanwendung keine Änderung oder Ergänzung erforderlich. Diesen Vorteil kann man kaum genügend betonen. Gerade bei einer Anbindung an fremde Systeme, über die man selbst keine Kontrolle hat, geht häufig gar kein Weg daran vorbei, über die HTML-Daten auf die Anwendung zuzugreifen. Zeigt sich später im Betrieb, dass ein API einen Vorteil bringen würde, kann dieses immer noch ergänzt werden. Für die ersten Iterationen ist der Zugriff über HTML ausreichend.
Auch das Beispiel der Kölner Verkehrsbetriebe KVB habe ich in der Aufgabenstellung bereits beschrieben. Seit einigen Wochen bietet die KVB die Live-Abfahrtzeiten der Haltestellen im Web an. Ein von außen zugängliches API existiert nicht. Mein Wunsch, eine iPhone-App zu schreiben, wäre nicht realisierbar gewesen, wenn ich auf ein API der KVB gewartet hätte. So entschloss ich mich, die Daten direkt aus HTML auszulesen. Das funktioniert einwandfrei und ist ausreichend schnell. Insofern besteht zunächst gar keine Notwendigkeit für ein API.
Selbst mit einem API muss man die gelieferten Daten in irgendeiner Form parsen. Durch Bereitstellung der Daten in Form von XML oder JSON wäre der reine Datentransfer über das Internet möglicherweise einen Tick schneller. Und vielleicht wäre auch das Parsen etwas schneller. Aber die Geschwindigkeit ist bei meiner auf HTML basierenden Lösung schon vollkommen ausreichend.
Ein gesondertes API würde sogar ein Risiko bergen: Solange eine App auf die HTML-Daten zugreift, verwendet sie exakt dieselben Daten wie Clients, die die Abfahrtzeiten per Browser anzeigen. Somit kann sich kein App-Anwender „beschweren“, er würde in der App andere Abfahrtzeiten sehen als im Browser, da beide die exakt gleiche Datenquelle verwenden. Zwar könnte ein API natürlich ebenfalls dafür sorgen, dass sie die gleichen Daten bereitstellt, aber die Komplexität nähme zu und damit das Risiko, unterschiedliche Datenstände zu liefern.
Nun aber genug der Werbung für HTML-Scraping. Wie geht’s denn nun? Zunächst müssen zwei Teilaufgaben unterschieden werden:
Beschaffen der HTML-Daten über das HTTP-Protokoll mithilfe der WebClient-Klasse aus dem .NET Framework oder Ähnlichem.
Parsen des HTML-Dokuments und Auslesen der benötigten Daten.
Für das Parsen habe ich das Html Agility Pack [1] verwendet. Das bietet den Vorteil, dass die Daten mittels XPath-Ausdrücken recht komfortabel aus dem HTML-Dokument ausgelesen werden können.
Das Lesen der HTML-Daten habe ich mit dem WebClient aus dem .NET Framework erledigt. Im ersten Schritt habe ich die Daten sogar synchron gelesen. Natürlich ist mir klar, dass dies in einer realen Anwendung keine gute Idee ist. Daher werde ich das in einem späteren Schritt umstellen. Zunächst ging es mir jedoch darum herauszufinden, ob der Aufbau des HTML-Dokuments überhaupt geeignet ist, die gewünschten Daten herauszulesen. Denn dazu bedarf es schon etwas Struktur.
Mein erster Schritt ist daher ein Spike, mit dem ich herausfinden möchte, ob sich die benötigten Daten auf diese Weise ermitteln lassen. Dazu musste ich zunächst herausfinden, unter welchem URL die Daten bereitgestellt werden. Für die Abfahrtzeiten der einzelnen Haltestellen existiert je ein eigener URL. Alle Haltestellen sind durchnummeriert und diese Nummer wird in dem URL verwendet: "http://www.kvb-koeln.de/qr/267/"
Dies ist beispielsweise der URL für die Haltestelle Feltenstraße. Dass hier qr im URL auftaucht, hängt damit zusammen, dass die KVB an den Haltestellen QR-Codes auf die Fahrpläne gedruckt hat. Diese kann man mit seinem Smartphone scannen und landet dann auf dem URL der jeweiligen Haltestelle.
Bevor ich begonnen habe, Code zu schreiben, habe ich mir die HTML-Seite erst einmal im Quelltext angeschaut. Um Daten herauszulesen, muss das HTML-Dokument über eine Struktur verfügen. Schließlich muss zum Auslesen der Abfahrtszeiten die richtige Stelle im HTML-Dokument gefunden werden. Glücklicherweise ist seit dem Einsatz von CSS dafür gesorgt, dass Webdesigner sich über die Struktur Gedanken machen und einzelne Elemente auch benennen. Im Fall der Abfahrtzeiten hätte die Struktur zwar etwas komfortabler ausfallen können, aber es genügt, um die Zeiten auszulesen. Listing 1 zeigt einen Ausschnitt aus der HTML-Datei.
<table width="100%" cellpadding="2" cellspacing="0" class="qr_table"> <tr style="background-color: #ededed;"> <td class="li_nr qr_td" style="border-bottom: 1px solid #ffffff; text-align: right;" valign="top"> 15 </td> <td class="qr_td" style="border-bottom: 1px solid #ffffff;"> Longerich </td> <td class="qr_td" style="text-align: right; border-bottom: 1px solid #ffffff;"> 6 Min </td> </tr> <tr style="background-color: #ffffff;"> <td class="li_nr qr_td" style="border-bottom: 1px solid #ffffff; text-align: right;" valign="top"> 139 </td> <td class="qr_td" style="border-bottom: 1px solid #ffffff;"> Bickendorf </td> <td class="qr_td" style="text-align: right; border-bottom: 1px solid #ffffff;"> 8 Min </td></tr>....</table>
Man erkennt fast den Wald vor lauter Bäumen nicht. Blendet man jedoch das Rauschen aus, das für die Formatierung erforderlich ist, sieht man eine Tabelle. Diese enthält jeweils pro Zeile drei Werte:
Liniennummer,
Richtung,
Dauer.
Und man kann auch gleich erkennen, dass das Auslesen der Daten nicht ganz trivial ist. Denn im Text der Liniennummer ist die Linie zusätzlich als Kommentar eingefügt. Mir ist nicht klar, wozu die KVB das macht. Jedenfalls müssen solche Dinge aus dem Text entfernt werden, wenn man mit den Daten irgendetwas anfangen möchte.
Und an dieser Stelle beschleicht mich dann schon das etwas mulmige Gefühl, dass sich ohne Ankündigung an Details des HTML-Dokuments etwas ändern könnte.
Um diese Tabelle im HTML-Dokument zu identifizieren, verwende ich das class-Attribut. Die Tabelle ist zur Formatierung der class qr table zugeordnet. Das macht diese Tabelle innerhalb des gesamten HTML-Dokuments eindeutig, sodass bei der Selektion folgender XPath-Ausdruck verwendet werden kann:
"//table[@class='qr_table']//td"
Dieser XPath-Ausdruck bedeutet: Suche zunächst alle table-Elemente. Durch den vorangestellten doppelten Schrägstrich werden die table-Elemente auch dann gefunden, wenn sie in andere Elemente eingeschachtelt sind. Die Bedingung, die in eckigen Klammern folgt, schränkt die Suche auf solche table-Elemente ein, die ein class-Attribut haben, dessen Wert qr_table lautet. Das @-Zeichen vor class definiert class als Attribut. Andernfalls würde nach einem Element gesucht. Innerhalb der gefundenen Tabelle werden dann durch //td alle Tabellenzellen geliefert. Auch hier gilt wieder, dass diese eingeschachtelt sein können. Innerhalb der Tabelle folgen die td-Elemente nicht unmittelbar, sondern sind in tr-Elemente verpackt. Somit ist der doppelte Schrägstrich erforderlich, um die tr-Elemente zu ignorieren. Ergebnis dieses XPath-Ausdrucks sind also alle td-Elemente innerhalb aller Tabellen, deren class-Attribut den Wert qr_table aufweist.
Um zu überprüfen, ob der XPath-Ausdruck korrekt ist, habe ich einen Spike implementiert. Diesen habe ich wieder mithilfe von NUnit automatisiert, sodass ich mehrere Spikes in einem Projekt ablegen und individuell starten kann. Im selben Projekt liegen später auch meine Tests. Damit die Spikes beim Ausführen aller Tests nicht jedesmal mitlaufen, habe ich das NUnit-Attribut [Explicit] ergänzt. Dadurch muss der Test explizit gestartet werden. Listing 2 zeigt den Spike.
Der kostenlose Auszug ist beendet.