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)