Glengamoi (Forum) · AspHeute · .NET Heute (RSS-Suche) · AspxFiles (Wiki) · .NET Blogs

Webseiten automatisiert scrapen, Teil 2

Geschrieben von: Christian Holm
Kategorie: C#

This printed page brought to you by AlphaSierraPapa

Im letzten Artikel haben wir die Webseite "as is" in die Datenbank geschrieben, heute parsen wir den Content einer Wetterberichtsseite auf, d.h. wir befreien den gescrapten Content der Webseite von den Html Tags und schreiben die Informationen sortiert nach Themengebieten wieder in eine SQL Datenbank. Die in C# geschriebene Konsolenanwendung läßt sich dann wiederum als Scheduled Task automatisch nach einer vordefinierten Zeit ausführen.

Im ersten Teil dieser Artikelserie haben wir die Basics besprochen, also wie man automatisch den Content einer Wetterberichtsseite (Wettervorhersage fuer die Adria) mit Hilfe des Microsoft .NET Frameworks und Scheduled Tasks scrapt. Das besprochene C#-Beispiel scrapte den gesamten Content der Webseite und dumpte ihn in eine SQL Datenbank. Dies war nicht weiters schwer, jedoch war das Ergebnis nicht wirklich weiterverwendbar. Wie gesagt, der gesamte Content mit den Html Tags unstrukturiert in der Datenbank abgelegt.

Die heutige Aufgabe besteht darin, den Content der Webseite von den Html Tags zu befreien und den Content schön gegliedert nach Themengebieten, in diesem Fall z.B. nach eventuellen Warnungen, der aktuellen Lage, etc. in der Datenbank abzulegen. Um dies zu erreichen, habe ich Regular Expressions und ein paar Methoden der .NET Framework String Klasse verwendet.

Bevor wir mit dem Parsen anfangen, müssen wir zunächst die Struktur der Webseite kennen. Nach einer Beobachtung über einen längeren Zeitraum stellte sich heraus, daß sich (zum Glück) bis auf den aktuellen Content der Webseite nichts an der Struktur ändert. Diese Webseite sieht in einem Browser schlicht und einfach so aus:

Sieht an sich nicht sonderlich kompliziert aus. Ein (kleiner) Blick hinter die Kulissen läßt einen aber erahnen, daß wieder einiges an Parsingarbeit zu tun ist:

<HTML>
<HEAD>
   <TITLE>Wettervorhersage fuer die Adria</TITLE>
</HEAD>
<BODY TEXT="#000000" BGCOLOR="#FFFFFF" LINK="#2020DF" VLINK="#30307F">
<CENTER><P>
<B><FONT COLOR="#000000"><FONT SIZE=+1>
        DER SEEWETTERBERICHT DES SEEWETTERZENTRUMS SPLIT<BR>
        VOM 10.09.2001.   UM 1300 UHR<BR>
</FONT></FONT></B></P></CENTER>
<P><B><FONT COLOR="#FF0000"><FONT SIZE=+1>
        WARNUNG:
</FONT></FONT></B></P>
<FONT COLOR="#000000">
        Erwarten sich Nordost-, auf dem offenen Meer Nordwestböen, meistens am
        Ended des Tages und in der Nacht.Anfangs auf der Süd- und im Teil der
        Mitteladria Südostböen Stärke 35 bis 50 kn, und das offene Meer in der
        Nacht 4 bis 5. Stellenweise Gewitter.

</FONT><BR></P>
<P><B><FONT COLOR="#000000">
        DIE LAGE:
<BR><BR></FONT></B>

        Die Zyklone und frontale Störung versetzen sich über der Adria nach
        Osten, und der Hochdruckkeil verstärkt sich langsam aus Westen.

<BR></P>
...

Haufenweises unstrukturiertes Html (zum Glück ohne Crapplets) mit mehreren Leerzeichen als Einrückungen. Diesen ungeordneten Haufen gilt es mit Hilfe von Regular Expressions und der String Klasse des .NET Frameworks zu ordnen. Da der Sourcecode dieses Artikelbeispiels auf dem des ersten Teiles aufbaut, beschränke ich mich bei den Erklärungen nur auf den neu hinzugefügten Sourcecode.

Da der Sourcecode, Dank der objektorientierten Möglichkeiten von C#, fein säuberlich in Klassen die jeweils die Blueprints der einzelnen Arbeitsschritte beinhalten aufgeteilt ist, ist eine Erweiterung auch einfach zu erledigen. Die Erweiterung besteht hauptsächlich darin, daß eine weitere Klasse, die das Parsing übernimmt, hinzugefügt wird.

Diese Klasse namens CleanseContent beinhaltet eine Methode (StripHtmlTags), die wie der Name schon sagt, den in der Stringvariable strContent gespeicherten "Roh"-Content von allen vorhandenen Html Tags befreit. Warum - nun, Html Tags haben in einer Datenbank normalerweise nichts verloren, wenn die gespeicherte Information andersweitig verarbeitet werden soll. Den "Strip"-Vorgang habe ich mit Hilfe einer Regular Expression gelöst. Regular Expressions bestehen auf den ersten Blick aus einer Ansammlung unverständlicher Zeichenketten, dem sogenannten RegEx Pattern. Dieser Pattern enthält die Filter Anweisungen nach denen der String bearbeitet werden soll.

Regular Expressions erfordern zwar einiges an Einlesearbeit, lösen aber die meisten Probleme um einiges schneller als mit den konventionellen Stringoperationen. Um Regular Expressions innerhalb des Frameworks verwenden zu können muß man auf den System.Text.RegularExpressions Namespace referenzieren. Sehen wir uns nun den ersten Abschnitt der StripHtmlTags Methode an, der Regular Expressions verwendet:

public void StripHtmlTags(string strContent, out string strTitle, 
    out string strHeading1, out string strHeading2, out string strHeading3, 
    out string strHeading4, out DateTime dtForecastTimestamp)
 	{
 	string strRet,strNoWh;
 	string DateExtract,TimeExtract;
 	int nYear,nMonth,nDay,nHour,nMin;
 		
 	Regex rexStripHtml = new Regex("<([^!>]([^>]|\n)*)>", RegexOptions.IgnoreCase);
 	Regex rexStripWhSpace = new Regex(" * ");
 		
 	strRet = rexStripHtml.Replace(strContent, "");
 		
 	strNoWh = rexStripWhSpace.Replace(strRet, " ");
    ...

Da ich die Webseite in die einzelen aus dem obigen Screenshot ersichtlichen Kategorien wie Titel, Warnung, Lage, etc. gliedern möchte, muß die Methode dementsprechend auch einige Parameter(werte) zurückliefern (out ...). Die erste Regular Expression rexStripHtml säubert den Content der der Methode übergebenen Stringvariable strContent von Html Tags. Da es (allgemein) vorkommen kann, daß Html Tags einmal groß einmal klein geschrieben werden, setze ich einfach die RegEx Option auf IgnoreCase.

Dies ist natürlich nicht genug! Wir sind zwar die Html Tags los geworden, es sind aber noch unzählige Leerzeichen vorhanden, die als Einrückungen verwendet wurden. Diese entfernen wir wiederum mit einer nun simpleren Regular Expression rexStripWhSpace. Diese hat als Pattern jeweils am Anfang und am Ende ein Leerzeichen. Der Asterisk (*) in der Mitte dieser bedeutet, daß dazwischen beliebige viele Leerzeichen vorhanden sein können.

Zuletzt verwenden wir die Replace Methode der Regex Klasse des .NET Frameworks um den Content String zu manipulieren und somit zuerst die Tags und später dann die Leerzeichen los zu werden. Weiters ist es für die spätere Weiterverwendung sinnvoll, den Content in die einzelnen Kategorien aufzuteilen. Jede Kategorie wird dann in einer separaten Datenspalte der SQL Datenbank abgelegt.

Da ich auch hier wieder nach einem Pattern (Kategorieüberschrift) suche, jedoch mehr als nur das Pattern zurückgeliefert haben will, sprich den dazugehörigen Text, verwende ich hier den einfacheren Weg über die Methoden der .NET String Klasse:

 ...
int idxHeading1 = strNoWh.IndexOf("WARNUNG:");
int idxHeading2 = strNoWh.IndexOf("DIE LAGE:");
int idxHeading3 = strNoWh.IndexOf("WETTERVORHERSAGE FUER DIE ADRIA FUER DIE NAECHSTEN 12 STUNDEN :");
int idxHeading4 = strNoWh.IndexOf("WETTERAUSSICHT FUER DIE ADRIA FUER DIE WEITEREN 12 STUNDEN:");

strTitle = strNoWh.Substring(1,idxHeading1-1);
strTitle = strTitle.Trim();

int idxDate = strTitle.IndexOf("VOM ");
int idxTime = strTitle.IndexOf("UM ");
DateExtract = strTitle.Substring(idxDate+4,10).Trim();
TimeExtract = strTitle.Substring(idxTime+3,4).Trim();

nYear = Convert.ToInt32(DateExtract.Substring(6,4));
nMonth = Convert.ToInt32(DateExtract.Substring(3,2));
nDay = Convert.ToInt32(DateExtract.Substring(0,2));
nHour = Convert.ToInt32(TimeExtract.Substring(0,2));
nMin = Convert.ToInt32(TimeExtract.Substring(2,2));

dtForecastTimestamp = new DateTime(nYear,nMonth,nDay,nHour,nMin,00);

strHeading1 = strNoWh.Substring(idxHeading1,idxHeading2-idxHeading1);
strHeading1 = strHeading1.Trim();

strHeading2 = strNoWh.Substring(idxHeading2,idxHeading3-idxHeading2);
strHeading2 = strHeading2.Trim();

strHeading3 = strNoWh.Substring(idxHeading3,idxHeading4-idxHeading3);
strHeading3 = strHeading3.Trim();

strHeading4 = strNoWh.Substring(idxHeading4,strNoWh.Length-idxHeading4);
strHeading4 = strHeading4.Trim();		
}
...

Zuerst suche ich mir die ersten Characterpositionen der einzelnen Kategorieüberschriften mittels der IndexOf Methode, die bekanntlich die erste Position eines Characters oder in diesem Fall eines Strings aus einer vorgegeben Zeichenkette (Kategorieübschrift) zurückliefert.

Die Substring Methode liefert mir einen (Sub)String aus einer vorgegebenen Zeichenkette, anhand einer definierten Start- bzw. Indexposition. Die Länge des Substrings wird durch den zweiten Parameter bestimmt. Die jeweilige Länge kann ich leicht aus den einzelnen Indexpositionen der anderen Überschriften ermitteln.

Auf speziellen Wunsch hin extrahiere ich aus dem Titel-String (strTitle), Datum und Uhrzeit der aktuellen Wetterprognose. Da dieses Angabeformat im Titel-String nach längerer Beobachtungszeit gleich blieb, ist das Extrahieren auch in Zukunft ohne FormatException möglich. Ich parse die einzelnen Teile (Jahr, Monat, Tag, Stunde, Minute) aus den Strings und wandle Sie anschließend in Integerwerte um. Dies ist notwendig, da der DateTime Constructor hier nur Integerwerte entgegennimmt.

Diese einzelnen Werte werden wie gesagt dem DateTime Constructor in der Reihenfolge yyyymmddhhmmss übergeben. Dadurch kann dann später in der Datenbank explizit nach einem gültigen Datum und nicht mühsamerweise nach einem String gesucht werden.

Damit die StripHtmlTags Methode der CleanseContent Klasse auch bei der Ausführung des Programms aufgerufen wird, ist folgender Eintrag in der Basisklasse MainClass zu erstellen:

CleanseContent MyCleanseContent = new CleanseContent();
MyCleanseContent.StripHtmlTags(strContent, out strTitle, out strHeading1, out strHeading2, 
          out strHeading3, out strHeading4, out dtForecastTimestamp);

Da wir nun mehr Felder in der Datenbank befüllen, müssen wir auch die Write2DB Klasse aus dem Beispiel des ersten Teiles anpassen. Dies ist nicht weiter tragisch, da die Datenbank nur zusätzliche Felder erhalten hat und wir keine Beziehungen verändern müssen, da es keine gibt:

class Write2DB
{
    public void WriteIt(DateTime dtGrabTime, bool bSuccess, string strTitle, 
        string strHeading1, string strHeading2, string strHeading3,string strHeading4, 
        string strErrCode, DateTime dtForecastTimestamp)
    {
    string strConn = "server=(local)\\NetSDK;database=ScrapAppImproved;Trusted_Connection=yes";
    string insertCmd = "insert into tContent (GrabTime, Success, Title, Heading1, Heading2, 
        Heading3, Heading4, ErrCode, ForecastTimestamp) values (@GrabTime,@Success, @Title, 
        @Heading1, @Heading2, @Heading3, @Heading4, @ErrCode,@ForecastTimestamp)";
    
    SqlConnection MySqlConnection = new SqlConnection(strConn);
    SqlCommand MyCmd = new SqlCommand(insertCmd, MySqlConnection);

    MyCmd.Parameters.Add(new SqlParameter("@GrabTime", SqlDbType.DateTime, 8));
    MyCmd.Parameters["@GrabTime"].Value = dtGrabTime;
    MyCmd.Parameters.Add(new SqlParameter("@Success", SqlDbType.Bit, 1));
    MyCmd.Parameters["@Success"].Value = bSuccess;
    MyCmd.Parameters.Add(new SqlParameter("@Title", SqlDbType.NVarChar,255));
    MyCmd.Parameters["@Title"].Value = strTitle;
    MyCmd.Parameters.Add(new SqlParameter("@Heading1", SqlDbType.NVarChar,1500));
    MyCmd.Parameters["@Heading1"].Value = strHeading1;
    MyCmd.Parameters.Add(new SqlParameter("@Heading2", SqlDbType.NVarChar,1500));
    MyCmd.Parameters["@Heading2"].Value = strHeading2;
    MyCmd.Parameters.Add(new SqlParameter("@Heading3", SqlDbType.NVarChar,1500));
    MyCmd.Parameters["@Heading3"].Value = strHeading3;
    MyCmd.Parameters.Add(new SqlParameter("@Heading4", SqlDbType.NVarChar,1500));
    MyCmd.Parameters["@Heading4"].Value = strHeading4;
    MyCmd.Parameters.Add(new SqlParameter("@ErrCode", SqlDbType.NVarChar, 255));
    MyCmd.Parameters["@ErrCode"].Value = strErrCode;
    MyCmd.Parameters.Add(new SqlParameter("@ForecastTimestamp", SqlDbType.DateTime, 8));
    MyCmd.Parameters["@ForecastTimestamp"].Value = dtForecastTimestamp;
    MyCmd.Connection.Open();
    MyCmd.ExecuteNonQuery();
    MyCmd.Connection.Close();
    }	
}

Nun können wir die Kommandozeilenapplikation erneut mit

csc /t:exe /out:ScrapAppImproved.exe main.cs

beim Kommandoprompt kompilieren. Passend zu unserer Applikation die neue Struktur der ScrapAppImproved SQL Datenbank:

Die einzelnen Kategorien werden jetzt säuberlich getrennt und ballastfrei in die einzelnen Spalten geschrieben. Anhand des Datumswertes ForecastTimestamp sieht man dann genau von welchem Tag und Uhrzeit die Prognose stammt. Damit die Applikation auch automatisiert abläuft ist diese wieder, wie im ersten Teil der Artikelserie beschrieben, in die Scheduled Task Liste einzutragen.

Schlußbemerkung

Da nun der gescrapte Content der Wetterprognoseseite aufbereitet wurde, steht einer Weiterverwendung nichts mehr im Wege. Ein Folgeartikel dieser Serie beschreibt die Versendung dieser Informationen per SMS (Short Message Service) Nachricht.

This printed page brought to you by AlphaSierraPapa

Download des Codes

Klicken Sie hier, um den Download zu starten.
http://www.aspheute.com/code/20010911.zip

Verwandte Artikel

Die String Klasse in C#
http:/www.aspheute.com/artikel/20000803.htm
Exception Handling in C#
http:/www.aspheute.com/artikel/20000724.htm
Gegengifte für SQL Injection
http:/www.aspheute.com/artikel/20011031.htm
Kopieren verboten - Lizenzsicherung bei ASP Scripts
http:/www.aspheute.com/artikel/20020411.htm
Scrapen von Webseiten
http:/www.aspheute.com/artikel/20000824.htm
Sonderzeichen korrekt grabben mit XmlHttp
http:/www.aspheute.com/artikel/20011113.htm
Webseiten automatisiert scrapen
http:/www.aspheute.com/artikel/20010910.htm
Wetterbericht per SMS versenden
http:/www.aspheute.com/artikel/20010913.htm

Links zu anderen Sites

Regular Expression Library
http://regexlib.com
Seewetterbericht Adria
http://prognoza.hr/jadran_e.html

 

©2000-2006 AspHeute.com
Alle Rechte vorbehalten. Der Inhalt dieser Seiten ist urheberrechtlich geschützt.
Eine Übernahme von Texten (auch nur auszugsweise) oder Graphiken bedarf unserer schriftlichen Zustimmung.