Скрипт разбора geofeed по RFC 8805 (Python)

Этот Python-скрипт разбирает и валидирует CSV-файл geofeed по RFC 8805 — формат prefix,country[,region[,city[,postal]]], который операторы сетей публикуют, чтобы заявить, где реально используются их блоки IP — агрегирует число префиксов по странам и сообщает о каждой синтаксической ошибке с точным номером строки. Он не делает сетевых запросов: проверка уже скачанного файла geofeed — чисто локальный разбор.

Как запустить этот скрипт

Зависимости: нет — только стандартная библиотека Python 3.8+ (urllib, json, csv, ipaddress, argparse). pip install не требуется.

  1. Скачайте geofeed-parse.py ниже.
  2. Возьмите CSV geofeed — свой собственный или опубликованный (операторы сетей ссылаются на свой из записей RDAP).
  3. Запустите: python3 geofeed-parse.py geofeed.csv
  4. Прочитайте список синтаксических ошибок (если есть) и число префиксов по странам.

Полный исходный код

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""geofeed-parse.py -- parse and validate an RFC 8805 self-published geofeed
CSV, aggregating prefix counts by country and reporting any syntax errors
with line numbers.

This recipe does NOT call the ip.xakki.pro API -- validating a geofeed file
you already downloaded is pure local parsing (stdlib csv + ipaddress), the
same honest "no API needed here" case as subnet-membership.js and
cidr-aggregate.py in this section. (Our own service ingests geofeeds as ONE
input among several for its own geo data -- see /docs/ -- but reading and
checking a geofeed file's syntax is not itself an IP lookup.)

RFC 8805 defines an unquoted CSV, one prefix per line, in this exact column
order: prefix,country[,region[,city[,postal code]]] -- only "prefix" and
"country" are required; region/city/postal may be blank or omitted
entirely. This script validates:
  - prefix parses as a valid IPv4 or IPv6 CIDR block.
  - country is exactly 2 letters (ISO 3166-1 alpha-2 shape; NOT checked
    against the real country-code list, since RFC 8805 itself doesn't
    require that and new codes do get assigned).
  - the line has between 2 and 5 comma-separated fields.

Zero dependencies -- stdlib only (csv, ipaddress, argparse, re). Python 3.8+.

Usage:
    python3 geofeed-parse.py geofeed.csv
"""

import argparse
import csv
import ipaddress
import re
from collections import Counter

COUNTRY_RE = re.compile(r"^[A-Za-z]{2}$")


def parse_geofeed(path):
    """Returns (rows, errors). rows is a list of dicts
    {prefix, country, region, city, postal, line}; errors is a list of
    "line N: message" strings for every row that failed validation."""
    rows, errors = [], []
    with open(path, newline="", encoding="utf-8") as f:
        for line_no, fields in enumerate(csv.reader(f), start=1):
            if not fields or (len(fields) == 1 and fields[0].strip() == ""):
                continue  # blank line, not a syntax error
            if not (2 <= len(fields) <= 5):
                errors.append("line {}: expected 2-5 fields (prefix,country[,region[,city[,postal]]]), got {}".format(
                    line_no, len(fields)))
                continue

            prefix_raw, country_raw = fields[0].strip(), fields[1].strip()
            try:
                prefix = ipaddress.ip_network(prefix_raw, strict=False)
            except ValueError as e:
                errors.append("line {}: invalid prefix {!r} -- {}".format(line_no, prefix_raw, e))
                continue
            if not COUNTRY_RE.match(country_raw):
                errors.append("line {}: invalid country code {!r} (expected 2 letters)".format(line_no, country_raw))
                continue

            rows.append({
                "prefix": str(prefix),
                "country": country_raw.upper(),
                "region": fields[2].strip() if len(fields) > 2 else "",
                "city": fields[3].strip() if len(fields) > 3 else "",
                "postal": fields[4].strip() if len(fields) > 4 else "",
                "line": line_no,
            })
    return rows, errors


def main():
    parser = argparse.ArgumentParser(description="Parse and validate an RFC 8805 geofeed CSV.")
    parser.add_argument("input_file", help="Path to the geofeed CSV file.")
    parser.add_argument("--top", type=int, default=20, help="How many countries to print (default: 20).")
    args = parser.parse_args()

    rows, errors = parse_geofeed(args.input_file)

    print("{} valid row(s), {} error(s)".format(len(rows), len(errors)))
    if errors:
        print("\nSyntax errors:")
        for err in errors:
            print("  " + err)

    per_country = Counter(row["country"] for row in rows)
    print("\nTop {} countries by prefix count:".format(args.top))
    for country, count in per_country.most_common(args.top):
        print("  {:6d}  {}".format(count, country))

    addresses_total = sum(ipaddress.ip_network(row["prefix"]).num_addresses for row in rows)
    print("\n{} prefixes covering {} addresses total (no overlap collapsing -- see "
          "cidr-aggregate.py in this section for that)".format(len(rows), addresses_total))


if __name__ == "__main__":
    main()

Скрипт распространяется по лицензии MIT (заголовок SPDX-License-Identifier: MIT в начале файла) — копируйте, изменяйте и используйте свободно в своём проекте, указывать авторство не обязательно.

Скрипт разбора geofeed по RFC 8805 (Python)

RFC 8805 задаёт CSV без кавычек, ровно от 2 до 5 полей в строке, в фиксированном порядке колонок — обязательны только «prefix» и «country», region/city/postal могут быть пустыми либо вовсе опущены справа. Скрипт валидирует каждое поле независимо: prefix должен разбираться как настоящий CIDR-блок IPv4 или IPv6 (ipaddress.ip_network, strict=False, поэтому префикс с установленными битами хоста нормализуется, а не отклоняется — та же снисходительность, что применяет cidr-aggregate.py из этого раздела), а country должен быть ровно из 2 букв.

Поле country проверяется только по ФОРМЕ (ровно 2 буквы), но никогда не сверяется с реальным списком присвоенных кодов ISO 3166-1 — сам RFC 8805 этого не требует, а новые коды со временем всё же присваиваются, так что жёстко зашитый список стран рано или поздно отклонил бы легитимный новый код. Значение «country» из 3 букв или цифр всё равно отклоняется, поскольку такая форма заведомо не может быть alpha-2.

Каждая ошибка сообщается с номером строки, начиная с 1 (как в текстовом редакторе), и некорректная строка никогда не прерывает выполнение — все остальные корректные строки по-прежнему разбираются и учитываются. Это повторяет ту же философию сбора ошибок, что и cidr-aggregate.py: одна плохая строка в большом реальном экспорте не должна стоить вам результатов по остальному файлу.

Глоссарий

Geofeed
Самостоятельно публикуемый CSV-файл (RFC 8805), которым оператор сети заявляет, в какой стране/регионе/городе реально используется каждый из его префиксов IP — один из нескольких источников, которые обрабатывает геоданные нашего собственного сервиса, см. /docs/.
RFC 8805
Стандарт IETF, определяющий формат CSV geofeed, по которому этот скрипт выполняет валидацию — «prefix,country[,region[,city[,postal]]]», от 2 до 5 полей через запятую.
ISO 3166-1 alpha-2
Стандарт 2-буквенных кодов стран («US», «DE», «RU»), которому по форме следует колонка country geofeed — этот скрипт проверяет только форму, но не принадлежность текущему списку присвоенных кодов.
strict=False
Опция ipaddress.ip_network, принимающая префикс с установленными битами хоста и нормализующая его, вместо того чтобы выбрасывать ошибку — та же снисходительность, что применяют и этот скрипт, и cidr-aggregate.py.
Ссылка на geofeed в RDAP
Механизм (зарегистрированный IANA remarks в RDAP или отдельная link-связь), которым оператор сети указывает на свой опубликованный файл geofeed для конкретного префикса.
модуль csv
Читатель/писатель CSV из стандартной библиотеки Python — единственная не встроенная в язык зависимость, нужная этому скрипту, используется здесь исключительно для надёжного разбиения по запятым/кавычкам, но не для записи вывода.

Аналог в API

Этот скрипт не делает сетевых запросов и не имеет эквивалента в API — вся задача локальные вычисления/разбор уже имеющихся данных, отвечать здесь нашему (или любому) API не на что. Если нужны ещё и живые факты об IP (геолокация, ASN, признаки угроз/фрода) — это описано в документации API:

Читать документацию API

Часто задаваемые вопросы

Проверяет ли скрипт geofeed по реальному списку кодов стран?
Нет — проверяется, что поле country состоит из 2 букв (правильная ФОРМА), а не что конкретный код действительно присвоен на сегодня. RFC 8805 не требует более строгой проверки, а зашитый список присвоенных кодов рано или поздно устареет по мере появления новых кодов.
В чём разница между этим рецептом и cidr-aggregate.py из этого раздела?
cidr-aggregate.py схлопывает/объединяет обычный список CIDR; этот скрипт валидирует и агрегирует именно строки geofeed вида prefix,country,... — разная форма входа, разная задача, хотя оба это чисто локальная логика на модуле ipaddress без обращения к API.
Может ли строка geofeed содержать больше 5 полей?
Не по RFC 8805 — этот скрипт помечает всё, что выходит за диапазон 2-5 полей, как синтаксическую ошибку с номером строки, а не молча игнорирует лишние поля.
Использует ли сам сервис ip.xakki.pro такие geofeed-файлы?
Да — данные geofeed (импортируются через `make geofeed» в конвейере данных проекта) — один из нескольких источников, которые наше геоопределение ранжирует по доверию и специфичности; см. /docs/, как выбирается победивший источник геоданных.
Что происходит с пустой строкой или комментарием в файле?
Полностью пустая строка молча пропускается, а не считается ошибкой — RFC 8805 не определяет синтаксис комментариев, поэтому всё остальное в строке разбирается как настоящие данные и валидируется как обычно.

Понравился инструмент? Бесплатный аккаунт нашего API даёт 1000 запросов в день.Смотреть документацию API →