Skip to main content

JSON SEO Python SDK

Официальный Python-клиент JSON SEO API: выдача Яндекса, Google и Bing, картинки и видео, поисковые подсказки, Яндекс Вордстат, прогноз показов Директа и геолокация по IP.

  • Работает на Python 3.8 и выше.
  • Без зависимостей: только стандартная библиотека.
  • Типы в комплекте (py.typed), редактор подсказывает поля ответов.
  • Двадцать один метод сервиса.
  • Три попытки на запрос по умолчанию: если сервис затупил, SDK сходит ещё раз сам.

Установка

pip install jsonseo

Ключ берётся в личном кабинете.

Быстрый старт

from jsonseo import Client

client = Client("ВАШ_КЛЮЧ")

serp = client.yandex("купить ноутбук", region=213)

for position, result in enumerate(serp["results"], start=1):
    print(position, result["domain"], "—", result["title"])

Первый аргумент — основной параметр метода, остальное передаётся по имени:

client.yandex("купить ноутбук", region=213, pages=2)
client.geoip("77.88.55.242")
client.wordstat_frequency("ремонт айфона", kind="exact")

Примеры запросов

Позиции сайта в Яндексе

break_domain останавливает сбор на нужном домене — платить за страницы ниже найденной позиции незачем.

serp = client.yandex(
    "ремонт айфона",
    region=213,            # Москва
    pages=10,              # до 100 позиций
    break_domain="example.com",
)

for position, result in enumerate(serp["results"], start=1):
    if result["domain"].endswith("example.com"):
        print("Позиция:", position)
        break

print("Собрано страниц:", serp["pages"])
print("Нашлось всего:", serp["found_human"])

В ответе:

{
    "pages": 3,
    "exhausted": False,
    "breakDomainHit": True,          # остановились на нужном домене
    "query": "ремонт айфона",
    "rawQuery": "ремонт айфона",
    "found": 28000000,
    "found_human": "нашлось 28 млн результатов",
    "lr": 213,
    "url": "https://yandex.ru/search/?text=...",
    "results": [
        {
            "url": "https://example.com/remont-iphone/",
            "domain": "example.com",
            "title": "Ремонт айфонов в Москве",
            "passage": "Починим за 30 минут...",
            "breadcrumbs": "example.com › услуги",
        },
    ],
}

Выдача Google по нужному городу

Регион задаётся числовым ID из справочника — сервис сам соберёт uule и подставит gl.

regions = client.google_regions("Казань")

serp = client.google(
    "заказать пиццу",
    region=regions["regions"][0]["id"],
    hl="ru",
    device="desktop",
    pages=2,
)

Если Google схлопнул часть результатов как «очень похожие», причина придёт в filter_description, а вернуть их можно параметром filter:

serp = client.google("заказать пиццу", filter=0)

Выдача Bing

serp = client.bing("buy a laptop", mkt="en-US", pages=2)

print(serp["mkt"], serp["lang"])  # фактический рынок и язык

Реклама на странице выдачи

Приходит отдельным списком, органика не меняется. Стоит +0.01 ₽ за страницу, на которой реклама нашлась.

serp = client.yandex("пластиковые окна", region=213, ads=True)

for ad in serp.get("ads", []):
    print(ad["block"], "#{}".format(ad["position"]), "—", ad["domain"])
    print("  ", ad.get("title"))

block — где стоял блок: top до органики, bottom после неё, inline между результатами. Пустой список ads значит «рекламу просили, но её не было», а отсутствие ключа — «не просили».

Ответ нейросети над выдачей

serp = client.yandex("чем отличается osb от фанеры", ai=True)

answer = serp.get("aiAnswer")

if answer:
    print(answer["markdown"])

    for source in answer.get("sources", []):
        print("[{}]".format(source["id"]), source["domain"])

Стоит +0.01 ₽ и только когда ответ есть: если поисковик его не показал, запрос обойдётся в обычную цену. Доступен только с первой страницы.

Картинки

images = client.yandex_images(
    "скандинавский интерьер",
    orientation="horizontal",
    size="large",
    format="jpg",
    pages=2,
)

for image in images["results"]:
    print("{}×{}".format(image.get("width"), image.get("height")), image["url"])
    print("   источник:", image["sourceUrl"])

Те же параметры работают у google_images() и bing_images() — SDK переводит общий фильтр в родной параметр движка. Если у поисковика такого значения нет, придёт ValidationError с указанием, чем заменить.

Видео

videos = client.google_video("как заменить ремень грм", duration="long", hl="ru")

for video in videos["results"]:
    print(video["title"], "—", video.get("durationText"))
    print("  ", video["url"], "({})".format(video.get("provider")))

Поле duration приходит в секундах, но не всегда: у прямых эфиров вместо длины стоит LIVE. Отбор вида duration < 600 молча выбросит такие ролики — ориентируйтесь на durationText, он на месте всегда.

Поисковые подсказки

suggest = client.yandex_suggest("купить кв", region=213)

print(suggest["results"])
# ['купить квартиру в москве', 'купить квартиру в новостройке', ...]

Есть у всех трёх поисковиков: yandex_suggest(), google_suggest(), bing_suggest().

Справочник регионов

regions = client.yandex_regions("Казань")

for region in regions["regions"]:
    print(region["id"], "—", region["name"], "({})".format(region["subname"]))
# 43 — Казань (Республика Татарстан)

Бесплатно, но ключ нужен: по нему считается лимит запросов в минуту. У google_regions() в ответе дополнительно приходит готовая строка uule.

Вордстат: частота запроса

frequency = client.wordstat_frequency(
    "ремонт айфона",
    kind="exact",      # точная частотность: "!ремонт !айфона"
    region=213,
)

print(frequency["results"]["totalValue"])  # 27356

Вид частотности задаётся параметром kind, кавычки и операторы расставит сервис — фразу передавайте как есть:

kind Что считает
base Базовая: фраза как есть
phrase Фразовая: "фраза"
exact Точная: "!слово !слово" — для прогноза трафика берут её
superexact Сверхточная: "[!слово !слово]"

Вордстат: расширение семантики

wordstat = client.wordstat("ремонт айфона", region=[213, 2])

for phrase in wordstat["results"]["popular"]:
    print(phrase["value"], phrase["text"])

for phrase in wordstat["results"]["associations"]:
    print(phrase["value"], phrase["text"])

popular — что ищут вместе с фразой, associations — соседняя семантика.

Вордстат: сезонность

graph = client.wordstat_graph("купить ёлку", graph_type="month")

for point in graph["results"]["graph"]:
    print(point["text"], point["absolute"])
# июнь 2026 9042
# июль 2026 11780

month и week отдают историю с 2018 года, day — последние 60 дней.

Вордстат: география спроса

geo = client.wordstat_map("купить ноутбук", map_type="regions")

for row in geo["results"]["rows"]:
    print(row["text"], row["absolute"], "индекс", row["popularity"])

popularity — affinity-индекс: 100 означает средний по стране интерес, выше — повышенный. В каждой строке приходит region_id, его можно сразу подставить в region других методов.

Прогноз показов Яндекс Директа

Рекламный кабинет не нужен. Список фраз передаётся списком — SDK склеит его сам.

forecast = client.direct(
    ["ремонт айфона", "замена экрана iphone", '"ремонт айфона"'],
    region=213,
    period="month",
)

for row in forecast["results"]:
    print("{}: {} показов".format(row["phrase"], row["shows"]))

    for place, bid in row["positions"].items():
        print("   {}: ставка {} ₽, бюджет {} ₽, кликов {}".format(
            place, bid["bid"], bid["budget"], bid["clicks"]
        ))

Вид частотности задаётся операторами прямо во фразе: ремонт айфона — базовая, "ремонт айфона" — фразовая, "!ремонт !айфона" — точная.

Стоимость — 0.01 ₽ за пачку до 4000 символов, это около 150 обычных фраз. За один запрос принимается до 1000 фраз, на аккаунт — не больше 100 запросов в час.

Геолокация по IP

location = client.geoip("77.88.55.242")

print(location["country"]["name"], location["region"]["name"])
print(location["latitude"], location["longitude"])

ID региона тот же, что у Яндекса, — его можно сразу подставить в region методов выдачи и Вордстата:

serp = client.yandex("доставка пиццы", region=location["region"]["id"])

Баланс

balance = client.balance()

print(balance["balance"], balance["currency"])  # 123.45 RUB

Справочник методов

Метод Путь API Что делает
yandex() /yandex Органическая выдача Яндекса
yandex_suggest() /yandex/suggest Поисковые подсказки
yandex_regions() /yandex/regions Справочник регионов, бесплатно
yandex_images() /yandex/images Поиск по картинкам
yandex_video() /yandex/video Поиск по видео
google() /google Органическая выдача Google
google_suggest() /google/suggest Подсказки
google_regions() /google/regions Регионы и готовый uule, бесплатно
google_images() /google/images Поиск по картинкам
google_video() /google/video Поиск по видео
bing() /bing Органическая выдача Bing
bing_suggest() /bing/suggest Подсказки
bing_images() /bing/images Поиск по картинкам
bing_video() /bing/video Поиск по видео
wordstat() /wordstat Популярные и похожие запросы
wordstat_frequency() /wordstat/frequency Частота запроса одним числом
wordstat_graph() /wordstat/graph Динамика по месяцам, неделям, дням
wordstat_map() /wordstat/map География показов
direct() /direct Прогноз показов Яндекс Директа
geoip() /geoip Геолокация по IPv4, бесплатно
balance() /balance Остаток на счёте, бесплатно

Полный список параметров каждого метода — в документации.

Появился метод, которого ещё нет в SDK? Его можно вызвать напрямую:

client.call("новый/метод", {"параметр": "значение"})      # разберёт JSON
client.call_raw("новый/метод", {"параметр": "значение"})  # вернёт тело как есть

Как SDK помогает с параметрами

Списки передаются списками. Фразы для Директа склеиваются переводом строки, остальные списки — запятой:

client.direct(["ремонт айфона", "ремонт телефона", "замена экрана"])
client.wordstat("ремонт", region=[213, 2], device=["desktop", "phone"])

Флаги принимаются флагами. True и False уезжают как 1 и 0:

client.yandex("купить ноутбук", ai=True, ads=True)

None и пустой список не отправляются. Необязательный параметр, который вы ещё не посчитали, можно не вычищать из вызова руками.

Родные параметры поисковиков проходят насквозь. Вертикали принимают не только общие фильтры, но и tbs у Google, isize у Яндекса, qft у Bing.

Ошибки

Всё, что бросает SDK, наследуется от JsonSeoError.

Ошибка Статус Когда
ValidationError 422 Параметры не приняты. errors — сообщения по полям, fields — их имена
UnauthorizedError 403, 401 Ключ не передан или недействителен
PaymentRequiredError 402 На счёте не хватает средств
RateLimitError 429 Превышен лимит частоты
ServiceUnavailableError 503 Выдачу получить не вышло. Деньги не списаны
ApiError прочие Любой другой отказ сервиса

У всех ошибок сервиса есть status, body, разобранный payload и retry_after — срок, который назвал сервис, если он его назвал.

Ошибка Когда
NetworkError До сервиса не достучались: сеть, DNS, TLS
TimeoutError Ответа не дождались
IncompleteResponseError Соединение оборвалось посреди тела
ParseError Ответ пришёл, но не разобрался как JSON. body — тело как есть
InvalidArgumentError SDK забраковал аргументы, запрос не отправлялся
from jsonseo import PaymentRequiredError, RateLimitError, ValidationError

try:
    serp = client.yandex("купить ноутбук", pages=50)
except ValidationError as error:
    for field, messages in error.errors.items():
        print(field, ":", ", ".join(messages))
except PaymentRequiredError:
    print("Баланс кончился:", client.balance()["balance"])
except RateLimitError as error:
    print("Вернуться через", error.retry_after, "с")

TimeoutError наследует и встроенный TimeoutError, так что привычный except TimeoutError его тоже поймает.

Повторы

У каждого запроса три попытки по умолчанию: одна основная и две повторных. Если сервис затупил и выдачу собрать не вышло (503), SDK сам сходит ещё дважды, и обычно этого хватает. attempts=1 отключает повторы совсем.

429, 5xx и обрывы связи повторяются автоматически — это ровно те отказы, за которые сервис денег не берёт. Отказы по ключу, балансу и параметрам не повторяются: сами они не изменятся.

Таймаут и оборвавшееся посреди тела соединение не повторяются, и это намеренно: работу на стороне сервиса обрыв у клиента не отменяет — выдача будет собрана и оплачена, а повтор стоил бы ещё раз. Если ответ не успевает прийти, поднимайте timeout, а не attempts.

Пауза между попытками удваивается и разбавляется случайной добавкой. Если сервис прислал Retry-After, SDK не вернётся раньше названного срока. Когда сервис просит ждать дольше max_retry_delay, SDK не ждёт вовсе, а отдаёт ошибку с retry_after — решение остаётся за вами.

Настройки клиента

client = Client(
    "ВАШ_КЛЮЧ",
    base_url="https://jsonseo.ru/api",  # адрес API
    timeout=300.0,                      # сколько ждать ответа на попытку, секунд
    attempts=3,                         # всего попыток, вместе с первой
    retry_delay=1.0,                    # стартовая пауза между попытками
    max_retry_delay=30.0,               # потолок паузы
    auth="header",                      # или "query" — ключ в параметре key
    user_agent="мой-проект/1.0",
    transport=my_transport,             # свой транспорт
)

Настройки — только именованные аргументы, поэтому опечатку ловит сам Python: Client("КЛЮЧ", timeuot=5) не запустится.

Таймаут по умолчанию намеренно большой: многостраничный запрос выдачи собирается минутами, и обрыв на стороне клиента не отменяет запрос на стороне сервиса — деньги за него уже списаны. Считается он на каждую попытку отдельно, а не на весь вызов.

Ключ по умолчанию едет в заголовке Authorization: Bearer, а не в адресе: так он не оседает в логах прокси и серверов. auth="query" нужен там, где заголовки до API не доходят.

Свой транспорт

Если HTTP в проекте уже ходит через requests, httpx или что-то своё, SDK можно отдать этот клиент — достаточно объекта с одним методом:

Транспорт обязан бросать ошибки SDK: от их класса зависит, повторит клиент запрос или нет. Чужие исключения пройдут мимо политики повторов и мимо пользовательского except JsonSeoError.

import requests

from jsonseo import IncompleteResponseError, NetworkError, TimeoutError
from jsonseo.transport import Response

class RequestsTransport:
    def __init__(self, session):
        self.session = session

    def send(self, method, url, headers, body, timeout):
        try:
            response = self.session.request(
                method, url, headers=headers, data=body, timeout=timeout
            )
        except requests.Timeout as error:
            raise TimeoutError("Ответа не дождались.") from error
        except requests.ConnectionError as error:
            raise NetworkError("Запрос не удался: {}.".format(error)) from error

        try:
            text = response.text
        except requests.RequestException as error:
            # Заголовки уже пришли: выдача собрана и оплачена, повторять нельзя.
            raise IncompleteResponseError("Ответ пришёл не целиком.") from error

        return Response(response.status_code, dict(response.headers), text)

Тот же приём годится для тестов: подмените транспорт заглушкой, и запросы никуда не пойдут.

Разработка

python -m unittest discover -s tests -t .

Тесты идут без сети: транспорт подменяется заглушкой.

Лицензия

MIT.

Metadata

Release files for jsonseo 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jsonseo 1.0.0
File Size Uploaded
jsonseo-1.0.0.tar.gz 33.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jsonseo 1.0.0
File Interpreter ABI Platform
jsonseo-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 56.0 kB

Release files / jsonseo-1.0.0.tar.gz

Download URL jsonseo-1.0.0.tar.gz
Size 33.6 kB
Tags Source
SHA-256 checksum
How to use checksums
c36775fb62184a38ec09896c6c380e0eac83a75c8d2dec08276e11c626ac4c4c
BLAKE2b-256 checksum
How to use checksums
c7287c678ea90183510f25c715d82367edb35b28d60085dd6482980f947d92e8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / jsonseo-1.0.0-py3-none-any.whl

Download URL jsonseo-1.0.0-py3-none-any.whl
Size 22.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a50c3582a51ad65a2bc4ddfbd0c1f6838a8feb2d45f34ef3974a93f8b7950f99
BLAKE2b-256 checksum
How to use checksums
b423ef44dcb8916b82c13f38522d2200ae800104a530922971ccba3b14e07576
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release history Release notifications | RSS feed

1.0.2

2 release files

1.0.1

2 release files

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page