Этот Python-скрипт разбирает и валидирует CSV-файл geofeed по RFC 8805 — формат prefix,country[,region[,city[,postal]]], который операторы сетей публикуют, чтобы заявить, где реально используются их блоки IP — агрегирует число префиксов по странам и сообщает о каждой синтаксической ошибке с точным номером строки. Он не делает сетевых запросов: проверка уже скачанного файла geofeed — чисто локальный разбор.
Зависимости: нет — только стандартная библиотека Python 3.8+ (urllib, json, csv, ipaddress, argparse). pip install не требуется.
#!/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 в начале файла) — копируйте, изменяйте и используйте свободно в своём проекте, указывать авторство не обязательно.
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: одна плохая строка в большом реальном экспорте не должна стоить вам результатов по остальному файлу.
Этот скрипт не делает сетевых запросов и не имеет эквивалента в API — вся задача локальные вычисления/разбор уже имеющихся данных, отвечать здесь нашему (или любому) API не на что. Если нужны ещё и живые факты об IP (геолокация, ASN, признаки угроз/фрода) — это описано в документации API:
Понравился инструмент? Бесплатный аккаунт нашего API даёт 1000 запросов в день.Смотреть документацию API →