Скрипт Python, который идентифицирует код страны данного IP-адреса

Это скрипт Python, который я написал для определения кода страны данного IP-адреса, используя данные, полученные из тор.

Оно использует геоip.txt для определения кода страны для IPv4-адресов и геоip6.txt сделать это для адресов IPv6.

Это делается путем преобразования адреса в целое число, а затем нахождения индекса начального IP-адреса диапазонов IP-адресов для этого типа IP-адреса, ближайшего к целому числу, с помощью двоичного поиска с использованием bisect.bisect. Затем проверьте конечный IP-адрес диапазонов IP-адресов, расположенных по этому индексу, если целое число не больше конечного IP-адреса, верните соответствующий код страны, расположенный по индексу.

Я написал сценарий и как задачу по программированию, и как библиотеку для использования в более поздних сценариях. Я специально решил не использовать ipaddress модуль, вместо этого я написал пользовательские функции для преобразования IPv4 и IPv6 в int и назад. И я сравнил свой код с ipaddress модуль, и я обнаружил, что мой код более экономичен по времени.

Код:

import re
from bisect import bisect

MAX_IPV4 = 2**32-1
MAX_IPV6 = 2**128-1
DIGITS = set('0123456789abcdef')
le255 = '(25[0-5]|2[0-4]\d|[01]?\d\d?)'
IPV4_PATTERN = re.compile(f'^({le255}\.){{3}}{le255}$')
EMPTY = re.compile(r':?\b(?:0\b:?)+')


def parse_ipv4(ip: str) -> int:
    assert isinstance(ip, str) and IPV4_PATTERN.match(ip)
    a, b, c, d = ip.split('.')
    return (int(a) << 24) + (int(b) << 16) + (int(c) << 8) + int(d)


def to_ipv4(n: int) -> str:
    assert isinstance(n, int) and 0 <= n <= MAX_IPV4
    return ".".join(str(n >> i & 255) for i in range(24, -1, -8))


def parse_ipv6(ip: str) -> int:
    assert isinstance(ip, str) and len(ip) <= 39
    segments = ip.lower().split(":")
    l, n, p, fields, compressed = len(segments), 0, 7, 0, False
    last = l - 1
    for i, s in enumerate(segments):
        assert fields <= 8 and len(s) <= 4 and not set(s) - DIGITS
        if not s:
            if i in (0, last):
                continue
            assert not compressed
            p = l - i - 2
            compressed = True
        else:
            n += int(s, 16) << p*16
            p -= 1
        fields += 1
    return n


def to_ipv6(n: int, compress: bool = False) -> str:
    assert isinstance(n, int) and 0 <= n <= MAX_IPV6
    ip = '{:032_x}'.format(n).replace('_', ':')
    if compress:
        ip = ':'.join(s.lstrip('0')
            if s != '0000' else '0' for s in ip.split(':'))
        longest = max(EMPTY.findall(ip))
        if len(longest) > 2:
            ip = ip.replace(longest, '::', 1)
    return ip


def parse_entry4(e: str) -> tuple:
    a, b, c = e.split(",")
    return (int(a), int(b), c)


def parse_entry6(e: str) -> tuple:
    a, b, c = e.split(",")
    return (parse_ipv6(a), parse_ipv6(b), c)


with open("D:/network_guard/geoip.txt", "r") as file:
    data4 = list(map(parse_entry4, file.read().splitlines()))
starts4, ends4, countries4 = zip(*data4)

with open("D:/network_guard/geoip6.txt", "r") as file:
    data6 = list(map(parse_entry6, file.read().splitlines()))
starts6, ends6, countries6 = zip(*data6)


class IP:
    parse = [parse_ipv4, parse_ipv6]
    starts = [starts4, starts6]
    ends = [ends4, ends6]
    countries = [countries4, countries6]


def geoip_country(ip: str, mode: int=0) -> str:
    assert mode in {0, 1}
    n = IP.parse[mode](ip)
    if not (i := bisect(IP.starts[mode], n)):
        return False
    i -= 1
    return False if n > IP.ends[mode][i] else IP.countries[mode][i]

if __name__ == '__main__':
    ipv6s = [
        '2404:6800:4003:c03::88', '2404:6800:4004:80f::200e',
        '2404:6800:4006:802::200e', '2607:f8b0:4004:800::200e',
        '2607:f8b0:4005:801::200e', '2607:f8b0:4006:81c::200e',
        '2607:f8b0:4006:822::200e', '2607:f8b0:4006:823::200e',
        '2607:f8b0:4006:824::200e', '2607:f8b0:400a:809::200e',
        '2800:3f0:4001:80b::200e', '2a00:1450:400b:c01::be'
    ]

    ipv4s = [
        '74.125.24.93', '74.125.24.136', '74.125.24.190',
        '74.125.68.91', '74.125.68.93', '74.125.68.136',
        '74.125.193.91', '74.125.193.93', '74.125.193.136',
        '74.125.193.190', '74.125.200.91', '142.250.64.78'
    ]

    for ipv4 in ipv4s:
        n = parse_ipv4(ipv4)
        print(ipv4, n, to_ipv4(n), geoip_country(ipv4))

    for ipv6 in ipv6s:
        n = parse_ipv6(ipv6)
        print(ipv6, n, to_ipv6(n, 1), geoip_country(ipv6, 1))

Выход:

74.125.24.93 1249712221 74.125.24.93 US
74.125.24.136 1249712264 74.125.24.136 US
74.125.24.190 1249712318 74.125.24.190 US
74.125.68.91 1249723483 74.125.68.91 US
74.125.68.93 1249723485 74.125.68.93 US
74.125.68.136 1249723528 74.125.68.136 US
74.125.193.91 1249755483 74.125.193.91 US
74.125.193.93 1249755485 74.125.193.93 US
74.125.193.136 1249755528 74.125.193.136 US
74.125.193.190 1249755582 74.125.193.190 US
74.125.200.91 1249757275 74.125.200.91 US
142.250.64.78 2398765134 142.250.64.78 US
2404:6800:4003:c03::88 47875086426100614638538221612324356232 2404:6800:4003:c03::88 AU
2404:6800:4004:80f::200e 47875086426101804896252833647432835086 2404:6800:4004:80f::200e AU
2404:6800:4006:802::200e 47875086426104222508084389947558076430 2404:6800:4006:802::200e AU
2607:f8b0:4004:800::200e 50552053919386769199309343019258355726 2607:f8b0:4004:800::200e US
2607:f8b0:4005:801::200e 50552053919387978143575701722142613518 2607:f8b0:4005:801::200e US
2607:f8b0:4006:81c::200e 50552053919389187567457406341475213326 2607:f8b0:4006:81c::200e US
2607:f8b0:4006:822::200e 50552053919389187678137870783732523022 2607:f8b0:4006:822::200e US
2607:f8b0:4006:823::200e 50552053919389187696584614857442074638 2607:f8b0:4006:823::200e US
2607:f8b0:4006:824::200e 50552053919389187715031358931151626254 2607:f8b0:4006:824::200e US
2607:f8b0:400a:809::200e 50552053919394022920247727457692557326 2607:f8b0:400a:809::200e US
2800:3f0:4001:80b::200e 53169199713192736830836323499043201038 2800:3f0:4001:80b::200e AR
2a00:1450:400b:c01::be 55827987829231936335941766789076091070 2a00:1450:400b:c01::be IE

Это действительно работает, но я хочу, чтобы мой код был более лаконичным и эффективным, а также более питоническим. Как я могу это сделать?

(PS Я также создал версию с документацией, используя mintlify только для концертов, я не несу ответственности за большинство документов, я просто нажал CTRL+. в Visual Studio Code, но я редактировал здесь и там. Поскольку это слишком многословно, я загрузил его на Гугл Диск)

1 ответ
1

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


Мы видим, что здесь определен Public API. Но это не задокументировано, кроме аннотированных подписей.

Мы нуждаться чтобы увидеть «»»строки документации»»» на них.

Процитируйте свои ссылки. Возможно, есть RFC, которому вы стремитесь соответствовать?


IPV4_PATTERN = re.compile(f'^({le256}\.){{3}}{le256}$')

Это очень мило, спасибо за СУШКУ.

le256 = '(25[0-5]|2[0-4]\d|[01]?\d\d?)'

Это «меньше или равно 256», OTOH, производит на меня впечатление неправильного подхода.

Используйте правильный инструмент для работы. Разберите цифры с помощью регулярного выражения, а затем оцените, является ли целое число A <= B, используя другие средства языка.

Имя определенно неправильный и должно быть исправлено — похоже, что намерение автора было lt256
(или, возможно, le255).

Идентификатор DIGITS вроде в порядке. Но подумайте о том, чтобы переименовать его в HEXDIGITS.


мне немного грустно, что parse_ipv4 и to_ipv4
не имеют параллельной структуры. То есть синтаксический анализ выполняется с помощью развернутого вручную цикла, а обратный вариант немного более элегантен из-за явного цикла.

Одно утверждение, выполненное четыре раза, было бы естественным способом выразить, что мы хотим 0 <= n < 256 значение байта.

Аннотации типов прекрасны; Я благодарю тебя.


Хорошо, давайте разберем IPv6!

    l, n, p, fields, compressed = len(segments), 0, 7, 0, False

Нет, пожалуйста, не делай этого. Да, короче. Нет, это не поможет внимательному читателю понять, что происходит. Экономия четырех SLOC просто не стоит того.

Также, l явно «длина», но я не знаю, как я должен мысленно произносить p. Может быть, переименовать его в pos за «положение»? Можем ли мы найти более конкретное имя?

Пеп-8
советует вам выбрать другое написание для вашей переменной длины.

Неоднократно утверждая not set(s) - DIGITS работает. Но кажется более удобным выполнить эту проверку набора символов только один раз для всей строки впереди.

Попробуйте переименовать s к seg.

В целом, это просто не очень приятная функция для работы. это делает
больше чем одна вещьи эти вещи чередуются в итерациях цикла.

Рассмотрите возможность переделки этой функции, чтобы разделить работу на три этапа:

  1. проверьте, есть ли только допустимые символы
  2. нормализовать, чтобы не было :: сокращения
  3. преобразовать в целое число

Как написано, проходя в "" пустая строка возвращает ноль. я предполагать это типа правильно? Но я не хочу говорить, что пустая строка является действительным адресом IPv6. В любом случае это определенно не широковещательный адрес со всеми нулями, так как его нет, с ff02::1 многоадресные рассылки берут на себя эту роль.


При преобразовании обратно в строку неясно, что антишаблон проверки isinstance(n, int) действительно делает все для нас. Утиного ввода достаточно, учитывая, что мы сразу сравниваем с нулем, а затем используем шестнадцатеричный формат. Типовая аннотированная подпись очень четко указывает на это для mypy и для людей.

Подумайте о том, чтобы обнять утку и удалить isinstance из четырех функций.

        ip = ':'.join(s.lstrip('0')
            if s != '0000' else '0' for s in ip.split(':'))

Нет, это выражение не поддерживается. Это правильно, конечно, но это неинтересно. Выделите именованный помощник, который может быть вызван модульным тестом.

Это, по-видимому, производит неправильный ответ, слишком короткий: to_ipv6(parse_ipv6(""))

Кажется, что намерение автора было форматной строкой '{:039_x}'


Анализ записи{4,6} относится к отдельному модулю, поскольку эти функции связаны с отдельной концепцией. Мы перешли от общих IP-адресов к форматам файлов, специфичным для GeoIP.

with open("D:/network_guard/geoip.txt", ...

Избегайте выполнения такой работы на верхнем уровне модуля. Почему? Таким образом, модульный тест, выполняющий import этого кода не может завершиться ошибкой FileNotFoundError. Лучше спрятать этот код в функцию, которую можно будет вызвать при необходимости. Вы (очень красиво) написали __main__ охранник с аналогичной мотивацией, чтобы избежать побочных эффектов при import время.


class IP:

Это большой имя класса. Рассмотрите возможность создания IPv4 и IPv6 классы, которые соответствуют контракту, определенному
абстрактный базовый класс.


def geoip_country(ip: str, mode: int=0) -> str:

mode идентификатор довольно расплывчатый. Цель здесь ip_type или ip_version.

Учитывая, что мы не будем использовать целые числа 4 или 6это, вероятно, должно быть
перечисление.

        return False

Нет. Ты только что сказал мне, несколькими строчками выше, что вернешься strи bool не является строкой.

Также предпочитаю возвращаться None вместо Falseесли по какой-то причине вы чувствуете "" пустая строка не подходит для вашего варианта использования.

Таким образом, подпись будет заканчиваться: ...) -> Optional[str]:


        print(ipv6, n, to_ipv6(n, 1), geoip_country(ipv6, 1))

Нет.

Вы сказали мне в подписи, что звонящие должны вместо этого сказать это:

        print(ipv6, n, to_ipv6(n, True), geoip_country(ipv6, 1))

Эта кодовая база достигает подмножества своих целей проектирования. Он работает правильно на части входного пространства. Он менее хорошо протестирован и задокументирован, чем конкурирующие библиотеки.

Любопытно, что эта кодовая база избегает единообразного представления IP-адреса в виде вектора байтов, предпочитая работать со строками и большими целыми числами.

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

  • отсутствие строк документации
  • отсутствие юнит-тестов
  • загадочные идентификаторы

Я только что взглянул на вывод mintlify. Это утилита, о которой я никогда не слышал.

Текст, который он генерирует, просто ужасен, похож на

    i += 1  # increment the value of index i by exactly one

Рекомендуем не использовать его в проектах кодирования. Напишите по одному предложению для каждой строки документации, за которым может следовать пустая строка и абзац.

Делать рассмотрите возможность использования
доктестгде вы добавляете «>>> некоторое (выражение)» и его результат.

Проблема с комментариями в том, что иногда они лгут и не синхронизируются с изменяющимися правками кода. Самое замечательное в комментариях doctest то, что они достоверно говорят правду, даже после правок.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *