Google PageSpeed Insights API mit Python abfragen

Mit Lighthouse gibt uns Google ein hervorragendes Tool an die Hand, um die Performance und Usability von einzelnen URLs zu analysieren. Will man nun hin und wieder eine URL checken, so reicht das Web-Interface vollkommen aus. Will man aber kontinuierlich monitoren oder eine Vielzahl von URLs überprüfen, ist der Weg über das Web-Interface nicht mehr ganz so schön.

Um dieses Problem zu umgehen, kann man die PageSpeed Insights API nutzen und die Ergebnisse ganz einfach per Script abrufen. Im Folgenden möchte ich euch meinen Python-Ansatz näher bringen. Mit diesem könnt ihr:

  • mehrer URLs auf einmal abrufen
  • Screenshots und Thumbnails generieren
  • categories festlegen
  • die Ergebnisse in ein Excel-Dokument speichern
  • bei Bedarf die wichtigsten Infos im Terminal ausgeben lassen

Project-Setup und benötigte Libraries

Das Setup des Projektes habe ich relativ simple gemacht. Alle Einstellungen / Settings werden direkt im Script gemacht. Lediglich die Liste an URLs die gecheckt werden sollen sind in einer separaten Datei (urllist.json). Die Ergebnisse, Screenshots und die ursprüngliche Antwort der Pagespeed Insights API werden in einem Testresults-Ordner gespeichert. Hier könnt ihr es euch herunterladen: https://github.com/huber-michael/pagespeed-insight-api-python

Alle Libraries findet ihr in der Datei requirements.txt und ihr könnt sie per

pip install -r requirements.txt

installieren. Am besten verwendet ihr für euer Setup PyCharm, da sich damit Projekte perfekt anlegen und auch z.B. verschieden Python Versionen festlegen lassen.

Nun zum eigentlichen Skript. Im ersten Schritt werden alle Libraries inkludiert, die wir benötigen.

import requests
import json
import os
from binascii import a2b_base64
from pathlib import Path
from urllib.parse import urlparse
from urllib import request
import pandas as pd
import traceback
from datetime import date

Pagespeed Insights API – Settings

Danach geht es mit den Einstellungen weiter.

  1. printout: Hier könnt ihr festlegen, ob es eine Ausgabe im Terminal geben soll. Gut für schnelle Adhoc-Checks
  2. usecache: Die Ergebnisse der Lighthouse-API werden pro Tag einmal gespeichert. Solltet ihr die Abfrage noch einmal durchführen, wird die gespeicherte Response verwendet. Habt ihr z.B Änderungen durchgeführt und wollt diese nun neu checken, setzt ihr den Wert einfach auf False
  3. categorie-list: Hier legt ihr fest, welche Tests die Lighthouse-API durchführen soll.
  4. detailsexport: Diese Liste legt fest, für welche Audits detailierte Infos in ein eigenes Excel-Blatt gespeichert werden sollen
  5. strategy: desktop oder mobile, da Google aber mittlerweile die meisten Seiten auf mobile-first umgestellt hat, solltet ihr primär mobile verwenden.
  6. api-key: Solltet ihr sehr viele Abfragen senden wollen so solltet ihr euch einen API-Key zulegen.
##### SETTINGS ######

printoutput = False #if you want some terminal output just set printout to True
usecache = True #if set to true, saved responses will be used for the current date.
# What Categories to check
categorieslist = ('seo','performance','best-practices')#'accessibility','pwa')
# For which audits to export detailed info
detailsexport = ('resource-summary','network-requests','total-byte-weight','unused-css-rules','errors-in-console','bootup-time','mainthread-work-breakdown','dom-size','unminified-javascript','uses-long-cache-ttl','tap-targets','deprecations','render-blocking-resources','third-party-summary')
# Strategy (mobile or desktop), since google switched to mobile first set to mobile
strategy = 'mobile'
# If you do regular request and automate it, go get an API-Key and put it there
apikey = '' #'&key=YOURKEY'

##### END ######

Execute the Script

Sind alle Settings festgelegt kann das Script durchlaufen. Dabei wird der Request für jede URL die in der urllist.json hinterlegt ist, durchgeführt.

python pagespeedapi.py

Die Testergebnisse findet ihr dann im entsprechenden Ordner (genau Pfad wird euch im Terminal genannt). Für einen Test von https://www.google.de wäre der relative Pfad ./testresults/www-google-de/2019-10-04/

Dort sind dann

  1. Der von google gerenderte finale Screenshot
  2. Thumbnails der Seite während des Ladevorgangs
  3. Eine Excel-Datei mit den wichtigsten Metriken und Audits.

Damit ihr nich alles per Copy & Paste machen müsst, habe ich ein Github-Repo angelegt. Hier könnt ihr auch die Datein ganz einfach laden.

Dies ist die erste Version des Skriptes und ich werde sicher noch weiter daran arbeiten. Die aktuellen Ergbisse aus der API sind aber schon sehr hilfreich um Problem zu erkennen und eine erste Einschätzung der Performance vornehmen zu können.

SEO QA & Monitoring mit Python – Teil 2 – Indexation Status

Nichts ist schlimmer im SEO Bereich als Seiten die auf einmal aus dem Index verschwinden oder gar nicht erst aufgenommen werden. Oft hängt dies mit technischen Fehlern zusammen. So hat sich vielleicht ein meta robots noindex eingeschlichen oder die robots.txt wurde angepasst ohne die Auswirkung auf verschieden URLs zu testen.

Mit dem folgenden Script könnte ihr für euer definiertes Set an URLs relativ einfach herausfinden, ob diese im Google-Index sein dürfen. Zudem erfahrt ihr für Seiten die auf noindex stehen, wieso dies so ist. Die folgenden Ausführungen basieren auf dem ersten Teil dieser Reihe. Sollten ihr das Grundsetup noch nicht haben, sollten ihr es dort kurz nachlesen.

Exkurs: Es gibt prinzipiell zwei einfache Methoden um eine Seite auf noindex zu setzen. Über eine meta robots Anweisung im HTML Quelltext sowie über den x-robots Header (diese kommt jedoch eher selten vor und kann z.B zum deindexieren von PDFs oder Bilder gut verwendet werden. Außerdem kann mit der robots.txt verhindert werden, dass der GoogleBot die Seite crawled (auf die inoffizielle noindex Funktion gehe ich nicht weiter ein). Dies stellt aber nicht sicher, dass die Seite nicht im Google-Index landet. Das canonical Link Element lasse ich einmal außen vor, da es nur einen Hinweis darstellt, es aber von Google ignoriert werden kann.

Diese drei Faktoren müssen wir also überprüfen um eine valide Aussage treffen zu können ob Google unsere gewünschte URL crawlen und indexieren kann.

Projekt-Setup und Libraries

Um unser Projekt für diese Aufgabe vorzubereiten müssen wir zwei weitere Libraries installieren. Zum einen installieren wir lxml um den HTML-Quelltext zu analysieren. Zum Anderen benötigen wir robotexclusionrulesparser damit wir die robots.txt nicht selber zerlegen und analysieren müssen.

Die Installation erfolgt wieder einfach mit pip

pip install robotexclusionrulesparser
pip install lxml

Jetzt haben wir alle Vorbereitungen fertig und können uns an das Testen von URLs machen.

import requests
import json
from lxml import html
from urllib.parse import urlparse
import robotexclusionrulesparser

with open('urllist.json') as json_file:
    data = json.load(json_file)
    for url in data['urls']:
        response = requests.get(url, allow_redirects=True)
        content = html.fromstring(response.text)
        indexable = True
        reason = ''
        response = requests.get(url, allow_redirects=True)
        if len(response.history) > 0:
            print (f'{url} redirected:')
            redirectindex = 1
            for history in response.history:
                print(f'{redirectindex}. Schritt: {history.url} leitet um auf {history.headers["location"]} mit Statuscode {history.status_code}')
                redirectindex += 1
            print(f'Finale URL ist {response.url} mit Statuscode {response.status_code}')
        elif response.status_code != 200:
            print(f'Finale URL ist {response.url} mit Statuscode {response.status_code} --> Indexation not possible')
            continue
            # Not a valid URL, no reason to check Indexation-Status
        print(f'Checking Indexation-Status of {response.url}:')
        #Build the path to the robots.txt file and download it
        robotsloc = urlparse(url)
        robotspath = robotsloc.scheme + "://"+robotsloc.netloc+"/robots.txt"
        robotstxt = robotexclusionrulesparser.RobotExclusionRulesParser()
        robotstxt.fetch(robotspath, timeout=None)

        #Check if urrent url is blocked via robots.txt
        if not (robotstxt.is_allowed('Googlebot',url)):
            indexable = False
            reason = reason + 'robots.txt (disallow) '

        #Check if current url is blocked via robotsmeta
        robotsmeta = content.xpath("//meta[@name='robots']/@content")
        if len(robotsmeta) > 0:
             if 'noindex' in robotsmeta[0]:
                 indexable = False
                 reason = reason + 'robots-meta (noindex) '

        #Check if current url is blocked via X-Robots-Tag
        if(response.headers.get('X-Robots-Tag', False)):
            if 'noindex' in response.headers['X-Robots-Tag']:
                indexable = False
                reason = reason + 'X-Robots-Tag (noindex) '

        if indexable:
            print (f'{response.url} can be indexed')
        else:
            print(f'{response.url} is not allowed to be indexed/crawled due to {reason}')

Das Skript mach nun folgende Dinge. Zuerst laden wir den Inhalt der URL herunter. Redirects folgen wir erst einmal und sehen uns die Ergebnisse für die finale Zielseite an. Danach müssen wir auch noch die Robots.txt der Zielseite laden. Die entsprechende URL bauen wir uns zusammen, damit wir jegliche Art von URL crawlen können und nicht immer den Pfad zur robots.txt mitgeben müssen. Dann testen wir gegen robots.txt, meta-robots und X-Robots-Tag

Disallowed durch robots.txt

        if not (robotstxt.is_allowed('Googlebot',url)):
            indexable = False
            reason = reason + 'robots.txt (disallow) '

Die meiste Arbeit nimmt uns der robotsexlcusionrulesparser ab. Wir müssen eigentlich nur noch einen User-Agent (in diesem Fall Googlebot) bestimmen und die URL gegen das Set an Seiten testen.

Noindex mit robots-meta Tag

        robotsmeta = content.xpath("//meta[@name='robots']/@content")
        if len(robotsmeta) > 0:
             if 'noindex' in robotsmeta[0]:
                 indexable = False
                 reason = reason + 'robots-meta (noindex) '

Für den robots-meta Tag müssen wir den Quelltext der Seite analysieren. Dazu verwenden wir xpath um die Existenz des Tags herauszufinden. Ist dieses vorhanden checken für auf noindex im Tag.

Noindex mit X-Robots-Tag

        if(response.headers.get('X-Robots-Tag', False)):
            if 'noindex' in response.headers['X-Robots-Tag']:
                indexable = False
                reason = reason + 'X-Robots-Tag (noindex) '

Der letzte Check überprüft den Response-Header auf einen X-Robots-Tag in Kombination mit noindex. Dadurch das es viele Varianten gibt, gehe ich hier den einfachen Weg und checke ob dieser gesendet wird und ob noindex vorkommt. Hier könnten wir False-Positives haben, da man theoretisch mehrere X-Robots-Header senden kann und so für Google-Bot einen mit Index-Anweisung und für andere Bots einen mit Noindex senden könnte. Diesen Edge-Case möchte ich aber an dieser Stelle nicht weiter beleuchten, da der X-Robots-Tag eher selten eingesetzt wird.

Testen unserer Lösung

Testen wir das Ganze nun wieder mit der URL-Liste sehen wir, dass http://de.wikipedia.org Weiterleitungen beinhalten und dass die finale URL indexierbar ist.

http://de.wikipedia.org/ redirected:
1. Schritt: http://de.wikipedia.org/ leitet um auf https://de.wikipedia.org/ mit Statuscode 301
2. Schritt: https://de.wikipedia.org/ leitet um auf https://de.wikipedia.org/wiki/Wikipedia:Hauptseite mit Statuscode 301
Finale URL ist https://de.wikipedia.org/wiki/Wikipedia:Hauptseite mit Statuscode 200
Checking Indexation-Status of https://de.wikipedia.org/wiki/Wikipedia:Hauptseite:
https://de.wikipedia.org/wiki/Wikipedia:Hauptseite can be indexed

Nun wollen wir aber auch noch testen, ob der Robots-Parser vernünftig arbeitet. Dafür sehen wir uns die Robots.txt von Wikipedia einmal genauer an.

# robots.txt for http://www.wikipedia.org/ and friends
#
# Please note: There are a lot of pages on this site, and there are
# some misbehaved spiders out there that go _way_ too fast. If you're
# irresponsible, your access to the site may be blocked.
#

# Observed spamming large amounts of https://en.wikipedia.org/?curid=NNNNNN
# and ignoring 429 ratelimit responses, claims to respect robots:
# http://mj12bot.com/
User-agent: MJ12bot
Disallow: /

# advertising-related bots:
User-agent: Mediapartners-Google*
Disallow: /

# Wikipedia work bots:
User-agent: IsraBot
Disallow:

User-agent: Orthogaffe
Disallow:

# Crawlers that are kind enough to obey, but which we'd rather not have
# unless they're feeding search engines.
User-agent: UbiCrawler
Disallow: /
....

Die Robots.txt von Wikipedia ist sehr ausführlich und z.B. der UbiCrawler wird von der kompletten Domain ausgeschlossen. Perfekt für einen Test. in unserem Script setzen wir nun den Useragent auf eben diese UbiCrawler

        if not (robotstxt.is_allowed('UbiCrawler',url)):
            indexable = False
            reason = reason + 'robots.txt (disallow) '

Lassen wir unser Script nun erneut laufen, sehen wir, dass der Noindex durch die Robots.txt erfolgreich erkannt wurde.

Checking Indexation-Status of https://de.wikipedia.org/wiki/Wikipedia:Hauptseite:
https://de.wikipedia.org/wiki/Wikipedia:Hauptseite is not allowed to be indexed due to robots.txt (disallow) 

SEO QA & Monitoring mit Python – Teil 1 – Status Codes

Vor allem im SEO Bereich muss man oft viele Seiten im Überblick behalten. Natürlich gibt es eine Vielzahl an Tools die einem diese Aufgabe abnehmen. Wenn man aber nicht viel Budget übrig hat, kann es sich durchaus lohnen, diese Aufgaben selbst zu automatisieren.

Im Folgenden möchte ich meinen Ansatz teilen, Seiten mit Python zu monitoren und gängige SEO-Fehler einfach und schnell zu finden.

Disclaimer: Ich bin kein Entwickler und auch kein Python-Profi, daher sind die vorgestellten Methoden vermutlich nicht die Besten und Schönsten, aber sie funktionieren. Da ich auch immer noch weiter lerne, werden die Scripte natürlich bei Bedarf erweitert / angepasst.

Virtuelle Umgebung einrichten

Im ersten Schritt betrachten wir das ganze als lokales Python-Script welches auf meinem Mac läuft und on demand ausgeführt werden kann. Das Setup ist relativ einfach. Ihr benötigt Python (ich verwende 3.6 es geht aber auch mit 3.7) und am besten gleich eine passende IDE. Ich verwende PyCharm, da damit das Einrichten einer virtuellen Umgebung relativ einfach ist und die Community-Edition sogar kostenlos ist.

Sobald das Grundsetup steht, kann man ein neues Projekt anlegen.

PyCharm Create Project Screen

Nun könnt ihr euch eine neue Datei (Python-File) anlegen, welche uns als Startpunkt dient. Nennen wir sie einfach seq-qa.py. Im ersten Schritt holen wir uns nun einfach mal den Inhalt einer URL. Dafür benötigen wir die Requests Library welche wir mit pip einfach installieren können. Dafür im Terminal (integriert in PyCharm) folgenden Befehl absetzen.

pip install requests

Danach können wir die Requests Library verwenden indem wir es mit import requests importieren. Damit haben wir das Grundgerüst fertig um einen URL abzufragen und den Status-Code dieser URL herauszufinden. Ein simples Script dafür würde wie folgt aussehen:

import requests

url = 'https://www.google.de'
response = requests.get(url)
print(response.status_code)

Führt man dieses mit python seo-qa.py aus, erhält man 200 zurück. Sprich, die Seite ist erreichbar. Will man nur den Status-Code der Seite erhalten, reicht außerdem ein einfacher head-request, das gesamte HTML-Dokument muss in diesem Fall nicht geladen werden.

import requests

url = 'https://www.google.de'
response = requests.head(url)
print(response.status_code)

Batch-Check von mehreren URLs

Hier checken wir nur eine URL. In der Praxis hat man aber meist ein ganzes Set an URLs die man gerne prüfen möchte. Daher wollen wir nun 5 URLs auf einmal überprüfen. Für dieses Vorhaben legen wir uns nun eine neue Datei an. Diese nennen wir einfach urllist.json. In diese packen wir nun die gewünschten URLs:

{
  "urls": [
    "https://www.google.com",
    "https://www.youtube.com",
    "https://www.amazon.de",
    "https://de.wikipedia.org",
    "https://www.ebay.de"
  ]
}

Um die json-Datei verarbeiten zu können, müssen wir nun zusätzlich die json Library importieren

import requests
import json

with open('urllist.json') as json_file:
    data = json.load(json_file)
    for url in data['urls']:
        response = requests.get(url)
        print(f'{url} returns status-code {response.status_code}')

Danach öffnen wir die urllist und iterieren über die einzelnen URLs. Die Ausgabe sieht nun wie folgt aus.

https://www.google.com returns status-code 200
https://www.youtube.com returns status-code 200
https://www.mozilla.org returns status-code 200
https://de.wikipedia.org returns status-code 200
https://www.ebay.de returns status-code 200

Wir sehen also, dass alle getesteten Seiten erreichbar sind. Wenn man die Liste nun mit den eigenen URLs befüllt, kann man relativ einfach sehen, ob die Seiten online erreichbar sind. Mit dieser Methode kann man auch sehr schön sehen, ob es eventuell Redirects gegeben hat bzw. ob die URLs die man überprüft redirecten. Requests speichert Redirects in eine History und so kann man alle Schritte nachvollziehen.

Dafür reduzieren wir erst einmal die URL-Liste auf eine URL und verwenden

{
  "urls": [
    "http://de.wikipedia.org"
  ]
}

Redirects nachverfolgen und Ketten finden

Wie ihr seht, haben wir nun die non-secure Version von Wikipedia. Wenn wir unsere Script ausführen, sehen wir wieder einen Status-Code 200. In Wahrheit wird aber ein Redirect durchgeführt.

Wenn wir unser Skript leicht anpassen, können wir diese Fälle jedoch auch abfangen und die einzelnen Redirect-Schritte sowie Statuscodes anzeigen lassen

import requests
import json

with open('urllist.json') as json_file:
    data = json.load(json_file)
    for url in data['urls']:
        response = requests.get(url, allow_redirects=True)
        if len(response.history) > 0:
            print (f'{url} redirected:')
            redirectindex = 1
            for history in response.history:
                print(f'{redirectindex}. Schritt: {history.url} leitet um auf {history.headers["location"]} mit Statuscode {history.status_code}')
                redirectindex += 1
            print(f'Finale URL ist {response.url} mit Statuscode {response.status_code}')
        elif response.status_code != 200:
            print(f'Finale URL ist {response.url} mit Statuscode {response.status_code}')

Führen wir unser Script nun erneut aus, sehen wir, dass beim Aufruf von http://de.wikipedia.org sogar zwei redirects durchgeführt werden bis wir bei der finalen URL https://de.wikipedia.org/ angekommen sind.

http://de.wikipedia.org/ redirected:
1. Schritt: http://de.wikipedia.org/ leitet um auf https://de.wikipedia.org/ mit Statuscode 301
2. Schritt: https://de.wikipedia.org/ leitet um auf https://de.wikipedia.org/wiki/Wikipedia:Hauptseite mit Statuscode 301
Finale URL ist https://de.wikipedia.org/wiki/Wikipedia:Hauptseite mit Statuscode 200

Wenn man dieses Redirect-Verhalten unterbinden möchte, kann man Redirects auch deaktivieren beim request.

response = requests.get(url, allow_redirects=False)

Dann würde unser Skript nun folgende Ausgabe erzeugen:

http://de.wikipedia.org returns status-code 301