Die Lösung im Detail
13.05.2016 17:30:49
Zwenn
Hallo Hans,
hallo Michael,
schön dass das noch jemand liest hier. Ist ja doch schon recht weit runter gerutscht in der Liste. Ich habe mir trotzdem viel Mühe gegeben und wenn wenigstens einer davon einen Nutzen hat, um so besser :-)
Vorbemerkung
Grundsätzlich ist mir noch keine HTML-Seite untergekommen, die sich nicht auslesen lässt. Michael, Du schreibst in einer Antwort dieses Threads, die Seite erkennt, das maschinell zugegeriffen wird. Nun habe ich ja den gleichen Link wie Du ausgewählt, um die Ergebnistabelle in Excel zu importieren. Ich habe Dein Script nicht getestet, dazu aber folgendes:
Das automatisiert (maschinell, wie Du es nanntest) zugegeriffen wird, kann ein Server nur feststellen, wenn er eine Routine implementiert hat, die erkennt, dass vom gleichen Client in kurzer Abfolge viele gleichartige Seitenaufrufe kommen. Ich weiß dass eBay das mindestens vor einigen Jahren noch gemacht hat. Man bekam nach etwa 400 Zugriffen nicht mehr die angesurfte Seite angezeigt, sondern ein Captcha. Auch Google hat das, zumindest zu gleicher Zeit, entsprechend abgefangen. Es gibt auch dafür eine Lösung, die ich hier aber nicht veröffentlichen möchte.
Ein weiteres Mal ist mir eine Seitensperre passiert, als der Betreiber vermutlich seine LogFiles ausgewertet hat und feststellte, dass pro Tag etwa 4.000 Zugriffe von der gleichen IP kamen. Das war ein Webshop der etwas dubioseren Art, auf dem wir für einen Kunden bestimmte Produkte beobachteten, die dort nix zu suchen hatten. Die Lösung des Shopbetreibers war, unsere IP zu sperren. Die Lösung für uns bestand dann darin, die IP vor jedem Auslesevorgang zu ändern.
Für das, was Hans möchte, brauchen wir aber nur einen einzigen Zugriff auf die Seite. Diesen erreichen wir durch das "Fernsteuern" des Internet Explorers, den auch der normale Anwender benutzt. Deshalb ist es nach meinem Dafürhalten in diesem Fall absolut unmöglich, dass der Server erkennt, ob ein Mensch die Adresse in den Browser eingegeben hat, oder ein Script.
Aufbau des Makros
In der angehängten Arbeitsmappe befindet sich das Modul NBA_Ergebnisse_holen. In diesem gibt es ein Sub NBA_Ergebnisse_Holen() als Makro, welches jedoch lediglich dazu dient, die eingelesenen Werte auszugeben. Die eigentliche Arbeit erledigt die Funktion ErgebnisArrayFuellen().
Diese Funktion liest alle verfügbaren Informationen der Ergebnistabelle in ein dynamisches Array ein. Ich habe es dynamisch gemacht, weil ich nicht weiß, ob immer 20 Datensätze geholt werden, wenn das Makro verwendet wird. Sollten noch nicht alle Spiele eines Spieltages beendet sein, könnten es ja auch weniger sein. Für ein Array habe ich mich entschieden, weil man damit für die Ausgabe in eine Tabelle komplett unabhängig vom Einlesen der Daten ist. Dazu mehr in den Kopf-Kommentaren des Makros.
Vorarbeit, um das Makro programmieren zu können
Um unser Ziel zu erreichen, die Ergebnistabelle von http://basketball.wettpoint.com/liga/nba-usa.html
nach Excel zu importieren, müssen wir den HTML-Code der Seite dahingehend analysieren, ob sich der Bereich mit der Tabelle eindeutig identifizieren lässt. Heutige Internetseiten halten sich glücklicherweise inzwischen an den Grundsatz die Struktur einer Seite vom Design zu trennen. Deshalb findet man in vielen der verwendeten HTML-Abschnitte zur Gliederung des Seitenaufbaus eindeutige IDs (einmalige Namen im Dokument), Namen von CSS Vorgaben und jede Menge Attribute mit zugehörigen Werten.
So ist es auch hier. Die Ergebnistabelle steht in einer HTML-Tabelle, die mit folgender Zeile eingeleitet wird (Tag-Klammerung habe ich durch Eckige Klammern ersetzt, weil die Darstellung hier sonst nicht funktioniert:
[table summary="NBA Ergebnisse USA"]
Nach einem kurzen Test (kopieren und im Quelltext suchen) stellen wir fest, das Attribut "summary" mit dem zugehörigen Wert "NBA Ergebnisse USA" ist auf der Seite nur einem einzigen table-Tag zugeordnet, wobei es 12 table-Tags gibt. Diesen Umstand nutzen wir aus.
Ich habe im Makro sehr viel kommentiert und bitte euch dort nachzusehen, wie diese Informationen in der Programmierung verwendet werden. An dieser Stelle möchte ich nur zwei Adressen angeben, die einem das Leben extrem vereinfachen, wenn man Webseiten auslesen will:
1. http://jsbeautifier.org/
In diese Seite kann man HTML-Code und Javascript-Code einfügen, der sich sonst nicht entziffern lässt. Nach einem Klick auf den Beautify-Button bekommt man sehr schön strukturierten Code. Ich habe das mit dem HTML-Teil gemacht, der die Ergebnistabelle enthält:
[table summary="NBA Ergebnisse USA"]
[tr]
[td width="120px"]14.04.16, 04:30 Uhr[/td]
[td class="sps1"][b]L.A. Lakers[/b] - Utah Jazz[/td]
[td class="sps1"][b]101 : 96[/b] (19:21) (23:36) (24:18) (35:21) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 04:30 Uhr[/td]
[td class="sps1"][b]Golden State Warriors[/b] - Memphis Grizzlies[/td]
[td class="sps1"][b]125 : 104[/b] (37:23) (33:27) (32:31) (23:23) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 04:30 Uhr[/td]
[td class="sps1"][b]Portland Trail Blazers[/b] - Denver Nuggets[/td]
[td class="sps1"][b]107 : 99[/b] (27:31) (31:25) (29:23) (20:20) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 04:30 Uhr[/td]
[td class="sps1"][b]Phoenix Suns[/b] - L.A. Clippers[/td]
[td class="sps1"][b]112 : 105[/b] (25:27) (37:28) (23:20) (27:30) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"]Cleveland Cavaliers - [b]Detroit Pistons[/b][/td]
[td class="sps1"][b]110 : 112[/b] (24:29) (20:29) (28:24) (31:21) n.V. [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"][b]Boston Celtics[/b] - Miami Heat[/td]
[td class="sps1"][b]98 : 88[/b] (13:35) (25:27) (25:5) (35:21) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"]Dallas Mavericks - [b]San Antonio Spurs[/b][/td]
[td class="sps1"][b]91 : 96[/b] (27:20) (25:14) (15:31) (24:31) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"][b]Washington Wizards[/b] - Atlanta Hawks[/td]
[td class="sps1"][b]109 : 98[/b] (33:33) (24:26) (27:17) (25:22) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"][b]Chicago Bulls[/b] - Philadelphia 76ers[/td]
[td class="sps1"][b]115 : 105[/b] (16:32) (35:28) (35:13) (29:32) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"][b]Minnesota Timberwolves[/b] - New Orleans Pelicans[/td]
[td class="sps1"][b]144 : 109[/b] (40:23) (32:27) (34:29) (38:30) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"][b]Houston Rockets[/b] - Sacramento Kings[/td]
[td class="sps1"][b]116 : 81[/b] (34:18) (30:26) (28:20) (24:17) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"]Brooklyn Nets - [b]Toronto Raptors[/b][/td]
[td class="sps1"][b]96 : 103[/b] (26:16) (21:33) (19:29) (30:25) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"][b]Charlotte Hornets[/b] - Orlando Magic[/td]
[td class="sps1"][b]117 : 103[/b] (38:22) (28:28) (20:29) (31:24) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]14.04.16, 02:00 Uhr[/td]
[td class="sps1"]Milwaukee Bucks - [b]Indiana Pacers[/b][/td]
[td class="sps1"][b]92 : 97[/b] (20:37) (26:25) (24:16) (22:19) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]13.04.16, 04:30 Uhr[/td]
[td class="sps1"][b]L.A. Clippers[/b] - Memphis Grizzlies[/td]
[td class="sps1"][b]110 : 84[/b] (29:22) (32:21) (23:21) (26:20) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]13.04.16, 02:00 Uhr[/td]
[td class="sps1"][b]San Antonio Spurs[/b] - Oklahoma City Thunder[/td]
[td class="sps1"][b]102 : 98[/b] (21:32) (22:21) (31:19) (19:21) n.V. [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]13.04.16, 01:30 Uhr[/td]
[td class="sps1"]Detroit Pistons - [b]Miami Heat[/b][/td]
[td class="sps1"][b]93 : 99[/b] (17:19) (33:31) (19:21) (24:28) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]13.04.16, 01:30 Uhr[/td]
[td class="sps1"][b]Toronto Raptors[/b] - Philadelphia 76ers[/td]
[td class="sps1"][b]122 : 98[/b] (32:23) (23:38) (32:13) (35:24) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]13.04.16, 01:00 Uhr[/td]
[td class="sps1"][b]Indiana Pacers[/b] - New York Knicks[/td]
[td class="sps1"][b]102 : 90[/b] (29:36) (24:18) (26:16) (23:20) [/td]
[td class="sps1"]FT[/td]
[/tr]
[tr]
[td width="120px"]12.04.16, 04:00 Uhr[/td]
[td class="sps1"]Phoenix Suns - [b]Sacramento Kings[/b][/td]
[td class="sps1"][b]101 : 105[/b] (22:26) (27:30) (23:31) (29:18) [/td]
[td class="sps1"]FT[/td]
[/tr]
[/table]
So sieht man sehr schön den gleichmäßigen Aufbau der Tabelle. Jeder Datensatz (also jedes Spiel) wird durch tr-Tags begrenzt. Innerhalb dieser Tags gibt es jedesmal 4 td-Tags, die die Informationen enthalten, die wir wollen. Wie wir da nun ran kommen, schaut bitte im Quellcode der Funktion ErgebnisArrayFuellen() nach. Leider läßt er sich nicht lesen, wenn ich ihn hier rein kopiere, weil sich sehr viele Zeilenumbrüche ergeben, die das Ganze unübersichtlich machen. Schaut also bitte in die hochgeladene Arbeitsmappe.
2. http://software.hixie.ch/utilities/js/live-dom-viewer/
In diese Seite kann man den Quelltext einer HTML Seite kopieren und sie gibt einen den kompletten DOM-Baum aus. Für ganze Seiten wird das allerdings sehr unübersichtlich, deshalb empfehle ich nur die Codeabschnitte einzufügen, die für Euer Vorhaben relevant sind. Mit Hilfe der Baumdarstellung kann man sich in vielen Fällen besser erschließen, wie eine bestimmte Seite funktioniert.
Etwas zum DOM (Document Object Model)
Im Makro verwende ich die Überprüfung auf ein Attribut in allen table-Tags, um den benötigten HTML-Abschnitt zu identifizieren:
'Alle table-Tags durchlaufen und die Schleife verlassen, wenn das richtige anhand des
'Attributs "summary" mit dem Wert "NBA Ergebnisse USA" gefunden wurde
For Each oKnotenAst In oBrowser.document.getElementsByTagName("table")
If oKnotenAst.getAttribute(sErkennungsAttribut) = sErkennungsString Then
Exit For
End If
Next oKnotenAst
Im Bezug auf das DOM sind hier die Folgenden beiden Code-Abschnitte relevant:
oBrowser.document.getElementsByTagName("table")
oKnotenAst.getAttribute(sErkennungsAttribut)
Wie man sich leicht denken kann, wird über die erste Zeile auf HTML Tags zugegriffen, die einfach mit ihrem Namen angegeben werden. Wir benötigen "table". Es kommt mitunter vor, dass man genau weiß, das wievielte Tag man benötigt. In diesem Fall kann man sich ForEach sparen und direkt auf das Tag zugreifen. Dazu muss man wissen, dass gleiche Tags im DOM mit Inizes beginnend mit 0 durchnummeriert sind. Um auf einen bestimmten Index zuzugreifen, wird er einfach in runden Klammern hinter dem Tag-Namen angegeben:
Z.B.
oBrowser.document.getElementsByTagName("table")(0)
würde auf die erste verfügbare Tabelle der HTML-Seite zugreifen.
Attribute hingegen lassen sich nicht direkt anwählen. Deshalb muss man alle Indizes zu einem Tag mit einer Schleife durchlaufen, wie ich es in der Funktion mache.
Neben diesen beiden Möglichkeiten des Zugriffs auf das DOM, gibt es unter anderem noch die Befehle:
getElementById(ID) 'Direkter Zugriff auf einen Ast des DOM über einen eindeutigen _
Namen
getElementsByClassName(CSS-Name) 'Zugriff auf die MENGE aller HTML-Elemente, mit dem _
angegebenen Namen
Zu beachten ist, das eine ID nur einmal im HTML Code vorkommen darf und wir deshalb keinen Index brauchen, um auf das identifizierte Element zuzugreifen. getElementByTagName und getElementByClassName hingegen bilden immer Arrays, auf deren Elemente wir ausschließlich über ihren Index zugreifen können. Auch wenn die gefundene Menge von Elementen in einem Dokument 1 ist.
Wie kommt man aber weiter, wenn die zuvor genannten Möglichkeiten scheitern, weil die benötigten Informationen einfach nicht von der gewünschten Seite verwendet werden? In dem Fall gibt es die Möglichkeit sich direkt durch den DOM-Baum zu hangeln. Dafür gibt es vor allem die beiden Befehle:
firstChild 'Springt in der Baumhirarchie eine Ebene nach unten
nextSibling 'Springt in der Baumhirarchie zum nächsten Geschwister auf der gleichen Ebene
Aus diesen beiden Befehlen lassen sich Ketten aufbauen, die durch den DOM-Baum navigieren und an einem Zielknoten ankommen. Aus diesem Zielknoten kann dann der gewünschte Wert ausgelesen werden.
Z.B.
document.firstChild.firstChild.nextSibling.firstChild..nextSibling.nextSibling.value
document.firstChild.nextSibling.firstChild.nextSibling.innerText
document.firstChild.firstChildfirstChild.nextSibling.outerHTML
Um rauszufinden, welchen Pfad man angeben muss, empfielt sich der Link Nr.2, den ich weiter oben empfohlen habe.
Aber Achtung:
Benutzt diese Möglichkeit außerst sparsam und möglichst nicht für lange Pfade durch den Baum. Sobald etwas am Aufbau der HTML-Seite verändert wird, müssen sämtliche Pfade geprüft und angepasst werden. Deshalb so viel wie möglich mit den get-Befehlen arbeiten. Auch da muss man anpassen. Aber das ist viel viel einfacher.
Schlussbemerkung
Ich möchte hier nicht weiter ins Detail gehen. Auch wenn ich weiß, dass die gegebene Info nur an der Oberfläche kratzt und auch viele Lücken enthält. Aber probiert damit zunächst mal rum und schaut, was passiert. Mit Euren vorhandenen VBA Kenntnissen könnt Ihr Euch ja einfach mal die Zwischenergebnisse als Text ausgeben lassen.
Das DOM unterstützen übrigens alle modernen Browser. Allerdings lässt sich nur der IE direkt aus Excel heraus mit VBA für DOM benutzen, weil Excel nur mit COM Anwendungen umgehen kann. Deshalb hier noch der Link zur MS DOM-Dokumentation:
https://msdn.microsoft.com/en-us/library/hh771916%28v=vs.85%29.aspx
Klickt Ihr z.B. Methods an, so findet Ihr in der sich öffnenden Seite unter anderem die Beschreibung zum Befehl getElementByID.
Ich hoffe Ihr seid jetzt nicht noch verwirrter als vorher ;-)
Hier ist noch der Donloadlink für das Makro:
Die Datei https://www.herber.de/bbs/user/105550.xlsm wurde aus Datenschutzgründen gelöscht
Viele Grüße,
Zwenn