GPTBot & Co. prüfen: Mit 50 Zeilen Python echte AI-Crawler in Logs verifizieren
AI-Crawler wie GPTBot, OAI-SearchBot oder PerplexityBot tauchen massenhaft in Access-Logs, doch ein User-Agent ist nur ein String und leicht fälschbar. Mit rund 50 Zeilen Python lässt sich per Reverse-DNS-Check prüfen, ob der Traffic wirklich von den Anbietern stammt.

Öffnet man heute ein Access-Log, sieht man einen Zauberstab von AI-Crawlern: GPTBot, OAI-SearchBot, ChatGPT-User, PerplexityBot. Bevor man entscheidet, sie zu blockieren, zuzulassen oder zu feiern, lohnt sich eine unspektakuläre Frage: Sind sie überhaupt echt?
Ein User Agent ist nur ein String. Jeder Scraper kann OAI-SearchBot/1.3 senden und darauf hoffen, dass die eigene Firewall ihn ungeprüft durchlässt. Die gute Nachricht ist, dass die Unternehmen hinter den großen Crawlern die von ihnen tatsächlich genutzten IP-Bereiche veröffentlichen, sodass man das überprüfen kann.
Jede dieser Dateien ist ein einfaches JSON mit einer prefixes-Liste aus ipv4Prefix- bzw. ipv6Prefix-Einträgen (alle am 26. Oktober 2026 geprüft):
Google dokumentiert für Googlebot außerdem eine DNS-Methode (Reverse-Lookup der IP, Prüfung, ob der Hostname auf googlebot.com oder google.com endet, anschließend Forward-Auflösung zurück zur selben IP). Die JSON-Liste ist für Sammelprüfungen einfacher, daher nutzt das Skript diesen Weg.
Nur die Standardbibliothek wird verwendet. Das Skript liest ein Log im Combined-Format ( dem nginx- und Apache-Standard ), filtert die Anfragen heraus, deren User Agent einen dieser Crawler nennt, und prüft die Client-IP gegen die veröffentlichten Bereiche des jeweiligen Crawlers.
""" Check which " AI crawler " hits in an access log really come from the crawler ' s owner. Usage: python3 verify_ai_bots.py access.log Reads a combined-format log (nginx/Apache default), finds requests whose user agent names a known crawler, and checks the client IP against the ranges the owner publishes. """ import ipaddress import json import re import sys import urllib.request from collections import Counter RANGES = { " OAI-SearchBot ": " https://openai.com/searchbot.json ", " GPTBot ": " https://openai.com/gptbot.json ", " ChatGPT-User ": " https://openai.com/chatgpt-user.json ", " PerplexityBot ": " https://www.perplexity.com/perplexitybot.json ", " Perplexity-User ": " https://www.perplexity.com/perplexity-user.json ", " Googlebot ": " https://developers.google.com/static/search/apis/ipranges/googlebot.json ", } LINE = re. compile ( r ' ^(\S+).*?
" [A-Z]+ (\S+)[^ " ]* " (\d{3}) \S+ " [^ " ]* " " ([^ " ]*) "' ) def load_networks ( url ): with urllib. request. urlopen ( url, timeout = 20 ) as r: data = json. load ( r ) return [ ipaddress. ip_network ( p. get ( " ipv4Prefix " ) or p. get ( " ipv6Prefix " )) for p in data [ " prefixes " ]] def main ( path ): nets = { bot: load_networks ( url ) for bot, url in RANGES. items ()} results = Counter () with open ( path, encoding = " utf-8 ", errors = " replace " ) as f: for line in f: m = LINE. match ( line ) if not m: continue ip, url, status, agent = m. groups () bot = next (( b for b in RANGES if b. lower () in agent. lower ()), None ) if not bot: continue addr = ipaddress. ip_address ( ip ) real = any ( addr in n for n in nets [ bot ] if n. version == addr. version ) results [( bot, " verified " if real else " SPOOFED ", status )] += 1 for ( bot, verdict, status ), n in sorted ( results. items ()): print ( f " { bot: 16 } { verdict: 9 } HTTP { status } x { n } " ) if __name__ == " __main__ ": main ( sys. argv [ 1 ]) Vollbildmodus öffnen Vollbildmodus schließen So sieht die Ausgabe aus Ich habe das Skript gegen ein von Hand geschriebenes, fünfeiliges Beispiellog laufen lassen: eine Anfrage aus einer IP innerhalb des veröffentlichten OAI-SearchBot-Bereichs von OpenAI, eine Anfrage, die sich als OAI-SearchBot ausgibt, aber aus einer rein dokumentatorischen IP ( 203.0.113.9 ) stammt, eine echte PerplexityBot-IP, eine echte Googlebot-IPv6-Adresse und ein gewöhnlicher Browser. Das Skript hat die aktuellen Listen abgerufen und Folgendes ausgegeben:
Googlebot verified HTTP 200 x1 OAI-SearchBot SPOOFED HTTP 403 x1 OAI-SearchBot verified HTTP 200 x1 PerplexityBot verified HTTP 200 x1 Vollbildmodus öffnen Vollbildmodus schließen Die Browser-Zeile wird ignoriert und der gefälschte OAI-SearchBot wird erkannt. Führt man das Skript auf dem echten Log aus, sind die interessanten Zeilen in der Regel:
Es gibt einige Möglichkeiten, dies über eine einmalige Prüfung hinaus zu nutzen:
Verifizierter Crawler-Traffic beweist, dass eine Suchmaschine eure Seiten lesen kann. Er sagt nicht, ob die Suchmaschine euch nennt, wenn ein Käufer eine Frage stellt. Das ist eine separate Prüfung: Man stellt die Fragen, die die eigenen Käufer stellen, mit aktivierter Suche, und schaut, wer empfohlen wird und welche Seiten zitiert werden.
Diese zweite Hälfte ist das, was wir bei Presence Scout entwickeln: Es verfolgt, ob ChatGPT, Gemini, Perplexity, AI Overviews und AI Mode eure Marke nennen, direkt neben euren Google-Rankings. Diese kurze Demo zeigt, wie das bei einer echten Marke aussieht:
Doch beginnt mit den Logs. Wenn die echten Crawler 403-Fehler erhalten, ist alles andere zunächst egal.
Ich bin Ankit und arbeite im Marketing bei Presence Scout. Das Skript dürft ihr gerne kopieren und anpassen.
Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.
Original (Englisch) anzeigen
Open any access log today and you'll see a parade of AI crawlers: GPTBot, OAI-SearchBot, ChatGPT-User, PerplexityBot. Before you decide to block them, allow them or celebrate them, it's worth asking a boring question: are they real?
A user agent is just a string. Any scraper can send OAI-SearchBot/1.3 and hope your firewall waves it through. The good news is that the companies behind the major crawlers publish the IP ranges they actually use, so you can check.
Each of these files is plain JSON with a prefixes list of ipv4Prefix / ipv6Prefix entries (all checked on 26 October 2026):
Google also documents a DNS method for Googlebot (reverse lookup on the IP, check the hostname ends in googlebot.com or google.com, then forward-resolve it back to the same IP). The JSON list is simpler for batch checks, so that's what the script uses.
Standard library only. It reads a combined-format log (the nginx and Apache default), picks out requests whose user agent names one of these crawlers, and checks the client IP against that crawler's published ranges.
""" Check which " AI crawler " hits in an access log really come from the crawler ' s owner. Usage: python3 verify_ai_bots.py access.log Reads a combined-format log (nginx/Apache default), finds requests whose user agent names a known crawler, and checks the client IP against the ranges the owner publishes. """ import ipaddress import json import re import sys import urllib.request from collections import Counter RANGES = { " OAI-SearchBot ": " https://openai.com/searchbot.json ", " GPTBot ": " https://openai.com/gptbot.json ", " ChatGPT-User ": " https://openai.com/chatgpt-user.json ", " PerplexityBot ": " https://www.perplexity.com/perplexitybot.json ", " Perplexity-User ": " https://www.perplexity.com/perplexity-user.json ", " Googlebot ": " https://developers.google.com/static/search/apis/ipranges/googlebot.json ", } LINE = re. compile ( r ' ^(\S+).*?
" [A-Z]+ (\S+)[^ " ]* " (\d{3}) \S+ " [^ " ]* " " ([^ " ]*) "' ) def load_networks ( url ): with urllib. request. urlopen ( url, timeout = 20 ) as r: data = json. load ( r ) return [ ipaddress. ip_network ( p. get ( " ipv4Prefix " ) or p. get ( " ipv6Prefix " )) for p in data [ " prefixes " ]] def main ( path ): nets = { bot: load_networks ( url ) for bot, url in RANGES. items ()} results = Counter () with open ( path, encoding = " utf-8 ", errors = " replace " ) as f: for line in f: m = LINE. match ( line ) if not m: continue ip, url, status, agent = m. groups () bot = next (( b for b in RANGES if b. lower () in agent. lower ()), None ) if not bot: continue addr = ipaddress. ip_address ( ip ) real = any ( addr in n for n in nets [ bot ] if n. version == addr. version ) results [( bot, " verified " if real else " SPOOFED ", status )] += 1 for ( bot, verdict, status ), n in sorted ( results. items ()): print ( f " { bot: 16 } { verdict: 9 } HTTP { status } x { n } " ) if __name__ == " __main__ ": main ( sys. argv [ 1 ]) Enter fullscreen mode Exit fullscreen mode What the output looks like I ran it against a five-line sample log I wrote by hand: one request from an IP inside OpenAI's published OAI-SearchBot range, one claiming to be OAI-SearchBot from a documentation-only IP ( 203.0.113.9 ), one real PerplexityBot IP, one real Googlebot IPv6 address, and one ordinary browser. The script fetched the live lists and printed:
Googlebot verified HTTP 200 x1 OAI-SearchBot SPOOFED HTTP 403 x1 OAI-SearchBot verified HTTP 200 x1 PerplexityBot verified HTTP 200 x1 Enter fullscreen mode Exit fullscreen mode The browser line is ignored, and the fake OAI-SearchBot is caught. Run it on your real log and the interesting rows are usually:
A few ways I'd use this beyond a one-off check:
Verified crawler traffic proves an engine can read your pages. It doesn't tell you whether the engine names you when a buyer asks a question. That's a separate check: ask the questions your buyers ask, with search on, and look at who gets recommended and which pages get cited.
That second half is what we build at Presence Scout: it tracks whether ChatGPT, Gemini, Perplexity, AI Overviews and AI Mode name your brand, next to your Google rankings. This short demo shows what that looks like on a real brand:
But start with the logs. If the real crawlers are getting 403s, nothing else matters yet.
I'm Ankit, and I work on marketing at Presence Scout. Feel free to copy and adapt the script.