AutoPodAutoPod

Měření dopadu citací AI: Analýza, protokoly a scrapování panelů

•22 min čtení
Audio článek
Měření dopadu citací AI: Analýza, protokoly a scrapování panelů
0:00—:—
Měření dopadu citací AI: Analýza, protokoly a scrapování panelů

Měření dopadu citací AI: Analýza, protokoly a scrapování panelů

Úvod

Objevování řízené umělou inteligencí je nyní rozděleno do několika měřicích vrstev. Člověk může vidět společnost citovanou v odpovědi AI, kliknout na odkaz bez užitečného refereru, navštívit stránku prohlížečem, který odstraňuje data kampaně, nebo si odpověď přečíst, aniž by vůbec klikl. Mezitím může crawler AI vyžádat stránku společnosti, aniž by představoval lidského návštěvníka.

Užitečný měřicí systém proto musí oddělit čtyři otázky:

  1. Byla stránka procházena nebo načtena?
  2. Byla stránka citována v odpovědi AI?
  3. Navštívil stránku z této odpovědi člověk?
  4. Vytvořila tato návštěva smysluplnou interakci nebo příjem?

K 22. září 2026 se zlepšuje reportování z první strany. Google Search Console poskytuje report výkonu generativní AI pro Přehledy AI a Režim AI, včetně zobrazení, stránek, zemí, zařízení a dat. Report se stal celosvětově dostupným 31. srpna 2026. Report výkonu generativní AI od Googlu (support.google.com)

Microsoft také poskytuje report Výkon AI v nástrojích Bing Webmaster Tools. Jeho veřejná ukázka reportuje celkové citace, citované stránky, základní dotazy a trendy citací napříč podporovanými AI zážitky Microsoftu. Výkon AI v Bing Webmaster Tools (blogs.bing.com)

Tyto nástroje by měly být považovány za autoritativní pro své vlastní povrchy, ale nevytvářejí kompletní systém měření napříč asistenty. Doporučený přístup spočívá v kombinaci reportů z první strany, serverových protokolů, řízených sledovacích odkazů, exportů z analytických nástrojů a plánovaných pozorování panelů odpovědí.


1. Proč je měření doporučení AI obtížné

Tradiční webová analytika silně závisí na hlavičce Referer a parametrech kampaně. Obojí může chybět nebo být neúplné.

Hlavička HTTP Referer může obsahovat předchozí stránku, pouze původ předchozího webu, nebo žádnou hodnotu. Zásady refereru prohlížeče mohou záměrně omezit nebo odstranit tyto informace. Dokumentace Referrer-Policy na MDN (developer.mozilla.org)

Systémy AI vytvářejí další komplikace:

  • Asistent může odkazovat na stránku, aniž by odeslal rozpoznatelný referer.
  • Asistent může použít přesměrovanou nebo přepsanou URL adresu.
  • Asistent může načíst obsah na straně serveru před zobrazením odpovědi.
  • Crawler AI může navštívit stránku, aniž by ji viděl jakýkoli člověk.
  • Odpověď může citovat kanonickou URL namísto URL, která byla původně načtena.
  • Odpověď se může měnit v závislosti na poloze, zařízení, jazyce, čase nebo personalizaci.

Například OpenAI rozlišuje mezi OAI-SearchBot pro objevování ve vyhledávání, GPTBot pro možné použití k trénování a ChatGPT-User pro akce iniciované uživatelem. Perplexity samostatně identifikuje PerplexityBot a Perplexity-User. Anthropic identifikuje ClaudeBot, Claude-SearchBot a Claude-User. Tyto požadavky by neměly být kombinovány do jednoho obecného čísla „návštěvnosti AI“. Dokumentace crawlerů OpenAI (openai.com) Dokumentace crawlerů Perplexity (docs.perplexity.ai) Dokumentace crawlerů Anthropic (support.anthropic.com)

Hlavní pravidlo zní:

Aktivita crawleru není doporučená návštěvnost, viditelnost citace není návštěvnost a návštěvnost není obchodní dopad.


2. Rámec měření

Použijte pět měřicích vrstev.

VrstvaHlavní otázkaDoporučená data
PřístupVyžádal si stránku systém AI?Protokoly webového serveru, protokoly firewallu
ViditelnostByla stránka zobrazena nebo citována?Google Search Console, Bing Webmaster Tools, pozorování panelů odpovědí
AkvizicePřišel člověk z povrchu AI?Referer, parametry kampaně, kliknutí na krátký odkaz, analytika
AngažovanostInteragoval návštěvník smysluplně?Události analytiky, zobrazení stránek, doba angažovanosti
HodnotaPřinesla návštěva požadovaný výsledek?Registrace, leady, nákupy, předplatné

Doporučená hierarchie zdrojů pravdy

Pro každou otázku použijte nejsilnější dostupný signál:

  1. Reporty platforem z první strany pro zobrazení a citace.
  2. Pozorování panelů odpovědí pro viditelnost na úrovni dotazů a srovnání s konkurencí.
  3. Protokoly přesměrování pro řízená kliknutí na odkazy.
  4. Webová analytika pro relace, angažovanost a konverze.
  5. Serverové protokoly pro aktivitu crawlerů a načítacích botů.
  6. Modelované odhady pouze v případě, že nejsou k dispozici přímé důkazy.

Nekombinujte všechny signály do jednoho čísla bez zachování zdroje a úrovně spolehlivosti.


3. Základní datový model

Vytvořte datový sklad se samostatnými tabulkami pro surová pozorování a odvozené metriky.

panel_runs

Jeden řádek pro každý dotaz a kontrolu povrchu.

text run_id observed_at surface topic prompt_id query_text locale country device panel_present panel_type answer_hash raw_html_path screenshot_path collector_version status

panel_citations

Jeden řádek pro každý odkaz pozorovaný v panelu odpovědí.

text run_id surface topic prompt_id cited_url cited_domain citation_position link_scope target_domain target_cited

ai_crawler_requests

Agregovaná data serverových protokolů.

text date bot_name bot_role path requests successful_requests error_requests unique_ip_count

shortener_clicks

Jeden řádek pro každý požadavek řízeného odkazu.

text click_id occurred_at slug surface topic prompt_id asset_id referrer_host user_agent_family ip_hmac destination_path

ai_sessions

Normalizovaná analytická tabulka.

text session_id date surface topic prompt_id landing_page attribution_method confidence engaged engagement_time_seconds key_event revenue

Pole confidence je důležité. Doporučené hodnoty jsou:

  • A — Přímá: řízené kliknutí na krátký odkaz nebo důvěryhodný parametr kampaně.
  • B — Silná: známý referer asistenta nebo ověřený požadavek na načtení uživatelem.
  • C — Pravděpodobná: důkazy z kampaně nebo vstupní stránky bez úplného refereru.
  • D — Modelovaná: odvozená z časování, pozorování panelů nebo agregovaných změn.

Reportujte A a B odděleně od C a D.


4. Definice metrik

Podíl citací dle tématu

Podíl citací dle tématu měří, jak často se cílová doména objevuje mezi zdroji pozorovanými pro dané téma.

[ \text{Citation Share}_{topic}

\frac{\text{Target-domain citation occurrences}} {\text{All citation occurrences}} ]

Příklad:

  • Celkem 100 citací pozorovaných pro „software pro řízení projektů“
  • 12 citací odkazuje na vaši doménu
  • Podíl citací dle tématu = 12%

Tato metrika měří podíl zdroje, nikoli to, zda odpověď obsahovala vaši stránku alespoň jednou.

Pokrytí citacemi

Použijte druhou metriku pro přítomnost na úrovni odpovědí:

[ \text{Citation Coverage}_{topic}

\frac{\text{Answer runs containing a target citation}} {\text{Answer runs where a panel was present}} ]

Pokrytí citacemi je pro netechnické publikum často snáze pochopitelné:

„Naše stránky se objevily ve 34 % pozorovaných odpovědí AI pro toto téma.“

Pozice citace

Zaznamenejte pozici citace v rámci odpovědi:

  • 1 = první pozorovaný zdroj
  • 2 = druhý pozorovaný zdroj
  • 3 = třetí pozorovaný zdroj

Protože rozhraní AI často seskupují nebo mění pořadí zdrojů, považujte pozici za popisné pozorování, nikoli za univerzální pořadí.


Míra doporučení asistentem

Míra doporučení asistentem měří podíl lidských relací na webu, které lze připsat povrchu AI.

[ \text{Assistant Referral Rate}

\frac{\text{AI-attributed human sessions}} {\text{All human sessions}} ]

Reportujte alespoň dvě verze:

text Potvrzená míra doporučení asistentem = relace AI se spolehlivostí A / všechny lidské relace

Rozšířená míra doporučení asistentem = relace AI se spolehlivostí A + B + C / všechny lidské relace

Potvrzená verze je konzervativnější. Rozšířená verze je užitečná pro analýzu trendů, ale neměla by být prezentována jako plně pozorovaná návštěvnost.

Nepoužívejte spuštění panelů jako zobrazení

Pokud scrapujete 500 dotazů, neznamená to, že těchto 500 odpovědí vidělo 500 lidí. Váš scraper měří řízený vzorek stavů odpovědí. Použijte reporty zobrazení platforem z první strany, pokud jsou k dispozici.


Angažovanost po citaci

Angažovanost po citaci měří, zda lidé, kteří přišli prostřednictvím citace AI, provedli smysluplnou akci.

[ \text{Post-Citation Engagement}

\frac{\text{Engaged AI sessions}} {\text{AI-attributed sessions}} ]

Pro Google Analytics 4 trvá angažovaná relace déle než 10 sekund, zahrnuje klíčovou událost nebo zahrnuje alespoň dvě zobrazení stránky či obrazovky. Dokumentace míry angažovanosti Google Analytics (support.google.com)

Také reportujte:

text Doba angažovanosti po citaci = medián angažovaných sekund na relaci AI

Konverzní poměr po citaci = relace AI s klíčovou událostí / relace připsané AI

Příjem z relace po citaci = příjem z relací AI / relace připsané AI

Medián doby angažovanosti je obvykle stabilnější než průměrná doba angažovanosti, protože malý počet velmi dlouhých relací může průměr zkreslit.


Další užitečné metriky

Míra úspěšnosti crawleru AI

[ \text{Crawler Success Rate}

\frac{\text{AI crawler requests returning 2xx or 3xx}} {\text{AI crawler requests}} ]

Rozdělte to podle bota a cesty. Vysoký pokles citací v kombinaci s rostoucí chybovostí crawleru může naznačovat problém s hostingem, firewallem nebo konfigurací robotů.

Míra prokliku z citace

Pro řízená pozorování panelů odpovědí:

[ \text{Observed Citation-to-Click Rate}

\frac{\text{Confirmed short-link clicks}} {\text{Observed target citations}} ]

Toto není skutečná míra prokliku uživatele, pokud jmenovatel nepředstavuje skutečné uživatelské zobrazení. Jasně ji označte jako pozorovanou provozní metriku.

Aktuálnost citace

Měřte, jak často citovaná URL odráží aktuální verzi stránky.

text Aktuálnost citace = citované stránky s aktuálním hash kódem obsahu / citované stránky

To je užitečné pro stránky s dokumentací, cenami, produkty a zásadami, které se často mění.


5. Analýza serverových protokolů

Konfigurace strukturovaných přístupových protokolů

Strukturované protokoly se snáze analyzují než tradiční protokoly oddělené mezerami.

Příklad konfigurace Nginx:

nginx log_format ai_json escape=json '{' '"ts":"$time_iso8601",' '"remote_addr":"$remote_addr",' '"request":"$request",' '"status":$status,' '"bytes":$body_bytes_sent,' '"referer":"$http_referer",' '"user_agent":"$http_user_agent",' '"host":"$host"' '}';

access_log /var/log/nginx/ai-access.jsonl ai_json;

Ukládejte soubory protokolů na omezeném místě. Neodesílejte nezpracované IP adresy do obecné analytické tabulky, pokud k tomu nemáte jasný provozní důvod.

Identifikace typů požadavků AI

Minimálně oddělte:

text OpenAI:

  • OAI-SearchBot
  • ChatGPT-User
  • GPTBot

Anthropic:

  • Claude-SearchBot
  • Claude-User
  • ClaudeBot

Perplexity:

  • PerplexityBot
  • Perplexity-User

Vyhledávače:

  • Googlebot
  • Bingbot

Funkce vyhledávání AI od Googlu obecně používají stávající ovládací prvky procházení Google Search namísto samostatného univerzálního „bota pro Přehled AI“. Google uvádí, že Googlebot kontroluje přístup pro Vyhledávání, včetně funkcí AI. Funkce a webové stránky AI od Googlu (developers.google.com)

Řetězce user-agent lze padělat. Pro provozní rozhodnutí kombinujte shodu user-agent s rozsahy IP adres zveřejněnými poskytovatelem nebo ověřeným reverzním DNS, kde je k dispozici. OpenAI a Perplexity zveřejňují informace o IP adresách pro své relevantní crawlery a načítací boty. (openai.com)

Reprodukovatelný sumarizátor protokolů

Uložte následující jako ai_log_summary.py.

python #!/usr/bin/env python3

import csv import hashlib import hmac import json import os import re import sys from collections import Counter, defaultdict from datetime import datetime from urllib.parse import urlsplit

BOT_PATTERNS = { "OAI-SearchBot": ("openai", re.compile(r"\bOAI-SearchBot\b", re.I)), "ChatGPT-User": ("openai_user", re.compile(r"\bChatGPT-User\b", re.I)), "GPTBot": ("openai_training", re.compile(r"\bGPTBot\b", re.I)), "Claude-SearchBot": ("anthropic", re.compile(r"\bClaude-SearchBot\b", re.I)), "Claude-User": ("anthropic_user", re.compile(r"\bClaude-User\b", re.I)), "ClaudeBot": ("anthropic_training", re.compile(r"\bClaudeBot\b", re.I)), "PerplexityBot": ("perplexity", re.compile(r"\bPerplexityBot\b", re.I)), "Perplexity-User": ("perplexity_user", re.compile(r"\bPerplexity-User\b", re.I)), "Googlebot": ("google_search", re.compile(r"\bGooglebot\b", re.I)), "Bingbot": ("bing_search", re.compile(r"\bbingbot\b", re.I)), }

SECRET = os.environ.get("HMAC_SECRET", "replace-this-in-production").encode()

def hmac_ip(ip): return hmac.new(SECRET, ip.encode(), hashlib.sha256).hexdigest()[:16]

def classify_bot(user_agent): for bot_name, (role, pattern) in BOT_PATTERNS.items(): if pattern.search(user_agent or ""): return bot_name, role return None, None

def parse_path(request_line): try: parts = request_line.split() return urlsplit(parts[1]).path except Exception: return "/"

def main(input_path): totals = defaultdict(lambda: { "requests": 0, "success": 0, "errors": 0, "ips": set(), "paths": Counter(), })

with open(input_path, "r", encoding="utf-8") as source:
    for line in source:
        try:
            record = json.loads(line)
        except json.JSONDecodeError:
            continue

        bot_name, bot_role = classify_bot(record.get("user_agent", ""))
        if not bot_name:
            continue

        timestamp = record.get("ts", "")
        date = datetime.fromisoformat(
            timestamp.replace("Z", "+00:00")
        ).date().isoformat()

        status = int(record.get("status", 0))
        path = parse_path(record.get("request", ""))
        key = (date, bot_name, bot_role)

        totals[key]["requests"] += 1
        totals[key]["ips"].add(hmac_ip(record.get("remote_addr", "")))
        totals[key]["paths"][path] += 1

        if 200 <= status < 400:
            totals[key]["success"] += 1
        else:
            totals[key]["errors"] += 1

writer = csv.writer(sys.stdout)
writer.writerow([
    "date",
    "bot_name",
    "bot_role",
    "requests",
    "successful_requests",
    "error_requests",
    "unique_ip_count",
    "top_paths",
])

for (date, bot_name, bot_role), values in sorted(totals.items()):
    top_paths = values["paths"].most_common(10)
    writer.writerow([
        date,
        bot_name,
        bot_role,
        values["requests"],
        values["success"],
        values["errors"],
        len(values["ips"]),
        json.dumps(top_paths),
    ])

if name == "main": if len(sys.argv) != 2: print("Usage: python ai_log_summary.py /path/to/access.jsonl") sys.exit(1)

main(sys.argv[1])

Spusťte to s:

bash HMAC_SECRET='a-long-random-secret'
python ai_log_summary.py /var/log/nginx/ai-access.jsonl \

data/ai_crawler_daily.csv

To vytváří provozní důkazy o aktivitě načítání, nikoli o lidské návštěvnosti.


6. Vlastní zkracovače odkazů pro souhrny směřující k AI

Zkracovač odkazů z první strany poskytuje řízený měřicí bod, když publikujete souhrny nebo zdrojové materiály, které mohou být referencovány systémy AI.

Použijte doménu, kterou ovládáte, například:

text https://go.example.com/ai-docs

Cíl by měl zahrnovat parametry kampaně, jako jsou:

text utm_source=perplexity utm_medium=ai_answer utm_campaign=product_documentation utm_content=pricing_summary_v3

Google doporučuje parametry kampaně pro identifikaci zdrojů návštěvnosti a kampaní. Dokumentace URL kampaně Google Analytics (support.google.com)

Doporučená pole odkazu

text surface topic prompt_id asset_id content_version destination_path

Nevkládejte do odkazu osobní údaje, e-mailové adresy ani uživatelské vyhledávací dotazy. Identifikátory kampaně by měly popisovat publikovaný asset, nikoli identifikovat osobu.

Důležitá omezení

Krátký odkaz je kalibrační zařízení, nikoli univerzální řešení:

  • Asistent může přepsat cílovou URL adresu.
  • Asistent může místo toho citovat kanonickou URL adresu.
  • Odkaz může být zobrazen, ale nikdy na něj nebude kliknuto.
  • Prohlížeč nebo nástroj pro ochranu soukromí může odstranit referer.
  • Některé systémy mohou načíst cíl na straně serveru.
  • Krátký odkaz neměří uživatele, kteří si značku zapamatují a vrátí se později.

Nepoužívejte zkracovač k skrytí cíle, obcházení bezpečnostních systémů nebo manipulaci s citacemi. Použijte jasné přesměrování na relevantní, kanonickou stránku.

Minimální zkracovač odkazů z první strany

Následující příklad používá FastAPI a SQLite. Před použitím v produkci chraňte koncový bod pro vytváření odkazů autentizací.

python

shortener.py

import hashlib import hmac import os import secrets import sqlite3 from datetime import datetime, timezone from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

from fastapi import FastAPI, HTTPException, Request from fastapi.responses import RedirectResponse from pydantic import BaseModel

DB_PATH = os.environ.get("SHORTENER_DB", "shortener.db") HMAC_SECRET = os.environ.get( "HMAC_SECRET", "replace-this-in-production" ).encode()

ALLOWED_HOST = "www.example.com"

app = FastAPI()

def db(): connection = sqlite3.connect(DB_PATH) connection.row_factory = sqlite3.Row return connection

def init_db(): connection = db() connection.execute(""" CREATE TABLE IF NOT EXISTS links ( slug TEXT PRIMARY KEY, destination TEXT NOT NULL, surface TEXT NOT NULL, topic TEXT, prompt_id TEXT, asset_id TEXT, created_at TEXT NOT NULL ) """) connection.execute(""" CREATE TABLE IF NOT EXISTS clicks ( id INTEGER PRIMARY KEY AUTOINCREMENT, occurred_at TEXT NOT NULL, slug TEXT NOT NULL, surface TEXT, topic TEXT, prompt_id TEXT, referrer_host TEXT, user_agent TEXT, ip_hmac TEXT ) """) connection.commit() connection.close()

def append_query(url, params): parsed = urlsplit(url) query = dict(parse_qsl(parsed.query, keep_blank_values=True)) query.update(params) return urlunsplit(( parsed.scheme, parsed.netloc, parsed.path, urlencode(query), parsed.fragment, ))

def hash_ip(ip): return hmac.new( HMAC_SECRET, (ip or "").encode(), hashlib.sha256 ).hexdigest()[:16]

class LinkRequest(BaseModel): destination_path: str surface: str topic: str | None = None prompt_id: str | None = None asset_id: str | None = None

@app.on_event("startup") def startup(): init_db()

@app.post("/links") def create_link(payload: LinkRequest): destination = f"https://{ALLOWED_HOST}{payload.destination_path}"

if not payload.destination_path.startswith("/"):
    raise HTTPException(400, "destination_path must be a site-relative path")

slug = secrets.token_urlsafe(7).replace("_", "-").replace("/", "-")

target = append_query(destination, {
    "utm_source": payload.surface,
    "utm_medium": "ai_answer",
    "utm_campaign": "citation_measurement",
    "utm_content": payload.asset_id or "unspecified",
})

connection = db()
connection.execute("""
    INSERT INTO links
    (slug, destination, surface, topic, prompt_id, asset_id, created_at)
    VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
    slug,
    target,
    payload.surface,
    payload.topic,
    payload.prompt_id,
    payload.asset_id,
    datetime.now(timezone.utc).isoformat(),
))
connection.commit()
connection.close()

return {"slug": slug, "url": f"https://go.example.com/{slug}"}

@app.get("/{slug}") def redirect(slug: str, request: Request): connection = db() row = connection.execute( "SELECT * FROM links WHERE slug = ?", (slug,) ).fetchone()

if not row:
    connection.close()
    raise HTTPException(404, "Unknown link")

referrer = request.headers.get("referer", "")
referrer_host = urlsplit(referrer).hostname or ""

connection.execute("""
    INSERT INTO clicks
    (occurred_at, slug, surface, topic, prompt_id,
     referrer_host, user_agent, ip_hmac)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
    datetime.now(timezone.utc).isoformat(),
    slug,
    row["surface"],
    row["topic"],
    row["prompt_id"],
    referrer_host,
    request.headers.get("user-agent", "")[:300],
    hash_ip(request.client.host if request.client else ""),
))
connection.commit()
connection.close()

return RedirectResponse(row["destination"], status_code=302)

Spusťte to s:

bash pip install fastapi uvicorn uvicorn shortener:app --host 0.0.0.0 --port 8080

Použijte přesměrování 302 pro měřicí odkazy, aby prohlížeče a mezilehlé systémy méně pravděpodobně trvale cachovaly přesměrování.


7. Plánované scrapování panelů odpovědí AI

Scrapování panelů je užitečné pro zodpovězení otázek, které analytika z první strany nemusí odhalit:

  • Které konkurenční zdroje jsou citovány?
  • Která stránka je citována pro konkrétní dotaz?
  • Mění se odpověď podle země nebo zařízení?
  • Objevuje se citace v odpovědi, v seznamu zdrojů, nebo v obojím?
  • Jak stabilní je odpověď v čase?
  • Cituje asistent krátký odkaz nebo kanonickou URL?

Scrapování však musí být prováděno opatrně.

Pravidla pro zodpovědný sběr

  1. Upřednostněte oficiální API, licencovaného poskytovatele vyhledávacích dat nebo report z první strany.
  2. Před automatizací požadavků si prostudujte podmínky poskytovatele.
  3. Respektujte pokyny robotů, kde je to relevantní.
  4. Neobcházejte CAPTCHA, přihlašovací bariéry, omezení rychlosti nebo ochrany proti botům.
  5. Použijte malou, pevnou sadu dotazů.
  6. Spouštějte s nízkou frekvencí.
  7. Uložte odpověď a časové razítko, aby bylo možné výsledky auditovat.
  8. Zastavte, když poskytovatel vrátí výzvu nebo odpověď „přístup odepřen“.

Protokol pro vyloučení robotů je mechanismus preferencí crawleru, nikoli bezpečnostní systém. Neautorizuje přístup k chráněnému obsahu. RFC 9309: Protokol pro vyloučení robotů (rfc-editor.org)

Vytvořte manifest dotazů

csv prompt_id,topic,query,locale,surface,target_domain pm_001,project-management,"best project management software for remote teams",en-US,google,example.com pm_001,project-management,"best project management software for remote teams",en-US,bing,example.com pm_002,analytics,"how to measure AI referral traffic",en-US,google,example.com

Udržujte dotazy stabilní. Pokud se dotazy změní, srovnání trendů se stane méně spolehlivým.

Uložte také:

  • země
  • jazyk
  • typ zařízení
  • verze prohlížeče
  • verze dotazu
  • verze obsahu
  • čas sběru v UTC

Google poznamenává, že odpovědi AI Overview a AI Mode se mohou lišit, protože používají různé systémy a mohou využívat související vyhledávání nebo „rozšíření dotazů“. Dokumentace funkcí a webových stránek AI od Googlu (developers.google.com)

Skript pro sběr pomocí Playwright

Následující skript je univerzální nástroj pro výzkum. Zaznamenává vykreslenou stránku, snímek obrazovky, odkazy na stránce a text odpovědi. Záměrně neobchází výzvy ani se nepokouší překonat kontrolu přístupu.

python

scrape_panels.py

import asyncio import csv import hashlib import json import os import re from datetime import datetime, timezone from pathlib import Path from urllib.parse import quote_plus, urljoin, urlsplit

from playwright.async_api import async_playwright

DATA_DIR = Path("data/panels") DATA_DIR.mkdir(parents=True, exist_ok=True)

SURFACE_URLS = { "google": "https://www.google.com/search?q={query}&hl=en&gl=us", "bing": "https://www.bing.com/search?q={query}&setlang=en-us", }

PANEL_MARKERS = { "google": ["AI Overview", "AI Mode"], "bing": ["Copilot", "Generative search"], }

BLOCK_MARKERS = [ "captcha", "unusual traffic", "verify you are human", "access denied", ]

def clean_text(value): return re.sub(r"\s+", " ", value or "").strip()

def host_matches(url, target_domain): host = (urlsplit(url).hostname or "").lower() target = target_domain.lower().lstrip("www.") return host == target or host.endswith("." + target)

def page_hash(text): normalized = clean_text(text).encode("utf-8") return hashlib.sha256(normalized).hexdigest()

async def collect_one(page, row): surface = row["surface"] query = row["query"] target_domain = row["target_domain"]

search_url = SURFACE_URLS[surface].format(
    query=quote_plus(query)
)

await page.goto(
    search_url,
    wait_until="domcontentloaded",
    timeout=45_000,
)
await page.wait_for_timeout(2500)

text = await page.locator("body").inner_text()
lowered = text.lower()

if any(marker in lowered for marker in BLOCK_MARKERS):
    return {
        **row,
        "observed_at": datetime.now(timezone.utc).isoformat(),
        "status": "blocked_or_challenged",
        "panel_present": False,
        "links": [],
    }

links = await page.locator("a").evaluate_all("""
    anchors => anchors.map(a => ({
        href: a.href || "",
        text: (a.innerText || "").trim()
    })).filter(x => x.href)
""")

panel_present = any(
    marker.lower() in lowered
    for marker in PANEL_MARKERS.get(surface, [])
)

observed_at = datetime.now(timezone.utc)
run_id = hashlib.sha256(
    f"{surface}|{row['prompt_id']}|{observed_at.isoformat()}".encode()
).hexdigest()[:20]

html_path = DATA_DIR / f"{run_id}.html"
screenshot_path = DATA_DIR / f"{run_id}.png"

await page.screenshot(path=str(screenshot_path), full_page=True)
html_path.write_text(await page.content(), encoding="utf-8")

output_links = []
for position, link in enumerate(links, start=1):
    output_links.append({
        "href": urljoin(search_url, link["href"]),
        "text": clean_text(link["text"])[:300],
        "position": position,
        "target_cited": host_matches(
            link["href"],
            target_domain
        ),
    })

return {
    **row,
    "run_id": run_id,
    "observed_at": observed_at.isoformat(),
    "status": "ok",
    "panel_present": panel_present,
    "answer_hash": page_hash(text),
    "answer_text": clean_text(text)[:50_000],
    "html_path": str(html_path),
    "screenshot_path": str(screenshot_path),
    "link_scope": "page_fallback",
    "links": output_links,
}

async def main(manifest_path): rows = [] with open(manifest_path, newline="", encoding="utf-8") as source: rows = list(csv.DictReader(source))

output_path = DATA_DIR / (
    datetime.now(timezone.utc).strftime("%Y-%m-%d") + ".jsonl"
)

async with async_playwright() as playwright:
    browser = await playwright.chromium.launch(headless=True)
    context = await browser.new_context(
        locale="en-US",
        timezone_id="UTC",
    )
    page = await context.new_page()

    with open(output_path, "w", encoding="utf-8") as output:
        for row in rows:
            try:
                result = await collect_one(page, row)
            except Exception as exc:
                result = {
                    **row,
                    "observed_at": datetime.now(timezone.utc).isoformat(),
                    "status": f"error:{type(exc).__name__}",
                    "panel_present": False,
                    "links": [],
                }

            output.write(json.dumps(result) + "\n")
            output.flush()

            # Keep request volume low.
            await asyncio.sleep(8)

    await browser.close()

if name == "main": asyncio.run(main("prompts.csv"))

Nainstalujte a spusťte:

bash pip install playwright playwright install chromium python scrape_panels.py

Naplánujte to se zámkem, aby se dva běhy nepřekrývaly:

cron 15 3 * * * flock -n /tmp/ai-panel.lock
/usr/bin/python3 /opt/ai-measure/scrape_panels.py

Zlepšete přesnost pomocí adaptérů pro povrchy

Záložní skript zaznamenává odkazy na úrovni stránky. To je užitečné pro objevování, ale může zahrnovat běžné odkazy z výsledků vyhledávání. Pro produkční použití:

  • Použijte licencované API panelu odpovědí, pokud je k dispozici.
  • Vytvořte samostatný adaptér selektoru pro každý povrch.
  • Uložte verzi selektoru.
  • Uložte nezpracované HTML a snímek obrazovky.
  • Označte každý záznam link_scope = "panel_selector" nebo link_scope = "page_fallback".

Vypočítávejte podíl citací pouze z dat panel_selector, když můžete spolehlivě identifikovat panel. Pozorování z fallbacku udržujte v samostatném průzkumném reportu.


8. Integrace Google Analytics a BigQuery

Google Analytics 4 může exportovat nezpracovaná data událostí do BigQuery. Denní exporty poskytují stabilní tabulky jako events_YYYYMMDD; streamovací exporty jsou užitečné pro monitorování téměř v reálném čase, ale mohou být neúplné, dokud nebude denní tabulka finalizována. Export Google Analytics do BigQuery (support.google.com)

Zjednodušený dotaz na relaci může vypadat takto:

sql WITH page_events AS ( SELECT event_date, user_pseudo_id,

(
  SELECT value.int_value
  FROM UNNEST(event_params)
  WHERE key = 'ga_session_id'
) AS session_id,

(
  SELECT value.string_value
  FROM UNNEST(event_params)
  WHERE key = 'page_location'
) AS page_location,

(
  SELECT value.string_value
  FROM UNNEST(event_params)
  WHERE key = 'session_engaged'
) AS session_engaged,

event_name

FROM PROJECT.analytics_PROPERTY.events_* WHERE _TABLE_SUFFIX BETWEEN '20260901' AND '20260921' ),

session_rollup AS ( SELECT event_date, user_pseudo_id, session_id,

MAX(
  CASE
    WHEN REGEXP_CONTAINS(
      LOWER(page_location),
      r'[?&]utm_medium=ai_answer'
    )
    THEN 1 ELSE 0
  END
) AS ai_session,

MAX(
  CASE
    WHEN session_engaged = '1'
    THEN 1 ELSE 0
  END
) AS engaged_session,

MAX(
  CASE
    WHEN event_name IN ('purchase', 'generate_lead', 'sign_up')
    THEN 1 ELSE 0
  END
) AS key_event

FROM page_events GROUP BY event_date, user_pseudo_id, session_id )

SELECT event_date, COUNTIF(ai_session = 1) AS ai_sessions, COUNTIF(ai_session = 1 AND engaged_session = 1) AS engaged_ai_sessions, COUNTIF(ai_session = 1 AND key_event = 1) AS ai_key_events, SAFE_DIVIDE( COUNTIF(ai_session = 1 AND engaged_session = 1), COUNTIF(ai_session = 1) ) AS post_citation_engagement_rate FROM session_rollup GROUP BY event_date ORDER BY event_date;

Pro pokročilou implementaci přidejte surface, topic a prompt_id do struktury kampaně nebo je připojte z tabulky kliknutí zkracovače odkazů.

Nevytvářejte analytickou dimenzi s vysokou kardinalitou pro každého jednotlivého uživatele nebo relaci. Google varuje, že nadměrné vlastní dimenze s vysokou kardinalitou mohou snížit kvalitu reportování. Vlastní dimenze a metriky Google Analytics (support.google.com)


9. Návrh dashboardu

Praktický dashboard by měl obsahovat pět zobrazení.

Souhrn pro management

Zobrazte:

  • Podíl citací dle tématu
  • Pokrytí citacemi dle tématu
  • Potvrzená míra doporučení asistentem
  • Rozšířená míra doporučení asistentem
  • Angažovanost po citaci
  • Míra úspěšnosti crawleru AI
  • Klíčové události nebo příjem připsané AI

Trend podílu citací

Spojnicový graf:

text Date × Topic × Surface

Zobrazte obojí:

  • výskyty citací cílové domény
  • všechny pozorované výskyty citací

Trychtýř návštěvnosti asistenta

text Pozorované citace ↓ Kliknutí na řízené odkazy ↓ Relace připsané AI ↓ Angažované relace AI ↓ Klíčové události

Každá fáze by měla zobrazovat svou spolehlivost atribuce.

Stav crawlerů

Tabulka dle bota:

text bot_name requests successful_requests error_rate unique_ip_count top_paths

Upozornění, když:

  • chybovost přesáhne 2 procenta
  • bot náhle zmizí
  • dříve přístupná sekce obsahu vrátí 403 nebo 429
  • požadavky crawleru se soustředí na nezamýšlenou soukromou cestu

Tabulka příležitostí pro obsah

Seřaďte stránky a témata podle:

text vysoký podíl citací + nízká míra doporučení vysoká zobrazení + nízké pokrytí citacemi vysoká angažovanost + nízká viditelnost citace vysoká chybovost crawleru + vysoká obchodní důležitost

Tyto kombinace obvykle vedou k lepším rozhodnutím než pouhé sledování objemu návštěvnosti.

Minimální Streamlit dashboard

Uložte jako dashboard.py.

python import glob import json from pathlib import Path

import pandas as pd import streamlit as st

st.set_page_config( page_title="AI Citation Impact", layout="wide", )

@st.cache_data(ttl=300) def load_panel_rows(): rows = []

for filename in glob.glob("data/panels/*.jsonl"):
    with open(filename, encoding="utf-8") as source:
        for line in source:
            record = json.loads(line)

            run_id = record.get("run_id")
            for link in record.get("links", []):
                hostname = link.get("href", "").split("/")[2]
                rows.append({
                    "run_id": run_id,
                    "observed_at": record.get("observed_at"),
                    "surface": record.get("surface"),
                    "topic": record.get("topic"),
                    "prompt_id": record.get("prompt_id"),
                    "panel_present": record.get("panel_present"),
                    "target_domain": record.get("target_domain"),
                    "cited_domain": hostname,
                    "target_cited": link.get("target_cited", False),
                    "position": link.get("position"),
                    "link_scope": record.get("link_scope"),
                })

return pd.DataFrame(rows)

panels = load_panel_rows()

st.title("AI Citation Impact Dashboard")

if panels.empty: st.warning("No panel observations found.") st.stop()

panels["date"] = pd.to_datetime( panels["observed_at"], errors="coerce" ).dt.date

citation_share = ( panels.groupby("topic") .apply( lambda group: group["target_cited"].sum() / max(len(group), 1) ) .reset_index(name="citation_share") )

coverage = ( panels.groupby(["topic", "run_id"], dropna=False) .agg( panel_present=("panel_present", "max"), target_cited=("target_cited", "max"), ) .reset_index() )

coverage = ( coverage[coverage["panel_present"] == True] .groupby("topic") .apply(lambda group: group["target_cited"].mean()) .reset_index(name="citation_coverage") )

col1, col2 = st.columns(2)

with col1: st.subheader("Citation Share by Topic") st.dataframe(citation_share, use_container_width=True) st.bar_chart( citation_share.set_index("topic")["citation_share"] )

with col2: st.subheader("Citation Coverage by Topic") st.dataframe(coverage, use_container_width=True) st.bar_chart( coverage.set_index("topic")["citation_coverage"] )

st.subheader("Citation Position") st.dataframe( panels.groupby(["topic", "surface"], dropna=False) .agg( citations=("target_cited", "sum"), median_position=("position", "median"), observations=("run_id", "nunique"), ) .reset_index(), use_container_width=True, )

st.subheader("Daily Observations") daily = ( panels.groupby(["date", "surface"], dropna=False) .agg( citations=("target_cited", "sum"), observed_links=("cited_domain", "count"), ) .reset_index() )

st.line_chart( daily.pivot( index="date", columns="surface", values="citations" ).fillna(0) )

Spusťte to s:

bash pip install streamlit pandas streamlit run dashboard.py

Pro produkci načtěte normalizované tabulky do DuckDB, BigQuery, Snowflake nebo PostgreSQL namísto přímého čtení surových souborů.


10. Kontroly kvality

Měřicí systém může produkovat přesně vypadající, ale zavádějící čísla. Přidejte tyto kontroly.

Oddělte boty od lidí

Nikdy nepočítejte OAI-SearchBot, GPTBot, ClaudeBot nebo PerplexityBot jako lidské relace. Jedná se o signály crawlerů nebo načítacích botů.

Zachovejte surové důkazy

Pro každé spuštění panelu zachovejte:

  • surové HTML
  • snímek obrazovky
  • text odpovědi
  • časové razítko
  • verze prohlížeče a sběrače
  • dotaz a lokalizace
  • hash odpovědi

To umožňuje vysvětlit, proč se metrika změnila.

Sledování nepřítomnosti panelu

Chybějící panel AI není totéž jako odpověď bez vaší citace.

Použijte:

text panel_not_present panel_present_no_target panel_present_target_cited blocked_or_challenged collection_error

Kontrola zkreslení výběru vzorku

Plánované scrapování je ovlivněno:

  • poloha
  • zařízení
  • jazyk
  • čas
  • personalizace
  • stav prohlížeče
  • formulace dotazu

Použijte pevnou základní sadu dotazů a rotující sadu průzkumných dotazů. Tyto dvě skupiny udržujte v reportech oddělené.

Vyhněte se falešné přesnosti

Reportujte intervaly spolehlivosti nebo minimální velikosti vzorku pro malé tematické skupiny. Téma se třemi pozorováními by nemělo být přímo srovnáváno s tématem obsahujícím 300 pozorování.

Srovnávejte srovnatelné

Při srovnávání povrchů udržujte konstantní:

  • dotaz
  • lokalizace
  • země
  • zařízení
  • hodina sběru
  • pravidla cílové domény
  • verze parseru citací

11. Doporučený plán implementace

Týden 1: Instrumentace

  • Konfigurujte strukturované serverové protokoly.
  • Vytvořte registr botů.
  • Vytvořte doménu pro zkracovač odkazů.
  • Přidejte pravidla pro pojmenování kampaní.
  • Definujte taxonomii témat.
  • Povolte export Google Analytics 4 do BigQuery.

Týden 2: Řízené odkazy

  • Publikujte pět až deset sledovaných souhrnů.
  • Vytvořte odkazy pro různá témata a verze obsahu.
  • Ověřte protokoly přesměrování oproti analytickým relacím.
  • Potvrďte, že do URL adres nebo analytických parametrů nevstupují žádné osobní údaje.

Týden 3: Pozorování panelů

  • Vytvořte manifest dotazů.
  • Začněte se dvěma povrchy a 20 až 50 stabilními dotazy.
  • Spouštějte jednou denně.
  • Ukládejte snímky obrazovky a HTML.
  • Měřte selhání sběru dat odděleně od nepřítomnosti citací.

Týden 4: Dashboard a správa

  • Vytvořte dashboardy pro témata a povrchy.
  • Přidejte upozornění na stav crawlerů.
  • Definujte pravidla spolehlivosti.
  • Porovnejte reporty z první strany s pozorováními panelů.
  • Dokumentujte zásady uchovávání, přístupu a mazání dat.

Závěr

Měření citací AI by mělo být postaveno jako vícesignálový atribuční systém, nikoli jako jediný report doporučení.

Nejspolehlivější rámec kombinuje:

  • Reporty viditelnosti z první strany od Googlu a Bingu
  • serverové protokoly pro aktivitu crawlerů a načítacích botů
  • krátké odkazy z první strany pro řízené měření kliknutí
  • plánovaná, vyhovující pozorování panelů odpovědí
  • analytická a konverzní data pro následnou hodnotu

Tři nejdůležitější metriky jsou:

  1. Podíl citací dle tématu — kolik pozorovaného zdrojového prostoru patří vaší doméně.
  2. Míra doporučení asistentem — kolik lidské návštěvnosti lze připsat povrchům AI.
  3. Angažovanost po citaci — zda návštěvníci doporučení AI provádějí smysluplné akce.

Systém se stává užitečným, když každé číslo zahrnuje svůj povrch, časové období, jmenovatel, atribuční metodu a úroveň spolehlivosti. Tato disciplína zabraňuje záměně zásahů crawlerů za uživatele, seškrábaných odpovědí za zobrazení a citací za obchodní výsledky.

Související články

Líbí se vám tento obsah?

Přihlaste se k odběru našeho newsletteru pro nejnovější poznatky z obsahového marketingu a průvodce růstem.

Tento článek slouží pouze pro informační účely. Obsah a strategie se mohou lišit v závislosti na vašich konkrétních potřebách.
Měření dopadu citací AI: Analýza, protokoly a scrapování panelů | AutoPod