Aus Linux-Magazin 01/2024

Youtube-Zuschauerzahlen mit Go verarbeiten

© Chookiat Kaewjaroen / 123RF.com

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?

Abbildung 1: Der Youtube-Channel des Linux-Magazins.

Abbildung 1: Der Youtube-Channel des Linux-Magazins.

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.

Abbildung 2: Zum Auslesen von Daten schalten Sie die Youtube-v3-API an.

Abbildung 2: Zum Auslesen von Daten schalten Sie die Youtube-v3-API an.

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).

Abbildung 3: Zum Datenzugriff benötigen Sie einen API-Key.

Abbildung 3: Zum Datenzugriff benötigen Sie einen API-Key.

Abbildung 4: Den einmal erzeugten API-Key können Sie einfach kopieren.

Abbildung 4: Den einmal erzeugten API-Key können Sie einfach kopieren.

Abbildung 5: Es empfiehlt sich, den API-Key auf Youtube zu beschränken.

Abbildung 5: Es empfiehlt sich, den API-Key auf Youtube zu beschränken.

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.

Abbildung 6: Die ID des Youtube-Kanals versteckt sich in der Channel-Seite.

Abbildung 6: Die ID des Youtube-Kanals versteckt sich in der Channel-Seite.

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&ouml;rdern wie gew&uuml;nscht die Channel-ID zutage.

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.

Abbildung 8: Die Youtube-Client-API holt Videos eines Channels.

Abbildung 8: Die Youtube-Client-API holt Videos eines Channels.

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.

Abbildung 9: Die Balkengrafik zu den Views der Videos im Channel zeigt klare Favoriten.

Abbildung 9: Die Balkengrafik zu den Views der Videos im Channel zeigt klare Favoriten.

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

  1. Youtube-Projekt registrieren: https://developers.google.com/youtube/registering_an_application
  2. Ergebnisse von Suchanfragen mit der Google-API: https://developers.google.com/youtube/v3/docs
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben