Реализация Log File Analysis для анализа поведения поисковых роботов
Лог-файлы веб-сервера — единственный источник данных о том, что поисковые роботы реально делают на сайте. Google Search Console показывает обобщённую картину с задержкой. Логи показывают точно: какие URL обходит Googlebot, как часто, сколько времени тратит, какие URL игнорирует, где получает ошибки 404/500, есть ли аномалии в поведении бота.
Для SEO log file analysis полезен при решении нескольких задач: диагностика crawl budget, поиск URL, которые бот обходит, но поисковик не индексирует, выявление медленно отвечающих страниц, обнаружение нежелательных ботов.
Структура записей в access.log
# Nginx access.log (combined формат)
66.249.64.13 - - [15/Nov/2024:14:23:01 +0300] "GET /products/laptop-apple/ HTTP/1.1" 200 45231 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
# Поля: IP, ident, auth, time, method+URL+protocol, status, bytes, referer, user-agent
Для детального анализа нужно добавить response time в лог. В nginx:
# nginx.conf
log_format detailed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/access.log detailed;
$request_time — полное время обработки запроса в секундах. $upstream_response_time — время ответа бэкенда.
Идентификация поисковых роботов
Основные User-Agent поисковиков:
CRAWLER_PATTERNS = {
'Googlebot': r'Googlebot(?:/\d+\.\d+)?',
'Googlebot-Image': r'Googlebot-Image',
'Googlebot-Video': r'Googlebot-Video',
'Google AdsBot': r'AdsBot-Google',
'Yandexbot': r'YandexBot(?:/\d+\.\d+)?',
'YandexImages': r'YandexImages',
'Bingbot': r'bingbot(?:/\d+\.\d+)?',
'Baiduspider': r'Baiduspider',
'DuckDuckBot': r'DuckDuckBot',
}
Важно: проверять подлинность Googlebot только через обратный DNS (PTR-запись):
import socket
import re
def verify_googlebot(ip: str) -> bool:
"""Проверка подлинности Googlebot через PTR запись"""
try:
# Обратный DNS lookup
hostname = socket.gethostbyaddr(ip)[0]
if not re.search(r'\.googlebot\.com$|\.google\.com$', hostname):
return False
# Прямой DNS lookup для подтверждения
resolved_ip = socket.gethostbyname(hostname)
return resolved_ip == ip
except socket.herror:
return False
Парсинг логов: базовый скрипт
import re
import gzip
from pathlib import Path
from datetime import datetime
from collections import defaultdict, Counter
from dataclasses import dataclass, field
from typing import Iterator
LOG_PATTERN = re.compile(
r'(?P<ip>[\d.]+) .+ \[(?P<time>[^\]]+)\] '
r'"(?P<method>\w+) (?P<url>[^\s]+) HTTP/[\d.]+" '
r'(?P<status>\d+) (?P<bytes>\d+) '
r'"[^"]*" "(?P<ua>[^"]*)"'
r'(?:\s+(?P<request_time>[\d.]+))?'
)
@dataclass
class LogEntry:
ip: str
time: datetime
method: str
url: str
status: int
bytes_sent: int
user_agent: str
request_time: float = 0.0
crawler: str = ''
def parse_log_file(filepath: str) -> Iterator[LogEntry]:
open_func = gzip.open if filepath.endswith('.gz') else open
with open_func(filepath, 'rt', encoding='utf-8', errors='replace') as f:
for line in f:
m = LOG_PATTERN.match(line)
if not m:
continue
try:
entry = LogEntry(
ip=m.group('ip'),
time=datetime.strptime(m.group('time'), '%d/%b/%Y:%H:%M:%S %z'),
method=m.group('method'),
url=m.group('url'),
status=int(m.group('status')),
bytes_sent=int(m.group('bytes')),
user_agent=m.group('ua'),
request_time=float(m.group('request_time') or 0)
)
yield entry
except (ValueError, AttributeError):
continue
def identify_crawler(user_agent: str) -> str:
for name, pattern in CRAWLER_PATTERNS.items():
if re.search(pattern, user_agent, re.I):
return name
return ''
def analyze_crawler_behavior(log_files: list[str]) -> dict:
crawler_stats = defaultdict(lambda: {
'total_requests': 0,
'urls': Counter(),
'status_codes': Counter(),
'slow_urls': [], # response_time > 2s
'errors': [],
'hourly_distribution': Counter()
})
for log_file in log_files:
for entry in parse_log_file(log_file):
crawler = identify_crawler(entry.user_agent)
if not crawler:
continue
entry.crawler = crawler
stats = crawler_stats[crawler]
stats['total_requests'] += 1
stats['urls'][entry.url] += 1
stats['status_codes'][entry.status] += 1
stats['hourly_distribution'][entry.time.hour] += 1
if entry.request_time > 2.0:
stats['slow_urls'].append({
'url': entry.url,
'time': entry.request_time,
'timestamp': entry.time.isoformat()
})
if entry.status >= 400:
stats['errors'].append({
'url': entry.url,
'status': entry.status,
'timestamp': entry.time.isoformat()
})
return dict(crawler_stats)
Ключевые метрики для анализа
Crawl rate (запросы в день):
def crawl_rate_by_day(entries: list[LogEntry]) -> dict:
daily = Counter()
for e in entries:
if e.crawler == 'Googlebot':
daily[e.time.date()] += 1
return dict(sorted(daily.items()))
Норма для среднего сайта: 100–5000 запросов Googlebot в сутки. Резкий спад — признак проблемы (блокировка в robots.txt, ошибки сервера, снижение приоритета).
Наиболее/наименее краулируемые разделы:
def crawl_distribution_by_section(urls: Counter) -> dict:
sections = defaultdict(int)
for url, count in urls.items():
# Первый сегмент пути
parts = url.split('/')
section = f'/{parts[1]}/' if len(parts) > 1 else '/'
sections[section] += count
return dict(sorted(sections.items(), key=lambda x: x[1], reverse=True))
URL, которые бот обходит, но они отдают ошибки:
error_urls = [
e for e in errors
if e['status'] in (404, 410, 500, 503)
]
# Эти URL нужно либо восстановить, либо добавить 301 редирект
Визуализация через ClickHouse + Grafana
Для непрерывного мониторинга логи лучше стримить в ClickHouse:
-- Таблица в ClickHouse
CREATE TABLE crawler_logs (
timestamp DateTime,
ip IPv4,
method LowCardinality(String),
url String,
status UInt16,
bytes UInt32,
user_agent String,
request_ms Float32,
crawler LowCardinality(String)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (crawler, timestamp)
TTL timestamp + INTERVAL 6 MONTH;
-- Запрос: топ URL, которые Googlebot посещает но не индексирует (200 OK, нет в GSC)
SELECT url, count() as visits
FROM crawler_logs
WHERE crawler = 'Googlebot'
AND status = 200
AND timestamp >= now() - INTERVAL 30 DAY
GROUP BY url
ORDER BY visits DESC
LIMIT 50;
Filebeat → Logstash/Vector → ClickHouse — стандартный pipeline для production.
Поиск паразитных ботов
Не все боты полезны. Поиск по user_agent:
def find_suspicious_crawlers(log_files: list[str]) -> list:
suspicious = []
known_good = set(CRAWLER_PATTERNS.keys()) | {'curl', 'wget', 'python-requests'}
ua_counter = Counter()
for log_file in log_files:
for entry in parse_log_file(log_file):
if not identify_crawler(entry.user_agent):
ua_counter[entry.user_agent] += 1
# UA с большим количеством запросов и неизвестным происхождением
for ua, count in ua_counter.most_common(50):
if count > 1000: # порог
suspicious.append({'user_agent': ua, 'requests': count})
return suspicious
Обнаруженных агрессивных ботов блокируем в nginx:
# nginx.conf
map $http_user_agent $bad_bot {
default 0;
~*SemrushBot 0; # Разрешённый SEO бот
~*AhrefsBot 0; # Разрешённый
~*MJ12bot 1; # Заблокировать
~*DotBot 1;
}
server {
if ($bad_bot) {
return 403;
}
}
Сроки
Разовый анализ логов за 1 месяц (до 5 GB) с отчётом — 2–3 рабочих дня. Настройка автоматизированного pipeline (парсинг → ClickHouse → Grafana дашборд) с алертами — 4–7 дней.







