◆IKORG
English

← Практика · 8 сентября 2026

Как подключить нейросеть к Яндекс.Вебмастеру и не заходить в интерфейс

Получаем токен, ходим в API Вебмастера из терминала, вешаем на крон переобход и наблюдение, а разбор жалоб отдаём Claude Code: спрашиваете словами «что там с сайтом?» и получаете диагноз с исправлением.

10 минут чтенияинструменты

У меня двадцать два сайта в Яндекс.Вебмастере. Заходить в интерфейс каждого раз в неделю — это два часа кликов, и я их не делаю. А значит, и не вижу вовремя ни выпавших из поиска страниц, ни жалоб диагностики: узнаю о них, когда уже надо чинить.

Вебмастер при этом отдаёт всё то же самое через API за секунду. Если рядом сидит нейросеть, которая умеет вызывать curl, интерфейс становится не нужен вовсе: «что там с eden96?» — и через минуту в терминале диагноз, а не отчёт.

Ниже ровно то, что стоит у меня и работает каждое утро.

Что получится в итоге

# я пишу в Claude Code:
# «Вебмастер жалуется на eden96, разберись»
#
# сессия сама:
#   1. снимает summary и diagnostics по API
#   2. сверяет страницы в поиске с картой сайта
#   3. курлит подозрительные адреса и находит петли редиректов
#   4. чинит .htaccess в репозитории, пересобирает, выкладывает
#   5. отправляет исправленные адреса на переобход

Плюс два скрипта на кроне: один тратит суточную квоту переобхода, другой раз в день записывает показатели и кричит, если что-то просело.

Шаг 1. Токен

API Вебмастера ходит по OAuth-токену. Токен получают одной ссылкой:

https://oauth.yandex.ru/authorize?response_type=token&client_id=ba16a05cf7134a29aadd139e8c12c891

Это идентификатор стандартного приложения «Яндекс.Вебмастер API» — у него нет секрета, свой регистрировать не нужно. Открываете ссылку, входите в аккаунт, которому принадлежат сайты, и Яндекс показывает токен прямо на странице. Копируете в файл:

umask 077
echo 'ВСТАВИТЬ_ТОКЕН' > ~/.yandex_webmaster_token

Три вещи, которые стоит знать заранее.

Токен привязан к аккаунту, а не к сайту. Один файл открывает все подтверждённые хосты сразу: у меня — двадцать два, любому проекту и любой сессии.

Живёт шесть месяцев. Выдан пятого сентября — протухнет в начале марта. Признак — на любой запрос приходит HTTP 401. Совсем чужой или битый токен, кстати, отдаёт 403, так что любой отказ с кодом 4xx на вчера работавший запрос — это про токен, а не про сайт. Продлить токен может только человек: та же ссылка, тот же вход. Нейросеть тут бессильна, и это правильно.

Значение токена в терминал не печатать. Все примеры ниже читают его из файла. Если сессия нейросети всё же выведет его в лог — заводите новый.

Шаг 2. Свой user_id и host_id сайта

Все адреса API начинаются с числового идентификатора пользователя:

T=$(cat ~/.yandex_webmaster_token)
curl -s -H "Authorization: OAuth $T" https://api.webmaster.yandex.net/v4/user/
# {"user_id":79452740}

Дальше — список хостов. Здесь появляются первые грабли:

U=79452740
curl -s -H "Authorization: OAuth $T" "https://api.webmaster.yandex.net/v4/user/$U/hosts/"
{"host_id": "https:eden96.ru:443", "verified": true, "ascii_host_url": "https://eden96.ru/"}
{"host_id": "https:xn--80aahsbi1ccix.xn--p1ai:443", "unicode_host_url": "https://играйсразу.рф/"}

host_id — это не URL, а строка вида схема:домен:порт. Она подставляется в адрес как есть, без urlencode. Если закодировать двоеточия, получите 404 на все запросы и полчаса потерянного времени. Кириллический домен уже в punycode — так и передаём.

Заводим переменную, все команды ниже от неё:

H="https:eden96.ru:443"
B="https://api.webmaster.yandex.net/v4/user/$U/hosts/$H"

Шаг 3. Что читать

Четыре запроса закрывают девяносто процентов вопросов.

Сводка — сколько в поиске, сколько исключено, индекс качества:

curl -s -H "Authorization: OAuth $T" "$B/summary/"
# {"sqi":30,"excluded_pages_count":1,"searchable_pages_count":96,"site_problems":{"POSSIBLE_PROBLEM":1}}

Диагностика — те самые «жалобы» из интерфейса:

curl -s -H "Authorization: OAuth $T" "$B/diagnostics/" | python3 -m json.tool

Тут важно, как это устроено. problems приходит не списком, а словарём, где ключ — код проблемы, и в нём перечислены все известные Вебмастеру проблемы, включая те, которых у сайта никогда не было:

"DOCUMENTS_MISSING_DESCRIPTION": {
    "severity": "POSSIBLE_PROBLEM",
    "state": "PRESENT",
    "last_state_update": "2026-08-23T10:24:03.804+03:00"
},
"SLOW_AVG_RESPONSE_TIME": {
    "severity": "CRITICAL",
    "state": "ABSENT",
    "last_state_update": null
}

Живая проблема — только state: PRESENT. ABSENT — снятая или никогда не возникавшая. И обязательно смотрите last_state_update: Вебмастер обновляет диагностику не каждый день, и отчёт часто описывает состояние, которого уже нет. Если дата последнего обновления раньше даты вашей выкладки — проблема, скорее всего, уже вылечена, просто робот не перепроверил.

Страницы в поиске — с заголовком и датой последнего обхода:

curl -s -H "Authorization: OAuth $T" "$B/search-urls/in-search/samples/?limit=100"

События — что появилось и что выпало, и главное — почему:

curl -s -H "Authorization: OAuth $T" "$B/search-urls/events/samples/?limit=100"
{"url": "https://eden96.ru/category/esteticheska-stomatologiya/restavraciya-zubov/",
 "event": "REMOVED_FROM_SEARCH",
 "excluded_url_status": "REDIRECT_NOTSEARCHABLE",
 "target_url": "http://eden96.ru/"}

Вот эта запись — готовый диагноз. Страница старого движка выпала, потому что редиректит, а редиректит она на http://, хотя сайт давно на https://. Про это ниже.

Шаг 4. Сверка с картой сайта

Самый быстрый способ найти причину жалобы — не читать отчёты, а вычесть одно множество из другого. Берём адреса из поиска и адреса из sitemap.xml:

curl -s -H "Authorization: OAuth $T" "$B/search-urls/in-search/samples/?limit=500" \
  | python3 -c "import json,sys; print('\n'.join(s['url'] for s in json.load(sys.stdin)['samples']))" \
  | sort > /tmp/in-search.txt

grep -o '<loc>[^<]*' dist/sitemap.xml | sed 's/<loc>//' | sort > /tmp/sitemap.txt

comm -23 /tmp/in-search.txt /tmp/sitemap.txt   # в поиске, но не в карте: мусор старого движка
comm -13 /tmp/in-search.txt /tmp/sitemap.txt   # в карте, но не в поиске: робот их не видит

На eden96 первая разница показала хвост от WordPress: /category/..., /tag/..., /page/2/. Сайт два раза сменил движок, а поисковик всё это помнил и держал в индексе, потому что адреса не отдавали ничего внятного.

Шаг 5. Проверять адреса курлом, а не глазами

Здесь вторые грабли, и они дорогие. У сайтов на Beget первый запрос без cookie получает не страницу, а JavaScript-заглушку на 274 байта с кодом 200. Браузер её проходит незаметно, а curl — нет:

curl -s -o /dev/null -w '%{http_code} %{size_download}\n' https://eden96.ru/
# 200 274        ← это не сайт, это заглушка
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' -b "beget=begetok" https://eden96.ru/
# 200 48013      ← а вот это сайт

Без cookie любой диагноз будет ложным: все страницы «отдают 200», редиректов «нет», а Вебмастер при этом жалуется. Поэтому у меня правило, записанное в инструкцию для нейросети: курл к сайту на Beget всегда с -b "beget=begetok".

Вторая проверка — петли и лишние прыжки:

curl -s -o /dev/null -L -b "beget=begetok" \
  -w '%{num_redirects} %{url_effective}\n' https://eden96.ru/category/novosti/

Если прыжков два, а не один, — смотрите на .htaccess. За прокси Beget Apache видит запрос как http, и относительная цель редиректа достраивается до http://, после чего срабатывает правило http→https, и получается лишний хоп. Лечится двумя вещами: цели редиректов пишутся абсолютными, с https://, а правило http→https ставится в файле последним.

Шаг 6. Чинить в репозитории и отдавать 410

У сайтов на Astro .htaccess лежит в public/ и уезжает на прод сборкой. Править его на проде по ssh — потерять правку при следующем деплое. Всё, что ниже, — в репозитории.

С мусорными адресами старого движка соблазн один: перекинуть всё на главную через 301. Не надо. Поисковик считает 301 на нерелевантную главную мягкой 404 и держит адрес в индексе месяцами, а страницы-переезды, наоборот, продолжают висеть как «редирект». Правильно так: у чего есть новый адрес — 301 ровно на него; чего больше нет — 410 Gone:

# --- Страницы, удалённые вместе с WordPress: 410 Gone ---
# Яндекс и Google выкидывают 410 из индекса быстрее, чем 301 на главную
RewriteRule ^author(/|$)      - [G,L]
RewriteRule ^bez-rubriki(/|$) - [G,L]

# --- Переезды: только абсолютные https-цели ---
RewriteRule ^novosti/?$ https://eden96.ru/o-klinike/novosti/ [R=301,L]
RewriteRule ^protezirovanie/?$ https://eden96.ru/protezirovanie-zubov/ [R=301,L]

# --- Рубрики WordPress: /category/<раздел>/ -> одноимённый раздел, если он есть ---
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^category/([^/]+)/?$ https://eden96.ru/$1/ [R=301,L]

# --- http -> https: строго последним ---

После выкладки — обход всех адресов из карты сайта на 200 и проверка, что нет петель. Только после этого «готово».

Шаг 7. Переобход и его квота

Исправить мало — надо, чтобы робот пришёл. Иначе диагностика будет висеть до следующего планового обхода, а это недели. Заявка на переобход:

curl -s -X POST -H "Authorization: OAuth $T" -H "Content-Type: application/json" \
     -d '{"url":"https://eden96.ru/o-klinike/novosti/"}' "$B/recrawl/queue/"

Квота на заявки:

curl -s -H "Authorization: OAuth $T" "$B/recrawl/quota/"
# {"daily_quota":560,"quota_remainder":560}

Две особенности. Квота своя у каждого сайта: у eden96 — 560 адресов в сутки, у играйсразу.рф — 150, у соседнего проекта — 850; Яндекс считает её сам, повлиять нельзя. И она не копится: непотраченное за сутки сгорает. Из второго прямо следует крон.

Ежедневный переобход по квоте

У играйсразу.рф три с половиной тысячи страниц и 150 заявок в день. Скрипт recrawl_daily.py каждое утро спрашивает остаток квоты и отправляет ровно столько адресов, сколько можно, двигаясь по циклической очереди:

TOKEN = open(os.path.expanduser('~/.yandex_webmaster_token')).read().strip()
BASE = f'https://api.webmaster.yandex.net/v4/user/{USER}/hosts/{HOST_ID}'

def api(method, path, data=None):
    req = urllib.request.Request(BASE + path, data=data, method=method,
        headers={'Authorization': f'OAuth {TOKEN}', 'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.loads(r.read().decode('utf-8') or '{}')

q = api('GET', '/recrawl/quota/')
left = q.get('quota_remainder', 0)
if left <= 0:
    print('Квота на сегодня исчерпана.'); return

queue = build_queue()            # листинги вперёд, потом 3000 самых читаемых страниц
st = load_state()                # {'pos': 450, 'circles': 0} в маленьком json
pos = st['pos'] % len(queue)
batch = [queue[(pos + i) % len(queue)] for i in range(left)]

ok = 0
for u in batch:
    try:
        api('POST', '/recrawl/queue/', json.dumps({'url': u}).encode('utf-8'))
        ok += 1
    except Exception:
        break                    # квота кончилась или отказ — дальше долбить бессмысленно
st['pos'] = (pos + ok) % len(queue)
save_state(st)

Очередь строится из исходников: сначала листинги, через которые робот находит остальное, потом страницы по убыванию просмотров. Позиция хранится в файле, и когда список кончается, всё начинается заново — повторная заявка не спам, это штатный механизм, а страницы к тому времени успевают измениться. Полный круг по голове очереди занимает двадцать дней.

В кроне одна строка:

40 6 * * * cd ~/accords && timeout 900 python3 tools/recrawl_daily.py >> tools/_recrawl/cron.log 2>&1

Лог за сегодня:

2026-09-07  квота: 150 в сутки, доступно 150
Очередь: 3005 адресов, позиция 300. Отправляю 150.
Отправлено на переобход: 150. Следующая позиция: 450.

Наблюдение за показателями

Второй скрипт, webmaster_watch.py, как раз и закрывает эту дыру: смотреть самому раз в неделю я не буду, а он смотрит каждый день. Раз в сутки он снимает сводку, пишет строку в историю и сравнивает с прошлой неделей:

s = api('/summary/')
searchable, excluded, sqi = s['searchable_pages_count'], s['excluded_pages_count'], s['sqi']
alerts = []

# 1) живые проблемы диагностики — только PRESENT
for name, p in api('/diagnostics/').get('problems', {}).items():
    if p.get('state') == 'PRESENT':
        mark = '🔴' if p.get('severity') in ('FATAL', 'CRITICAL') else '🟡'
        alerts.append(f'{mark} диагностика: {name}, с {(p.get("last_state_update") or "?")[:10]}')

# 2) обвал страниц в поиске относительно среднего за неделю
prev = [r[1] for r in history()[-7:]]
if prev and searchable < sum(prev) / len(prev) * 0.7:
    alerts.append(f'🔴 в поиске {searchable} — падение со среднего {sum(prev)/len(prev):.0f}')

# 3) ошибки в картах сайта
for sm in api('/sitemaps/').get('sitemaps', []):
    if sm.get('errors_count'):
        alerts.append(f'🔴 sitemap: {sm["errors_count"]} ошибок')

# 4) квота не тратится — тихая потеря
q = api('/recrawl/quota/')
if q.get('quota_remainder') == q.get('daily_quota'):
    alerts.append(f'🟡 квота переобхода не тратится: {q["daily_quota"]} в сутки')

Итог — два файла. history.tsv: по строке в сутки, дата, в поиске, исключено, индекс качества. alerts.log: только отклонения. Пустой файл отклонений означает «всё в порядке», и читать его достаточно раз в неделю — или попросить нейросеть заглянуть.

2026-09-05  501 27  10
2026-09-06  501 27  10
2026-09-07  688 21  10

Порог падения — тридцать процентов, не меньше: молодой сайт Яндекс регулярно переоценивает, и на десять–пятнадцать процентов показатель гуляет сам по себе. Кричать на каждое колебание — значит через неделю перестать читать лог.

Чего в API нет

Честно, чтобы вы не искали.

Удаления страниц. Раздел «Удалить URL» в интерфейсе есть, в API v4 — нет. Если нужно срочно выкинуть адрес из поиска, а не ждать, пока робот увидит 410, — только руками.

Регионов. Привязка сайта к региону — тоже только в интерфейсе.

Полного списка страниц. Методы samples отдают выборку, максимум по несколько сотен за запрос, а не все страницы. Для сверки с картой сайта на сотне страниц этого хватает, на десяти тысячах — уже сравниваете выборку.

Остальное — сводка, диагностика, события, карты сайта, переобход, история индексирования — есть, и работает без сюрпризов.

Что здесь делает нейросеть

Все команды выше человек может выполнить сам. Разница в том, что делать это раз в неделю по двадцати двум сайтам он не будет. Я не буду точно.

Поэтому вся эта инструкция в сжатом виде лежит у меня в ~/.claude/CLAUDE.md — файле, который Claude Code читает в начале каждой сессии. Там путь к файлу с токеном, user_id, шаблоны запросов, правило про cookie Beget, правило про 410 и алгоритм разбора жалобы из восьми пунктов. После этого диалог выглядит так:

— Что там с eden96?

— В поиске 96 страниц, исключена 1, живая проблема одна: DOCUMENTS_MISSING_DESCRIPTION, отмечена 23 августа. Сверил с картой сайта: все 88 адресов из карты в поиске есть. Восемь адресов в поиске лишние — хвост от WordPress, все отдают 410 после правок 5 сентября, робот их ещё не перепроверил. Отклонений нет, делать ничего не нужно.

Минута вместо получаса. А при реальной жалобе сессия проходит весь алгоритм сама: снимает диагностику, сверяет с картой сайта, курлит подозрительные адреса с правильной cookie, находит петлю в редиректах, правит .htaccess в репозитории, пересобирает, выкладывает, проверяет все адреса на 200 и отправляет исправленное на переобход. На eden96 это заняло один вечер вместе с выкладкой; руками я бы потратил его только на чтение отчётов.

Где помощник экономит часы: обход всех сайтов сразу, сверка множеств, чтение словаря проблем с датами, проверка сотни адресов курлом, написание правил редиректов по списку мусорных адресов, отправка на переобход пачкой.

Где решает человек, и это в инструкции сказано прямо:

И одно правило, которое я вывел из первых попыток: нейросети нужно давать не доступ, а алгоритм. Токен без инструкции про cookie Beget привёл к уверенному «все страницы отдают 200, проблем нет» — при живой жалобе Вебмастера. Восемь пунктов в файле инструкций стоят дороже любого доступа.

Грабли, кратко