Der Scraper Colly hilft in der Programmiersprache Go beim Datenmopsen vom Web. Mit einigen praktischen Beispielen illustriert Mike Schilli die Fähigkeiten des leistungsfähigen Tools.
So lange es Webseiten zum Anschauen für die Massen von Browserkunden im Netz gibt, sitzen auf der Konsumentenseite auch einige, die die Daten in einem anderen Format haben wollen und deshalb Scraper-Skripte schreiben, um diese Daten automatisch abzusaugen.
Das verstößt zwar regelmäßig gegen die von Webseiten-Betreibern geforderten Terms of Service (ToS), aber so lange die Sauger die Daten nicht kommerziell verwerten oder die Webseite zu heftig mit ihren Anfragen bombardieren, regt sich auch keiner darüber auf.
Perl-Füchse schätzen als Hilfsmittel für die Absaugetätigkeit vermutlich »WWW::Mechanize«, Python-Afficinados vielleicht das »selenium«-Paket [2], und in Go bieten gleich mehrere Pakete Scraper-Dienste an.
Eines der neueren ist »Colly« (wohl von “collect”, einsammeln), und wie in Go üblich lässt es sich einfach direkt vom Github-Repository mittels
go get github.com/gocolly/colly
kompilieren und installieren. Danach kann etwa ein Programm wie Listing 1 auf die Webseite des Nachrichtensenders CNN zugreifen, sich durch die im HTML der Seite versteckten Links wühlen und sie zu Testzwecken ausgeben.
Listing 1
linkfind.go
01 package main
02
03 import (
04 "fmt"
05 "github.com/gocolly/colly"
06 )
07
08 func main() {
09 c := colly.NewCollector()
10
11 c.OnHTML("a[href]",
12 func(e *colly.HTMLElement) {
13 fmt.Println(e.Attr("href"))
14 })
15
16 c.Visit("https://cnn.com")
17 }
Ade, Dependency Hell
Go-typisch erzeugt »go build linkfind.go« ein Binary »linkfind«, das zwar mit 14 MByte reichlich gewampert daherkommt, aber eben alle abhängigen Bibliotheken schon enthält und auf ähnlichen Architekturen ohne weitere Fisimatenten läuft. Dass Go die heutzutage gängige “Dependency Hell” von der Laufzeit zum Compile-Zeitpunkt verlagert und dem Enduser die Arbeit abnimmt, ist wohl eine der großartigsten Ideen überhaupt in letzter Zeit.
Listing 1 initialisiert in Zeile 9 mit »NewCollector()« hierzu eine neue Colly-Struktur, deren Funktion »Visit()« später die URL der Webseite von CNN anfährt und den Callback »OnHTML()« anspringt, sobald das HTML der Seite eingetrudelt ist. Der Selector »”a[href]”« ruft den nachfolgenden »func()«-Code nur für Links im Format »<A HREF=…>« auf, übergibt ihm einen Pointer auf eine Struktur vom Typ »colly.HTMLElement« – und damit liegt dort in »e.Attr(“href”)« die Link-URL als String vor.
Das Ganze lässt sich noch etwas verfeinern. Wie schwierig wäre es, festzustellen, welche HTML-Anker zu externen Webseiten verzweigen und welche die Seite intern verlinken? Listing 2 baut auf dem gleichen Grundgerüst auf, definiert aber noch eine Zählerstruktur »Stats« und verdrahtet die zu untersuchende URL nicht mehr fest im Code, sondern nimmt sie als Parameter auf der Kommandozeile entgegen.
Listing 2
linkstats.go
01 package main
02
03 import (
04 "fmt"
05 "github.com/gocolly/colly"
06 "net/url"
07 "os"
08 )
09
10 type Stats struct {
11 external int
12 internal int
13 }
14
15 func main() {
16 c := colly.NewCollector()
17 baseURL := os.Args[1]
18
19 stats := Stats{}
20
21 c.OnHTML("a[href]",
22 func(e *colly.HTMLElement) {
23 link := e.Attr("href")
24 if linkIsExternal(link, baseURL) {
25 stats.external++
26 } else {
27 stats.internal++
28 }
29 })
30
31 c.Visit(baseURL)
32
33 fmt.Printf("%s has %d internal "+
34 "and %d external links.\n", baseURL,
35 stats.internal, stats.external)
36 }
37
38 func linkIsExternal(link string,
39 base string) bool {
40 u, err := url.Parse(link)
41 if err != nil {
42 panic(err)
43 }
44 ubase, _ := url.Parse(base)
45
46 if u.Scheme == "" ||
47 ubase.Host == u.Host {
48 return false
49 }
50 return true
51 }
Um die externen von internen Links zu unterscheiden, zerlegt die Funktion »linkIsExternal()« ab Zeile 38 sowohl die Link-URL als auch die ursprüngliche Basis-URL mit »Parse()« aus dem Paket »net/url« in ihre Einzelteile. Anschließend prüft sie, ob der Link mit »http(s)://« beginnt (mittels »Schema()«) oder ob der Host in beiden URLs identisch ist – in beiden Fällen verweist der Link wieder auf die ursprüngliche Seite, ist dann also intern.
Strukturiert mit Default
Zeile 19 initialisiert eine Instanz der Struktur »Stats«, und wie in Go üblich erhalten alle Mitglieder ihre Default-Werte zugewiesen, im Fall der beiden Integer also jeweils den Wert »0«. So brauchen die Zeilen 25 und 27 für jeden untersuchten Link nur jeweils den Integerwert um 1 hochzuzählen, und am Ende des Programms kann Zeile 33 die Anzahl der internen respektive externen Links ausgeben. Für die Startseite des Linux-Magazins ergibt sich
$ ./linkstats https://www.linux-magazin.de https://www.linux-magazin.de has 588 internal and 16 external links.
also eine ganz schön komplexe Webseite! Doch Link-Sammeln erschöpft die Funktion von »Colly« noch lange nicht. Die Dokumentation auf [3] ist zwar noch etwas spärlich, aber anhand der dort veröffentlichten Beispiele kann der Interessierte weitere Anwendungsbereiche ableiten.
Wir gehen surfen
Zum Beispiel zeigt die Webseite »surfline.com« die Höhe der Wellen an ausgewählten Stränden der Welt an, und da ich ja fürs Leben gern surfen gehe, bietet sich ein Kommandozeilentool an, das schnell auf die Seite meines Hausstrands Ocean Beach in San Francisco schaut und nachsieht, ob irgendwelche Monsterwellen dagegen sprechen, rauszupaddeln, denn bei mehr als zwei Metern flattert mir als Hobbysurfer gehörig der Wetsuit. Abbildung 1 zeigt die Informationen auf der Webseite, Abbildung 2 illustriert, wo sich die Daten im HTML der Seite laut Chromes “Developer Tools” verstecken. Aufgabe des Scrapers ist es nun, sich durch die Tags zu hangeln und die Zahlenwerte herauszufieseln.
Im vorliegenden Fall findet sich die Angabe der Wellenhöhe in einem »span«-Tag der Klasse »quiver-surf-height«, allerdings ist dieses Tag mehrfach im Dokument vertreten, weil die Seite auch die Konditionen an anderen benachbarten Surfspots anzeigt. Der Trick besteht nun darin, einen Pfad von der Dokumentwurzel zu den Daten zu finden, der eindeutig ist. Wie Abbildung 2 zeigt, führt dieser Pfad über ein Element der Klasse »sl-spot-forecast-summary«.
Die Programmierung geht flugs von der Hand, kaum erhält die »OnHTML()«-Funktion in Zeile 12 von Listing 3 beide Klassennamen als Leerzeichen-separierten String als erstes Argument, bohrt sich der Query-Prozessor genau auf diesem Pfad ins Dokument hinein und findet nur eine, nämlich die richtige Wellenhöhe des aktuell ausgewählten Spots.
Listing 3
surfline.go
01 package main
02
03 import (
04 "fmt"
05 "github.com/gocolly/colly"
06 "github.com/PuerkitoBio/goquery"
07 )
08
09 func main() {
10 c := colly.NewCollector()
11
12 c.OnHTML(".sl-spot-forecast-summary " +
13 ".quiver-surf-height",
14 func(e *colly.HTMLElement) {
15 e.DOM.Contents().Slice(0,1).Each(
16 func(_ int, s *goquery.Selection) {
17 fmt.Printf("%s\n", s.Text())
18 })
19 })
20
21 c.Visit("https://www.surfline.com/" +
22 "surf-report/ocean-beach-overview/" +
23 "5842041f4e65fad6a77087f8")
24 }
Goquery: Jquery für Go
Aber hoppla, im gefundenen »span«-Element befinden sich nun drei Zeilen, die erste gibt mit »”8-12″« die gesuchte Wellenhöhe an, aber die zweite stellt im Browser mit »<sup>FT>/sup>« noch ein Fuß-Zeichen (das amerikanische Längenmaß) daneben und die dritte Zeile gibt mit »”+”« noch an, dass es auch ein bisserl mehr sein kann.
Wie kann der Query-Prozessor diese drei Zeilen trennen? In der Colly-Dokumentation fand ich dazu nichts, aber zum Glück nutzt Colly intern die Query-Sprache »Goquery«, die »jQuery« sehr ähnlich ist, und von einer in Colly gefundenen Struktur vom Typ »HTMLElement« kommt man über deren Attribut »DOM« schnell zur zugehörigen »goquery«-Struktur.
Deren zugehörige Dokumentation [4] führt aus, dass die Funktion »Contents()« auf das gefundene Element den Text in seine drei Teile zerlegt, ein nachfolgender Aufruf von »Slice(0,1)« schneidet dann den ersten Teil heraus.
Wie in Go üblich beziehen sich die Indexnummern für ein Slice immer auf den Anfang (einschließlich) und das letzte Element (ausschließlich). Das nachfolgende »Each()« schnappt sich also nur das eine Ergebnis und ruft mit der gefundenen Selektion die nachgeschaltete Callback-Funktion auf. Diese extrahiert mit »s.Text()« dann den Text mit der Höhenangabe. Ganz schön kompliziert!
Hohe Wellen
Der folgende Aufruf des kompilierten Binary mit
$ ./surfline 8-12
zaubert so zu Tage, dass die Wellenhöhe mit 8 bis 12 Fuß (also 2,4 bis 3,6 Meter) dann doch etwas über meinen Möglichkeiten liegt und ich das Surfbrett aus Sicherheitsgründen heute besser zu Hause lasse. Aber morgen ist ja auch noch ein Tag!
Online PLUS
Im Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/videos/
Infos
- Listings zu diesem Artikel: https://www.linux-magazin.de/static/listings/magazin/2019/04/snapshot/
- Michael Schilli, “Serviler Wächter”: Linux-Magazin 02/17, S. 88, https://www.linux-magazin.de/ausgaben/2017/12/snapshot/
- “Colly, Fast and Elegant Scraping Framework for Gophers”:http://go-colly.org/docs/
- Goquery-Dokumentation: https://godoc.org/github.com/PuerkitoBio/goquery








