Um den Erfolg seiner Youtube-Videos im Auge zu behalten, schreibt Mike Schilli ein Go-Programm, das die Zuschauerzahlen des LM-Youtube-Kanals via Google-API abfragt und grafisch aufpoliert.
Begleitend zu dieser Kolumne erscheint zu jeder Ausgabe des Programmier-Snapshots ein Screencast, der auf einem Youtube-Kanal läuft (Abbildung 1). Dort erklärt der Autor die wesentlichen Schritte zum Zusammenbauen und Ausprobieren der vorgestellten Go-Programme. Videoschauen ist bequemer als Lesen, und wir machen es unseren Lesern ja gern so einfach wie möglich. Wie wäre es mit einem Go-Programm, das die Zuschauerzahlen des Kanals abfragt?
Registrieren, dann probieren
Vor das Einsammeln der Zuschauerzahlen per API hat Google allerdings eine Registrierung [1] gesetzt, um Bot-Aktivitäten im Auge zu behalten und notfalls per Quota-Regelung zu beschränken. Dabei unterscheidet Google zweierlei Mechanismen zur Authentifizierung eingehender API-Anfragen. Lesen Requests private User-Daten oder wollen gar im Auftrag des Benutzers den Server beschreiben, müssen OAuth-Credentials her. Für das Lesen von Daten, die sowieso schon öffentlich verfügbar sind, zum Beispiel der Anzahl der Aufrufe eines Videos, genügt ein API-Key zu einem flugs registrierten Projekt auf der Google-Konsole, das die Youtube-API (Abbildung 2) verwendet.
Ein API-Key lässt sich schnell erzeugen (Abbildung 3) und erlaubt es der App, an die öffentlichen Daten zu gelangen. Um Missbrauch mit eventuell gestohlenen Keys einzuschränken, empfiehlt es sich aber, die Gültigkeit erzeugter Schlüssel (Abbildung 3) unter Restrict API-Key auf die tatsächlich benötigten Bereiche einzuschränken – im vorliegenden Fall also auf die Youtube-API (Abbildung 5).
Zwar dürfte die App eines potenziellen Schlüsselräubers mit der Beute keine Daten modifizieren, doch sie könnte böswillig die Quota-Zuteilung des Bestohlenen aufbrauchen. Deswegen ist es eine gute Idee, die Reichweite zu beschränken.
Fragen statt scrapen
Bevor es nun ans Auslesen der Videos eines Youtube-Channels geht, benötigen API-Abfragen dessen interne Channel-ID. Dabei handelt es sich nicht, wie man meinen könnte, um den Benutzernamen des Users, sondern um einen Hex-String, den Google im Quellcode der Channel-Webseite unter dem Eintrag »externalId« versteckt (Abbildung 6). Allerdings missbilligen Googles Nutzungsbedingungen derartiges Screen-Scraping entschieden. Als mustergültiger Netzteilnehmer holt Listing 1 deswegen die ID mittels einer Suchanfrage an den Youtube-API-Server aus dem »Search«-Bereich der offiziellen Client-API ein.
Listing 1
ytsearch.go
package main
import (
"fmt"
"log"
)
func main() {
service, err := apiInit()
if err != nil {
log.Fatalf("%v", err)
}
query := "Linux Magazin"
resp, err := service.Search.List([]string{"snippet"}).Q(query).Type("channel").Do()
if err != nil {
log.Fatalf("%v", err)
}
for i := 0; i < len(resp.Items); i++ {
snippet := resp.Items[i].Snippet
fmt.Printf("%20s: %s\n", snippet.ChannelTitle, snippet.ChannelId)
}
}
Bevor es ans Absetzen der Anfrage an den Server geht, hilft die Client-API beim Aufbau der dafür notwendigen URL. Die Methoden des API-Clients (»List()«, »Q()«, »Type()« und so weiter) geben jeweils ein Objekt mit der bis dato zusammengestöpselten URL für die Anfrage zurück. So hängt Zeile 12 zum Beispiel mit ».Q()« den Suchstring an und setzt anschließend mit »Type()« den URL-Parameter für den Typ gewünschter Suchergebnisse auf »”channel”«.
Das abschließende »Do()« führt die Webanfrage aus und liefert die Antwort des Servers in der Variablen »resp« zurück, verpackt in eine Go-Datenstruktur. Die wiederum enthält eine Struktur, die im Attribut »Items« die Treffer als Array-Slice enthält, die ihrerseits auf weitere Strukturen zeigen. Unter »Snippet« finden sich dort Details zu den Treffern, darunter der Titel des Kanals (»ChannelTitle«) und der gesuchte Hex-String (»ChannelId«) der Kanal-ID.
Bequemer Service
Listing 1 greift für Abfragen mit »service« bequem auf ein Service-Objekt zur Client-API zu, verlässt sich aber für dessen Aufbau auf die Funktion »apiInit()« in Listing 2.
Damit Anfragen auch das API-Token an den Server übermitteln, der ohne es nur Fehlermeldungen zurückgäbe, nimmt »apiInit()« das Token vom User auf der Kommandozeile entgegen. Das erfolgt mit der Option »–api-key« beim Aufruf des kompilierten Binarys. Der Kommandozeilenparser von Go schnappt sich das Token in Zeile 11 mit »Parse()« und legt es als Pointer in der Variablen »apiKey« ab.
Das in Zeile 16 erzeugte Service-Objekt schluckt das Token für später und bekommt zusätzlich noch ein Context-Objekt spendiert. Das könnte beispielsweise dazu dienen, angestoßene Requests per Fernsteuerung abzubrechen – davon macht Listing 2 aber keinen Gebrauch. Am Ende der Funktion »apiInit()« gibt Zeile 17 die fertige »service«-Variable ans Hauptprogramm zurück, das damit autorisierte Requests an Googles API-Server absetzen kann.
Listing 2
api-init.go
package main
import (
"context"
"flag"
"log"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
)
func apiInit() (*youtube.Service, error) {
apiKey := flag.String("api-key", "", "API Key")
flag.Parse()
if *apiKey == "" {
log.Fatalf("Provide an API Key")
}
ctx := context.Background()
service, err := youtube.NewService(ctx, option.WithAPIKey(*apiKey))
return service, err
}
Listing 3 zeigt den üblichen Dreisprung, der aus den Quellen von Listing 1 und 2 das Binary »ytsearch« baut. Dabei zieht der Go-Compiler unter anderem die Quellen des Youtube-API-Clients vom Google-Server, übersetzt sie und verlinkt alles zu einem ausführbaren Programm. Dessen Ausgabe – mit einem wie vorher beschrieben eingeholten gültigen API-Key – zeigt Abbildung 7. Wie Sie sehen, fördert der erste Treffer auf die Suchanfrage nach “Linux Magazin” tatsächlich die in Abbildung 6 untermalte Channel-ID zutage, also ist den Terms of Service Genüge getan.
Listing 3
Kompilieren
$ go mod init ytsearch $ go mod tidy $ go build ytsearch.go api-init.go

Abbildung 7: Die Treffer bei der Suche nach “Linux Magazin” fördern wie gewünscht die Channel-ID zutage.
Dazu ruft in Listing 1 Zeile 12 die Methode »List()« des in Listing 2 erzeugten API-Clients auf und übergibt ihr einen Array-Slice mit dem String »”snippet”« als einziges Element. Die Google-Dokumentation für Entwickler beschreibt die bereitgestellten Datentypen [2] der API im Detail. In »snippet« liegen für gefundene Channels im Ergebnis dann Titel und Beschreibung des Kanals sowie Referenzen auf Playlists, um die dort vorgestellten Videos abzuspielen.
Universale Playlist
Gewappnet mit dieser Channel-ID kann nun Listing 4 daran gehen, die Metadaten einzelner Videos des Kanals einzuholen. Das ist gar nicht so einfach. Zuerst ruft Zeile 13 die Eckdaten des Channels ab, indem sie der API-Funktion »List()« den Parameter »contentDetails« mitgibt. In der Antwort steht unter »RelatedPlaylists« der Eintrag »Uploads«. Er gibt die ID einer Playlist an, die alle Videos des Channels enthält.
Im »service«-Bereich »PlaylistItems« startet daraufhin Zeile 23 eine Abfrage der Titel dieser Upload-Playlist. Auf unerfahrene Nutzer der Google-API wartet hier allerdings die Paging-Falle: Selbst, wenn Sie in »MaxResults« eine hohe Zahl erwarteter Videos angeben, liefert der API-Server grundsätzlich maximal 50 Treffer pro Request aus. Der Rest kommt später in nachfolgenden Anfragen, für die Sie aber »pageToken« setzen müssen. Den Wert dafür liefern vorhergehende Server-Antworten mit, falls der Server nicht alles eingepackt hat und auf einen erneuten Aufruf wartet, um den Rest nachzuliefern.
Nun liegen die Eckdaten der Videos des Channels vor. Allerdings fehlen noch die Zuschauerzahlen. Dafür iteriert die For-Schleife in Zeile 31 über alle Einträge der Playlist und ruft für jedes Video die »List()«-Funktion mit dem Parameter »statistics« auf. Zurück kommt für jeden Request unter anderem der Eintrag »ViewCount«. Zeile 41 füllt damit ein Array-Slice mit Strukturen vom Typ »chStats« (definiert ab Zeile 6), sprich: den Ergebnissen.
Listing 4
channel.go
package main
import (
"google.golang.org/api/youtube/v3"
"log"
)
type chStats struct {
vid string
title string
views uint64
}
func channelViews(service *youtube.Service, id string) ([]chStats, error) {
stats := []chStats{}
resp, err := service.Channels.List([]string{"contentDetails"}).Id(id).Do()
if err != nil {
log.Fatalf("%v", err)
}
if len(resp.Items) == 0 {
log.Fatal("Channel not found")
}
plid := resp.Items[0].ContentDetails.RelatedPlaylists.Uploads
pageToken := ""
for {
plResp, err := service.PlaylistItems.List([]string{"snippet"}).
PlaylistId(plid).
MaxResults(50).
PageToken(pageToken).
Do()
if err != nil {
log.Fatalf("%v", err)
}
for _, item := range plResp.Items {
videoID := item.Snippet.ResourceId.VideoId
videoResp, err := service.Videos.List([]string{"statistics"}).
Id(videoID).
Do()
if err != nil {
log.Fatalf("%v", err)
}
video := videoResp.Items[0]
viewCount := video.Statistics.ViewCount
stats = append(stats, chStats{vid: videoID, views: viewCount, title: item.Snippet.Title})
}
pageToken = plResp.NextPageToken
if pageToken == "" {
break
}
}
return stats, nil
}
Mit Kommas separiert
Nun bleibt dem Hauptprogramm in Listing 5 nur noch, die Client-API zu initialisieren und »channelViews()« mit der Channel-ID des Kanals des Linux-Magazins in Zeile 13 aufzurufen. Die zurückgelieferten Videotitel, IDs und Zuschauerzahlen schreibt Zeile 20 zeilenweise im CSV-Format auf die Standardausgabe.
Listing 5
ytfetch.go
package main
import (
"log"
"fmt"
"encoding/csv"
"os"
)
func main() {
service, err := apiInit()
if err != nil {
log.Fatal("%v", err)
}
stats, err := channelViews(service, "UCWmOnWFWo_NJf_ZJqgf-LwQ")
if err != nil {
log.Fatalf("%v", err)
}
w := csv.NewWriter(os.Stdout)
defer w.Flush()
for _, stat := range stats {
w.Write([]string{stat.vid, fmt.Sprintf("%d", stat.views), stat.title})
}
}
Das Programm »ytfetch« übersetzen Sie mit »ytfetch.go channel.go api?init.go«; Aufruf und Ausgabe des Binarys zeigt Abbildung 8. In der Shell lassen sich die Ausgabezeilen für spätere Auswertungen in eine CSV-Datei umlenken.
Grafisch ausgeschmückt
Welche Clips im Channel sind nun heiß begehrt, und welche dümpeln vor sich hin? Visuell lässt sich das leichter ablesen als in Zahlenkolonnen. Listing 6 schickt sich deshalb an, aus den CSV-Daten eine formschöne Balkengrafik zu erzeugen.
Dazu nutzt es das Go-Paket »go-chart« von Github, liest die CSV-Daten mit dem Standardmodul »encoding/csv« aus der vorher erzeugten Datei und verfüttert sie ab Zeile 27 an eine Struktur des Typs »BarChart«. Das Ergebnis malt Zeile 40 mit »Render()« in die vorher geöffnete Datei »views.png«.
Listing 6
views.go
package main
import (
"encoding/csv"
"os"
"strconv"
"github.com/wcharczuk/go-chart/v2"
)
func main() {
file, err := os.Open("ytfetch.csv")
if err != nil {
panic(err)
}
defer file.Close()
reader := csv.NewReader(file)
records, err := reader.ReadAll()
if err != nil {
panic(err)
}
values := []chart.Value{}
for _, record := range records {
v, err := strconv.ParseFloat(record[1], 64)
if err != nil {
panic(err)
}
values = append(values, chart.Value{Value: v})
}
graph := chart.BarChart{
Title: "Youtube Views By Video",
Background: chart.Style{
Padding: chart.Box{
Top: 40,
},
},
Height: 512,
BarWidth: 60,
Bars: values,
}
f, _ := os.Create("views.png")
defer f.Close()
graph.Render(chart.PNG, f)
}
Abbildung 9 zeigt anschaulich, dass es nur wenige Videos in der Beliebtheitsskala über 10 000 Views schaffen; die breite Masse erreicht nur zwei- oder dreistellige Zuschauerzahlen.
Hitparade
Welche Videos sind die erfolgreichsten? Dazu sortiert Listing 7 die ausgelesenen CSV-Daten absteigend nach Zuschauerzahlen. Die Standardfunktion »sort.Slice()« nimmt dazu ein Array »entries« mit den gesammelten Statistiken aller Videos entgegen und definiert in einem Callback, wie zwei Einträge miteinander zu vergleichen sind.
Das Ganze klappt nicht so einfach wie in einer Skriptsprache wie Python, denn Go fordert strenge Typen. Deswegen definiert Zeile 15 eine Struktur »entry« mit dem Videotitel und einer numerischen Zuschaueranzahl. Die For-Schleife ab Zeile 21 baut aus den Daten ein Array-Slice mit entsprechenden Einträgen auf. Die absteigend nach Zuschauerzahlen sortierten Einträge beschränkt die Slice-Operation in Zeile 32 mit »entries[0:6]« auf die ersten (und damit besten) sechs, die die nachfolgende For-Schleife ab Zeile 33 ausgibt.
Listing 7
yttop.go
package main
import (
"encoding/csv"
"fmt"
"os"
"sort"
"strconv"
)
func main() {
file, err := os.Open("ytfetch.csv")
if err != nil {
panic(err)
}
defer file.Close()
type entry struct {
title string
views int64
}
entries := []entry{}
r := csv.NewReader(file)
for {
e, err := r.Read()
if err != nil {
break
}
v, _ := strconv.ParseInt(e[1], 10, 64)
entries = append(entries, entry{title: e[2], views: v})
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].views > entries[j].views
})
entries = entries[0:5]
for _, e := range entries {
fmt.Printf("%5d %s\n", e.views, e.title)
}
}
Es stellt sich heraus, dass die erfolgreichsten Videos des Linux-Magazin-Channels einige Knoppix-Videos sind. Es folgt der erfolgreichste Programmier-Snapshot aller Zeiten über die Vor- und Nachteile der Programmiersprache Go. Der zweitpopulärste Snapshot dreht sich um Gesichtserkennung in Fotos und stammt aus dem Jahr 2018. Bis zum Influencer-Millionär hat das Linux-Magazin zwar noch einen weiten Weg vor sich, aber immerhin: Die Richtung stimmt! ((uba))
Infos
- Youtube-Projekt registrieren: https://developers.google.com/youtube/registering_an_application
- Ergebnisse von Suchanfragen mit der Google-API: https://developers.google.com/youtube/v3/docs














