TechNet Magazine > Home > Alle Ausgaben > 2008 > September >  Hey, Scripting Guy!: Bleiben Sie in Verbindung ...
Hey, Scripting Guy! Bleiben Sie in Verbindung mit Ihrem Toaster
Die Scripting Guys von Microsoft

Codedownload verfügbar unter: HeyScriptingGuy2008_09.exe (150 KB)

Wenn irgendetwas unsere moderne Welt in wenigen Worten zusammenfasst, sind es diese drei Wörter: in Verbindung bleiben. Dank Handy müssen Sie nicht mehr zu Hause sein, damit andere Sie anrufen können. Sie sind jederzeit tagsüber und nachts überall erreichbar. (Oh, wie … gut). Dank Drahtloscomputern müssen Sie nicht im Büro sein, um Ihre Arbeit zu tun. Heutzutage können Sie von zu Hause aus, am Strand, ja, praktisch überall arbeiten.
Eine wahre Geschichte: Die Eltern des Scripting Editors haben kürzlich einen Campingausflug unternommen, wobei sie, genau wie die berühmten Entdecker vergangener Tage, mit den rauen Bedingungen auskommen mussten, als sie beim Herstellen einer Verbindung zum Drahtlosnetzwerk des Campingplatzes auf Probleme stießen. Gott sei Dank funktionierte das Satellitenfernsehen noch!
Das ist aber noch nicht alles. Mithilfe von GPS-Geräten wissen Sie mit wenigen Zentimetern Abweichung genau, wo Sie stehen. Diese Geräte können auch anderen genau mitteilen, wo Sie stehen. (Die alte Redensart, dass Sie wegrennen, sich aber nicht verstecken können, war noch nie so wahr wie heute.) Wenn der Scripting Guy, der diese Kolumne schreibt, es wollte, könnte er sich jedes Mal, wenn ein Scheck abgerechnet wird, von seinem Scheckkonto anrufen lassen. Oder sein Auto könnte ihm per E-Mail monatliche Zustandsberichte senden. Doch damit nicht genug: Der Toaster hat angeboten, immer dann, wenn er Urlaub macht, den Hund auszuführen und die Blumen zu gießen.
Also gut, das stimmt nicht so ganz – noch nicht. Doch wenn er es wollte, könnte der Scripting Guy einen internetfähigen Toaster kaufen. Dann könnte er seinen Toaster auf dem Heimweg anrufen, sodass frisch getoastetes Brot auf ihn wartet, wenn er die Haustür aufschließt. Um ehrlich zu sein, wissen wir nicht, warum frisch getoastetes Brot warten sollte, sobald Sie die Haustür aufschließen. Aber wenn Sie es so wollen …
Wenn es das Ziel aller ist, in Verbindung zu bleiben, überrascht es natürlich nicht weiter, dass die Scripting Guys, die noch nie ein Sklave von Trends waren, für mehr Trennung eintreten. Bedeutet das, dass die Scripting Guys Ihnen empfehlen, Ihr Handy oder den Laptop wegzuwerfen? Nein, selbst die Scripting Guys sind nicht so dumm. Sie befürworten jedoch, dass Sie Ihrem Skriptingarsenal getrennte Recordsets hinzufügen. Doch wenn Sie Ihr Handy oder Ihren Laptop wegwerfen wollen, werden wir Sie nicht davon abhalten.
Hinweis: Laut einer Umfrage von Harris Interactive haben 43 Prozent der Amerikaner im Urlaub einen Laptop verwendet, um arbeitsrelevante E-Mails zu überprüfen und zu senden. Und mehr als 50 Prozent der Amerikaner verwenden ihr Handy während des Urlaubs, um ihre E-Mails und/oder Voicemail abzurufen. Und diese Zahlen umfassen nicht die 40 Prozent der Amerikaner, die innerhalb eines Jahres keinen Urlaub nehmen.
Es versteht sich von selbst, dass viele Menschen ihrem Skriptingarsenal getrennte Recordsets hinzufügen würden, wobei es jedoch ein Problem gibt: Sie haben keinerlei Vorstellung davon, was ein getrenntes Recordset ist. Falls Sie mit dem Konzept nicht vertraut sind: Ein getrenntes Recordset ist (mehr oder weniger) eine Datenbanktabelle, die nicht mit einer eigentlichen Datenbank verbunden ist. Stattdessen wird sie durch ein Skript erstellt, befindet sich nur im Speicher und verschwindet, sobald das Skript endet. Anders ausgedrückt: Ein getrenntes Recordset ist eine erstellte Datenstruktur, die nur für einige Minuten vorhanden ist und dann verschwindet, wobei sie Ihre Daten mitnimmt. Also wirklich, Scripting Guys! Das hört sich sehr nützlich an. Danke für eure Hilfe!
Gut, zugegeben: Getrennte Recordsets hören sich wirklich nicht sehr aufregend an. Und sie sind es tatsächlich nicht. Doch sie können äußerst nützlich sein. Wie altgediente VBScript-Ersteller nur allzu gut wissen, verfügt VBScript nicht gerade über die besten Funktionen für das Sortieren von Daten. (Es sei denn, dass Sie überhaupt keine Funktionen zum Sortieren von Daten als die besten Funktionen zum Sortieren von Daten betrachten.) Ebenso ist die Fähigkeit von VBScript, große Datensätze zu behandeln, bestenfalls begrenzt. Außerhalb des Dictionary-Objekts (das Sie darauf beschränkt, mit Elementen zu arbeiten, die höchstens zwei Eigenschaften haben) oder des Array (das größtenteils auf Datenlisten mit einer einzigen Eigenschaft begrenzt ist) … ist das praktisch schon alles.
Mithilfe des getrennten Recordset werden beide Probleme (und mehr) gelöst. Sie müssen Daten sortieren, speziell Daten mit mehreren Eigenschaften? Kein Problem. Wie bereits erwähnt, ist ein getrenntes Recordset das virtuelle Äquivalent einer Datenbanktabelle, und nichts auf der Welt ist einfacher als das Sortieren einer Datenbanktabelle. (OK, wenn Sie pingelig sein wollen, ist das Nichtsortieren einer Datenbanktabelle wohl einfacher als das Sortieren.) Oder vielleicht haben Sie einen großen Satz an Elementen, Elemente mit mehreren Eigenschaften, die Sie verfolgen müssen? Kein Problem. Haben wir schon erwähnt, dass ein getrenntes Recordset das virtuelle Äquivalent einer Datenbanktabelle ist? Müssen Sie diese Informationen in irgendeiner Weise filtern oder diese Daten auf einen bestimmten Wert hin durchsuchen? Wenn es doch nur eine Möglichkeit gäbe, das virtuelle Äquivalent einer Datenbanktabelle zu verwenden …
Richtig bemerkt: Vielleicht sollten wir Ihnen einfach zeigen, um was es hier geht. (Vorausgesetzt, wir wissen, um was es hier geht.) Nehmen wir einfach einmal an, dass wir die in Abbildung 1 dargestellte Baseballstatistik haben. Es handelt sich um eine der Website MLB.com entnommene Statistik, die in der durch Tabulator getrennten Wertedatei C:\Scripts\Test.txt gespeichert wurde.
Player Home Runs RBI Average
D Pedroia 4 28 .276
K Kouzmanoff 8 25 .269
J Francouer 7 35 .254
C Guzman 5 20 .299
F Sanchez 2 25 .238
I Suzuki 3 15 .287
J Hamilton 17 67 .329
I Kinsler 7 35 .309
M Ramirez 12 39 .295
A Gonzalez 17 55 .299
Das ist alles gut und schön, aber angenommen, wir möchten Ihnen diese Spielerliste nach Anzahl der Homeruns sortiert zeigen. Kann uns ein getrenntes Recordset dabei helfen? Das werden wir gleich herausfinden. Sehen Sie sich Abbildung 2 an. Ja, sie enthält ziemlich viel Code. Aber keine Sorge. Sie werden gleich sehen, dass es nicht so schlimm ist, wie es aussieht.
Const ForReading = 1
Const adVarChar = 200
Const MaxCharacters = 255
Const adDouble = 5

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "Player", _
  adVarChar, MaxCharacters
DataList.Fields.Append "HomeRuns", adDouble
DataList.Open

Set objFSO = _
  CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile _
  ("C:\Scripts\Test.txt", ForReading)

objFile.SkipLine

Do Until objFile.AtEndOfStream
    strStats = objFile.ReadLine
    arrStats = Split(strStats, vbTab)

    DataList.AddNew
    DataList("Player") = arrStats(0)
    DataList("HomeRuns") = arrStats(1)
    DataList.Update
Loop

objFile.Close

DataList.MoveFirst

Do Until DataList.EOF
    Wscript.Echo _
        DataList.Fields.Item("Player") & _
        vbTab & _
        DataList.Fields.Item("HomeRuns")
    DataList.MoveNext
Loop
Zuerst definieren wir vier Konstanten:
  • ForReading. Wir verwenden diese Konstante, wenn wir die Textdatei öffnen und aus ihr lesen.
  • AdVarChar. Dies ist eine Standard-ADO-Konstante zum Erstellen eines Felds, das den Variant-Datentyp verwendet.
  • MaxCharacters. Dies ist eine Standard-ADO-Konstante, die zum Anzeigen der maximalen Anzahl von Zeichen (in diesem Fall 255) dient, die ein Variant-Feld enthalten kann.
  • AdDouble. Eine letzte ADO-Konstante zum Erstellen eines Felds, das einen doppelten (numerischen) Datentyp verwendet.
Nach dem Definieren der Konstanten begegnen wir diesem Codeblock:
Set DataList = CreateObject _
    ("ADOR.Recordset")
DataList.Fields.Append "Player", _
    adVarChar, MaxCharacters
DataList.Fields.Append "HomeRuns", _
    adDouble
DataList.Open
Dies ist der Teil des Skripts, in dem wir unser getrenntes Recordset einrichten und konfigurieren. Dazu erstellen wir zuerst eine Instanz des ADOR.Recordset-Objekts, das unsere virtuelle Datenbanktabelle (das heißt unser getrenntes Recordset) erstellt.
Dann verwenden wir diese Codezeile (und die Append-Methode), um dem Recordset ein neues Feld hinzuzufügen:
DataList.Fields.Append "Player", adVarChar, MaxCharacters
Wie Sie sehen können, ist dies nicht weiter kompliziert: Wir rufen einfach die Append-Methode auf, gefolgt von drei Parametern:
  • Der Name des Felds (Players).
  • Der Datentyp für das Feld (adVarChar).
  • Die maximale Anzahl der Zeichen, die im Feld gespeichert werden können (MaxCharacters).
Nach dem Hinzufügen des Felds „Players“ können wir ein zweites Feld hinzufügen: HomeRuns, das einen numerischen (adDouble) Datentyp hat. Wenn wir damit fertig sind, können wir die Open-Methode aufrufen, um unseren Recordset als offen und einsatzbereit zu deklarieren.
Als nächstes erstellen wir eine Instanz von Scripting.FileSystemObject und öffnen die Datei „C:\Scripts\Test.txt“. Dieser Teil des Skripts hat nichts mit dem getrennten Recordset zu tun. Er ist nur vorhanden, weil wir Daten aus einer Textdatei abrufen müssen. Die erste Zeile in der Textdatei enthält unsere Headerinformationen:
Player     Home Runs     RBI        Average
Wir benötigen diese Informationen für unser Recordset nicht. Daher rufen wir als Erstes nach dem Öffnen der Datei die SkipLine-Methode auf, um diese erste Zeile zu überspringen:
objFile.SkipLine
Jetzt, da wir uns in der ersten Zeile mit eigentlichen Daten befinden, richten wir eine Do Until-Schleife ein, damit wir die übrige Datei Zeile für Zeile lesen können. Jedes Mal, wenn wir eine Zeile aus der Datei einlesen, speichern wir diesen Wert in einer Variablen namens „strLine“ und verwenden dann die Split-Funktion, um diese Zeile in ein Array von Werten zu konvertieren (indem wir die Zeile jedes Mal, wenn wir auf einen Tabulator stoßen, aufteilen):
arrStats = Split(strStats, vbTab)
Zugegebenermaßen ist dies eine recht schnelle Übersicht, aber wir erwarten, dass Sie mittlerweile recht gut in der Lage sind, Informationen aus Textdateien abzurufen. Um es kurz zu fassen: Beim ersten Durchlauf der Schleife enthält das Array „arrStats“ die Elemente in Abbildung 3.
Item Number Item Name
0 D Pedroia
1 4
2 28
3 .276
Jetzt wird es unterhaltsam:
DataList.AddNew
DataList("Player") = arrStats(0)
DataList("HomeRuns") = arrStats(1)
DataList.Update
Hier fügen wir die Informationen für Spieler 1 (D Pedroia) zum getrennten Recordset hinzu. Um dem Recordset einen Datensatz hinzuzufügen, beginnen wir mit dem Aufrufen der AddNew-Methode. Dadurch wird ein neuer, leerer Datensatz für uns erstellt, mit dem wir arbeiten können. Wir verwenden die nächsten beiden Codezeilen, um den beiden Recordset-Feldern (Player und HomeRuns) Werte zuzuweisen. Dann rufen wir die Update-Methode auf, um offiziell diesen Datensatz in den Recordset zu schreiben. Und dann beginnen wir wieder oben in der Schleife, wo wir den Prozess mit der nächsten Zeile, d. h. dem nächsten Spieler, in der Textdatei wiederholen. Es stimmt also doch: Es ist zwar viel Code vorhanden, aber alles ist ziemlich einfach und übersichtlich.
Was geschieht nun, wenn dem Recordset alle Spieler hinzugefügt wurden? Nachdem wir die Textdatei geschlossen haben, führen wir diesen Codeblock aus:
DataList.MoveFirst

Do Until DataList.EOF
  Wscript.Echo _
    DataList.Fields.Item("Player") & _
    vbTab & _
    DataList.Fields.Item("HomeRuns")
  DataList.MoveNext
Loop
In Zeile 1 verwenden wir die MoveFirst-Methode, um den Cursor am Anfang des Recordset zu positionieren. Tun wir dies nicht, besteht das Risiko, dass nur einige Daten im Recordset angezeigt werden. Dann richten wir eine Do Until-Schleife ein, die fortfährt, bis keine Daten mehr vorhanden sind (d. h. bis die EOF-Eigenschaft (end-of-file, Dateiende) des Recordset „True“ lautet).
In der Schleife geben wir nur die Werte der Felder „Player“ und „HomeRuns“ zurück (beachten Sie die etwas ungewöhnliche Syntax zum Anzeigen eines bestimmten Felds: DataList.Fields.Item("Player"). Und dann rufen wir einfach die Move­Next-Methode auf, um zum nächsten Datensatz im Recordset überzugehen.
Wie Sie sehen, war dies höchst einfach. Schließlich kehren wir zu Folgendem zurück:
D Pedroia       4
K Kouzmanoff    8
J Francouer     7
C Guzman        5
F Sanchez       2
I Suzuki        3
J Hamilton      17
I Kinsler       7
M Ramirez       12
A Gonzalez      17
Wie Sie sehen können, ist dies eigentlich gar nicht so gut. Stimmt, wir haben die Namen der Spieler und die Gesamtzahlen der Homeruns erhalten, doch die Gesamtzahlen der Homeruns sind nicht sortiert. Verdammt. Warum hat das getrennte Recordset die Daten für uns nicht sortiert?
Dafür gibt es einen guten Grund: Wir haben dem Recordset nicht mitgeteilt, welches Feld sortiert werden soll. Doch das lässt sich leicht korrigieren: Ändern Sie einfach das Skript, sodass die Sortierinformationen direkt vor dem Aufruf der MoveFirst-Methode hinzugefügt werden. Das heißt, der Teil des Skripts sollte wie folgt aussehen:
DataList.Sort = "HomeRuns"
DataList.MoveFirst
Offensichtlich gibt es dabei keinen Trick. Wir weisen der Sort-Eigenschaft einfach das Feld HomeRuns zu. Betrachten Sie jetzt die Ausgabe, die wir beim Ausführen des Skripts erhalten:
F Sanchez       2
I Suzuki        3
D Pedroia       4
C Guzman        5
J Francouer     7
I Kinsler       7
K Kouzmanoff    8
M Ramirez       12
J Hamilton      17
A Gonzalez      17
Viel besser. Mit Ausnahme einer Sache: Meistens werden Gesamtzahlen von Homeruns in absteigender Reihenfolge aufgeführt, wobei die Spieler mit den meisten Homeruns zuerst aufgeführt werden. Gibt es eine Möglichkeit, ein getrenntes Recordset in absteigender Reihenfolge zu sortieren?
Selbstverständlich. Wir müssen nur den hilfreichen DESC-Parameter anfügen, und zwar so:
DataList.Sort = "HomeRuns DESC"
Und was macht der DESC-Parameter für uns? Ganz genau:
A Gonzalez      17
J Hamilton      17
M Ramirez       12
K Kouzmanoff    8
I Kinsler       7
J Francouer     7
C Guzman        5
D Pedroia       4
I Suzuki        3
F Sanchez       2
Es ist übrigens durchaus legal, mehrere Eigenschaften zu sortieren. Die einzelnen Eigenschaften müssen einfach nur der Sortierreihenfolge zugewiesen werden. Nehmen wir beispielsweise an, dass Sie zuerst die Homeruns und dann die RBIs sortieren möchten. Kein Problem. Mit diesem Befehl erreichen Sie dies:
DataList.Sort = "HomeRuns DESC, RBI DESC"
Probieren Sie es ruhig selbst einmal aus. Es macht nicht so viel Spaß wie das Abrufen von E-Mails während des Urlaubs, aber es kommt dem schon sehr nahe.
Hinweis: Denken Sie daran, dass Sie in einem Feld, das dem Recordset nicht hinzugefügt wurde, nicht sortieren können. Was bedeutet das? Es bedeutet, dass Sie vor dem Hinzufügen einer Eigenschaft wie z. B. RBI zur Sort-Anweisung Ihrem Skript diese Zeilen an den entsprechenden Stellen hinzufügen müssen:
DataList.Fields.Append "RBI", adDouble

DataList("RBI") = arrStats(2)
Und wenn Sie sich die Ausgabe ansehen wollen, müssen Sie auch Ihre Wscript.Echo-Anweisung ändern:
Wscript.Echo _
  DataList.Fields.Item("Player") & _
  vbTab & _
  DataList.Fields.Item("HomeRuns") & _
  vbTab & DataList.Fields.Item("RBI")
Was können wir mit getrennten Recordsets noch tun? Hier ist noch etwas. Angenommen, wir rufen alle Informationen für alle Spieler ab und sortieren diese Daten dann nach der durchschnittlichen Zahl der Schläge. (Unter anderem bedeutet dies, dass wir unser ursprüngliches Skript ändern müssen, um Felder mit der Bezeichnung „RBI“ und „Batting­Average“ zu erstellen.) Die Ausgabe sieht folgendermaßen aus:
J Hamilton      17      67      0.329
I Kinsler       7       35      0.309
A Gonzalez      17      55      0.304
C Guzman        5       20      0.299
M Ramirez       12      39      0.295
I Suzuki        3       15      0.287
D Pedroia       4       28      0.276
K Kouzmanoff    8       25      0.269
J Francouer     7       35      0.254
F Sanchez       2       25      0.238
Das ist gut und schön. Doch was tun, wenn wir nur eine Liste der Spieler wünschen, die .300 oder besser schlagen? Wie können wir die angezeigten Daten auf die Spieler begrenzen, die einigen angegebenen Kriterien entsprechen? Nun, eine Möglichkeit besteht darin, dem Recordset einen Filter zuzuweisen:
DataList.Filter = "BattingAverage >= .300"
Ein Recordset-Filter hat denselben Zweck wie eine Datenbankabfrage: Er bietet eine Methode zum Begrenzen zurückgegebener Daten an eine Teilmenge aller Datensätze im Recordset. In diesem Fall bitten wir den Filter einfach, alle Datensätze mit Ausnahme der Datensätze auszumerzen, bei denen das Feld „Batting-Average“ einen Wert von größer oder gleich .300 hat. Und wissen Sie was? Der Filter macht genau das Gewünschte:
J Hamilton      17      67      0.329
I Kinsler       7       35      0.309
A Gonzalez      17      55      0.304
Wenn unsere Kinder doch genauso reagieren würden!
Übrigens können Sie mehrere Kriterien in einem einzigen Filter verwenden. Dieser Befehl beispielsweise begrenzt die zurückgegebenen Daten auf Datensätze, bei denen das BattingAverage-Feld einen Wert von größer oder gleich .300 hat und der Wert des HomeRuns-Felds größer als 10 ist:
DataList.Filter = _
  "BattingAverage >= .300 AND HomeRuns > 10"
Im Gegensatz dazu begrenzt dieser Filter Daten auf Datensätze, bei denen das BattingAverage-Feld einen Wert von größer oder gleich .300 hat oder der Wert des HomeRuns-Felds größer als 10 ist:
DataList.Filter = "BattingAverage >= .300 OR HomeRuns > 10"
Probieren Sie beides aus, um zu sehen, wo der Unterschied liegt. Doch damit nicht genug: Hier ist noch ein Filter, den Sie ausprobieren können:
DataList.Filter = "Player LIKE 'I*'"
Wie sich herausstellt, können Sie auch Platzhalter in Ihren Filtern verwenden. Dazu verwenden Sie den LIKE-Operator (im Gegensatz zum Gleichheitszeichen) und verwenden dann das Sternchen, so wie Sie es beim Ausführen eines MS-DOS®-Befehls wie „dir C:\Scripts\*.txt“ tun würden. Im vorhergehenden Beispiel sollten wir eine Liste der Spieler erhalten, deren Name mit dem Buchstaben „I“ beginnt, denn die von uns verwendete Syntax besagt: „Zeig mir eine Liste aller Datensätze, in denen der Wert des Player-Felds mit einem I beginnt, auf den dann praktisch irgendetwas folgt.“ Probieren Sie es aus. Mittlerweile wissen Sie, wie es geht.
Übrigens sind Durchschnittszahlen von Schlägen wie 0.309 auch kein Problem. (In der Regel werden Durchschnittszahlen ohne die vorstehende Null, also z. B. als .309 ausgedrückt.) Doch das ist in Ordnung. Sie können einfach die FormatNumber-Funktion verwenden, um die Durchschnittszahl der Schläge ganz nach Belieben zu formatieren:
FormatNumber (DataList.Fields.Item("BattingAverage"), 3, 0)
Nehmen Sie diese Funktion einfach in Ihre Wscript.Echo-Anweisung auf, wenn Sie die Zahl anzeigen (alternativ könnten Sie die Ausgabe einer Variablen zuweisen und die Variable in Ihre Echo-Anweisung stellen):
Wscript.Echo _
  DataList.Fields.Item("Player") & _
  vbTab & _
  DataList.Fields.Item("HomeRuns") & _
  vbTab & DataList.Fields.Item("RBI") & _
  vbTab & _
  FormatNumber _
  (DataList.Fields.Item("BattingAverage"), _
   3, 0)
Das macht Spaß, nicht wahr?
Doch leider ist dies alles, wofür wir in diesem Monat Zeit haben. Zusammenfassend möchten wir nur sagen – einen Augenblick, das Telefon klingelt.
Also, wir wollten darauf hinweisen – na super, jetzt klingelt das Handy. Und gerade haben wir eine E-Mail vom Toaster erhalten. Ganz wichtig: Unser Toast ist fertig. Wollen wir Butter oder Marmelade? Ich muss los, aber wir sehen uns im nächsten Monat wieder!
Der Scripting Perplexer von Dr. Scripto
Die monatliche Herausforderung, die nicht nur Ihr Talent zum Rätsellösen testet, sondern auch Ihre Skriptingfähigkeiten.

September 2008: Skriptingsuche
Hier ist eine einfache (oder vielleicht doch nicht so einfache) Wortsuche. Suchen Sie alle VBScript-Funktionen und Anweisungen in der Liste. Das Ganze hat jedoch einen Haken: Die verbleibenden Buchstaben ergeben ein verstecktes Wort, das – richtig geraten – ein Windows PowerShell™-Cmdlet ist!
Wortliste Abs, Array, Atn, CCur, CLng, CInt, DateValue, Day, Dim, Else, Exp, Fix, InStr, IsEmpty, IsObject, Join, Len, Log, Loop, LTrim, Mid, Month, MsgBox, Now, Oct, Replace, Set, Sin, Space, Split, Sqr, StrComp, String, Timer, TimeValue, WeekdayName.

Show Answer

Die Scripting Guys arbeiten für Microsoft (oder sind zumindest dort angestellt). Wenn sie nicht gerade ihrem Hobby, dem Baseball (oder verschiedenen anderen Aktivitäten) nachgehen, betreiben sie das TechNet-Skriptcenter. Besuchen Sie es unter www.scriptingguys.com.

Page view tracker