Этот рецепт показывает одну и ту же задачу на двух языках: единая функция asn_lookup(ip) / asnLookup(ip), оборачивающая GET /api/v1/ip?ip=<addr>&include=network бесплатного API ip.xakki.pro, с аккуратной обработкой ошибок для невалидного IP, ответа с ограничением частоты и сбоя сети — скопируйте ту версию, что подходит вашему стеку.
Зависимости: нет — только стандартная библиотека Python 3.8+ (urllib, json, csv, ipaddress, argparse). pip install не требуется.
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""asn-lookup.py -- a single asn_lookup(ip) function wrapping
GET /api/v1/ip?ip=<addr>&include=network, with clean error handling.
Shown alongside asn-lookup.js -- the same task in Python and JavaScript/Node,
the two languages this section's audience is most likely to already have
installed, so you can copy whichever one matches your stack without
translating the other by hand.
Zero dependencies -- stdlib only (urllib, json, ipaddress). Python 3.8+.
Usage as a library:
from importlib.machinery import SourceFileLoader
m = SourceFileLoader("asn_lookup", "asn-lookup.py").load_module()
print(m.asn_lookup("8.8.8.8"))
Usage as a CLI:
python3 asn-lookup.py 8.8.8.8
"""
import ipaddress
import json
import sys
import urllib.error
import urllib.request
DEFAULT_BASE_URL = "https://ip.xakki.pro"
USER_AGENT = "myip-asn-lookup-recipe/1.0 (+https://ip.xakki.pro/tools/scripts/asn-lookup.html)"
class InvalidIPError(ValueError):
"""Raised when the input string is not a valid IPv4/IPv6 address."""
class RateLimitedError(RuntimeError):
"""Raised when the API's "network" group came back rate-limited
(soft-withheld -- see the rate-limit note in bulk-enrich.py) for THIS
lookup, so no ASN data is available right now."""
def asn_lookup(ip, base_url=DEFAULT_BASE_URL, timeout=15):
"""Returns a dict {asn, as_name, prefix, registry, allocated, country,
classification_type} for ip. Raises InvalidIPError for a malformed
input (checked BEFORE any request -- an invalid `ip` query param is
silently ignored by the API and answers about the caller's own IP
instead, which would silently mislabel the result), RateLimitedError
when the network group is withheld this call, or urllib.error.URLError
/ json.JSONDecodeError for a genuine transport/protocol failure."""
try:
ipaddress.ip_address(ip)
except ValueError:
raise InvalidIPError('"{}" is not a valid IPv4 or IPv6 address'.format(ip))
url = "{}/api/v1/ip?ip={}&include=network".format(base_url, ip)
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "application/json"})
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode("utf-8"))
if ipaddress.ip_address(data.get("ip", ip)) != ipaddress.ip_address(ip):
raise RuntimeError("unexpected response: ip mismatch (got {!r})".format(data.get("ip")))
if "network" in (data.get("limited") or []):
raise RateLimitedError("the network group was rate-limited for this call; retry later")
network = data.get("network") or {}
rdap = network.get("rdap") or {}
classification = network.get("classification") or {}
return {
"asn": network.get("asn"),
"as_name": network.get("asName"),
"prefix": network.get("prefix"),
"registry": network.get("registry"),
"allocated": network.get("allocated"),
"country": rdap.get("country"),
"classification_type": classification.get("type") or None,
}
def main():
if len(sys.argv) != 2:
sys.exit("usage: python3 asn-lookup.py <ip>")
ip = sys.argv[1]
try:
result = asn_lookup(ip)
except InvalidIPError as e:
sys.exit("invalid input: {}".format(e))
except RateLimitedError as e:
sys.exit("rate limited: {}".format(e))
except (urllib.error.URLError, json.JSONDecodeError) as e:
sys.exit("request failed: {}".format(e))
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
// SPDX-License-Identifier: MIT
// asn-lookup.js -- a single asnLookup(ip) function wrapping
// GET /api/v1/ip?ip=<addr>&include=network, with clean error handling.
//
// Shown alongside asn-lookup.py -- the same task in JavaScript/Node and
// Python, the two languages this section's audience is most likely to
// already have installed, so you can copy whichever one matches your stack
// without translating the other by hand.
//
// Zero dependencies -- only Node.js built-ins: global fetch (stable since
// Node 18, this repo's minimum target for client-side code across every
// TOOLS-0x recipe) and the `net` core module for IP validation. No
// npm install, no package.json.
//
// Usage as a module:
// const { asnLookup } = require('./asn-lookup.js');
// asnLookup('8.8.8.8').then(console.log);
//
// Usage as a CLI:
// node asn-lookup.js 8.8.8.8
'use strict';
const net = require('net');
const DEFAULT_BASE_URL = 'https://ip.xakki.pro';
const USER_AGENT = 'myip-asn-lookup-recipe/1.0 (+https://ip.xakki.pro/tools/scripts/asn-lookup.html)';
class InvalidIPError extends Error {}
class RateLimitedError extends Error {}
// asnLookup(ip, baseUrl?) -> Promise<{asn, asName, prefix, registry,
// allocated, country, classificationType}>. Rejects with InvalidIPError for
// a malformed input (checked BEFORE any request -- an invalid `ip` query
// param is silently ignored by the API and answers about the caller's own
// IP instead, which would silently mislabel the result), RateLimitedError
// when the network group is withheld this call, or a plain Error for a
// transport/HTTP failure.
async function asnLookup(ip, baseUrl) {
if (net.isIP(ip) === 0) {
throw new InvalidIPError('"' + ip + '" is not a valid IPv4 or IPv6 address');
}
const base = baseUrl || DEFAULT_BASE_URL;
const url = base + '/api/v1/ip?ip=' + encodeURIComponent(ip) + '&include=network';
const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT, Accept: 'application/json' } });
if (!response.ok) {
throw new Error('request failed: HTTP ' + response.status);
}
const data = await response.json();
// Guard against the API's own documented behavior: an invalid `ip` query
// param is silently IGNORED (not rejected), and the endpoint falls back
// to answering about the CALLER's own IP instead. We already validated
// ip above, so this should never fire -- but if it ever does, never
// attribute the wrong address's ASN to this call. A full semantic
// comparison (matching IPv4-mapped forms, zone ids, etc.) needs a real IP
// library; `net.isIP` has no equality helper, so this checks the
// cheaper, still-useful signal available from Node's core module alone:
// same address family AND same literal string as requested.
if (data.ip !== ip && net.isIP(data.ip || '') !== net.isIP(ip)) {
throw new Error('unexpected response: ip family mismatch (got ' + data.ip + ')');
}
const limited = data.limited || [];
if (limited.indexOf('network') !== -1) {
throw new RateLimitedError('the network group was rate-limited for this call; retry later');
}
const network = data.network || {};
const rdap = network.rdap || {};
const classification = network.classification || {};
return {
asn: network.asn || null,
asName: network.asName || null,
prefix: network.prefix || null,
registry: network.registry || null,
allocated: network.allocated || null,
country: rdap.country || null,
classificationType: classification.type || null,
};
}
async function runCLI(argv) {
if (argv.length !== 1) {
console.error('usage: node asn-lookup.js <ip>');
process.exitCode = 1;
return;
}
try {
const result = await asnLookup(argv[0]);
console.log(JSON.stringify(result, null, 2));
} catch (err) {
if (err instanceof InvalidIPError) {
console.error('invalid input: ' + err.message);
} else if (err instanceof RateLimitedError) {
console.error('rate limited: ' + err.message);
} else {
console.error('request failed: ' + err.message);
}
process.exitCode = 1;
}
}
if (require.main === module) {
runCLI(process.argv.slice(2));
}
module.exports = { asnLookup: asnLookup, InvalidIPError: InvalidIPError, RateLimitedError: RateLimitedError };
Скрипт распространяется по лицензии MIT (заголовок SPDX-License-Identifier: MIT в начале файла) — копируйте, изменяйте и используйте свободно в своём проекте, указывать авторство не обязательно.
Обе версии выбрасывают/отклоняют промис с отдельной, именованной ошибкой для каждого из трёх классов сбоев, а не одним общим исключением: InvalidIPError / InvalidIPError для некорректного входа (проверяется локально ДО любого запроса), RateLimitedError / RateLimitedError, когда группа «network» в этом вызове пришла с ограничением, и обычная error/URLError для настоящего сбоя сети или HTTP — вызывающий код может перехватить именно тот случай, который ему важен, а не сравнивать строку сообщения.
Важный нюанс, о котором стоит знать до запуска на реальных данных: параметр запроса ?ip= в API не отклоняет некорректный адрес — если он не парсится, запрос молча откатывается к ответу о СОБСТВЕННОМ IP вызывающей стороны. Для одиночного ручного запроса это безобидно, но в неконтролируемом массовом скрипте это привело бы к тому, что «плохие» строки молча подписывались бы данными IP самого скрипта, а не пропускались. Этот скрипт сначала сам проверяет каждый IP локально (модуль ipaddress в Python / net.isIP в Node), а затем дополнительно сверяет поле "ip" в ответе с запрошенным значением, отказываясь приписать несовпадающий ответ не той строке.
Python-версия сверяет поле «ip» в ответе через точное семантическое сравнение (равенство ipaddress.ip_address, которое корректно считает совпадением по-другому отформатированный, но тот же адрес); JS-версия, не имея аналогичной встроенной возможности в модуле `net` ядра Node, проверяет более дешёвый, но всё же полезный сигнал, доступный без зависимостей — что в ответе указан адрес ТОГО ЖЕ СЕМЕЙСТВА, что и запрошенный — и документирует это различие в собственном комментарии, а не тихо делает вид, что строгость одинаковая.
GET /api/v1/ip никогда не отвечает жёстким 429 от самого приложения — группы обогащения (geo, network, security) лимитируются по уникальному целевому IP в сутки (UTC) плюс более жёсткий поминутный бюджет; при превышении группа мягко придерживается: HTTP 200, "status":"partial", имя группы в "limited", и заголовки RateLimit-Limit/RateLimit-Remaining/RateLimit-Reset в ответе. Этот скрипт читает эти сигналы, а не угадывает, и явно фиксирует придержанный результат (никогда как пустое/null значение — это выглядело бы так же, как «у API нет данных по этому IP»). Настоящий 429/503 всё же возможен на границе — это собственный анти-флуд nginx, независимый от лимитов выше — если слать запросы слишком быстро с одной машины; скрипт делает экспоненциальные повторы на 429/5xx и учитывает заголовок Retry-After, если сервер его прислал. У анонимных вызовов небольшой суточный/поминутный бюджет на группу (см. заголовок RateLimit-Limit в любом ответе или /api/v1/tiers); для заметно бо́льшего бюджета зарегистрируйте бесплатный API-токен.
Сам этот скрипт и есть пример работы с API — отдельный блок curl/ответ не нужен, код выше и есть запрос. Полная схема ответа и все параметры — в документации API:
Понравился инструмент? Бесплатный аккаунт нашего API даёт 1000 запросов в день.Смотреть документацию API →