Пользователь
0,0
рейтинг
19 апреля 2012 в 14:18

Разработка → GeoIP и Django

Вебразработчики частенько сталкиваются с классической задачей определения местоположения пользователя по его IP-адресу. Существует множество различных решений, например на основе мировой базы Maxmind Geolite или российской IpgeoBase. Все они обладают достаточно низкуровневыми API, ну оно и понятно: на входе айпишник, на выходе страна, либо город и, если повезёт, ещё какая-нибудь полезная информация.

У всех сайтов с GeoIP, которые мы запускали, есть общая черта: они не только нуждаются в простой геолокации, необходимо также выводить различный контент на сайте в зависимости от месторасположения пользователя. Чтобы упростить для себя эту задачу мы написали небольшую батарейку django-geoip, вдохновившись приложением django-ipgeobase.


Django-geoip


Основное достоинство приложения: оно умеет автоматически определять географию пользователя и передавать её в объект request. Теперь контент на сайте, обладающий «региональностью», легко отфильтровать во views.py по значению request.location.

Фичи
  • pip install django-geoip и ещё несколько простых шагов для установки;
  • подробная документация на ReadTheDocs;
  • покрытие тестами;
  • обновление базы management-командой;
  • понятный API для создания «своей» модели географии в дополнение к существующим;
  • пользователь может поменять свой регион с помощью вьюхи;
  • интеграция с django-hosts.


Особенности реализации

Приложение django-geoip поддерживает иерархию географии Страна — Область — Город, которая хранится в нормализованном виде в СУБД. Данные по диапазонам IP-адресов со связями ко всем элементам географии — в четвёртой таблице. Текущая версия работает только с данными ipgeobase.ru, это почти тысяча городов России и Украины и 150 тысяч IP-диапазонов.

Одной из причин хранения данных в БД является потребность в создании своей модели географии, отвечающей задачам бизнес-логики. Например, в одном из проектов мы ограничиваем определение локации пользователя областями России, в другом — набором городов, в которых присутствует компания. Эта модель реализует паттерн «фасада» к иерархии географии ipgeobase, позволяя гибко настроить геолокацию под себя.

Представим, что наш сайт имеет несколько региональных «версий», каждая из которых может содержать свой контент. При этом регион может иметь произвольное имя и, к примеру, содержать несколько областей РФ (таблица geo_location на схеме):

Вот пример того, как это настраивается и работает. Определим свою модель географии MyCustomLocation:

# geo/models.py
class MyCustomLocation(GeoLocationFacade):
    name = models.CharField(max_length=100)
    region = models.OneToOneField(Region, related_name='my_custom_location')
    is_default = models.BooleanField(default=False)

    @classmethod
    def get_by_ip_range(cls, ip_range):
        """ Получаем модель географии по IP-дапазону.
	        В данном примере диапазон связан с регионом, тот, в свою очередь,  
 	    связан с нашей кастомной моделью географии
        """
        return ip_range.region.geo_location

    @classmethod
    def get_default_location(cls):
     """ Локация по-умолчанию, на случай, если не смогли определить местоположение по IP"""
        return cls.objects.get(is_default=True)

    @classmethod
    def get_available_locations(cls):
        return cls.objects.all()

	class Meta:
	db_table = 'geo_location'

Это обычная джанго-модель, дополненная тремя классметодами, реализующие “интерфейс” фасада GeoIP. Назначение каждой функции понятно из названия и докстринга. Осталось заполнить базу названиями городов и привязать их к моделям djagno-geoip.

Прописываем в settings.py:
GEOIP_LOCATION_MODEL = 'geo.models.MyCustomLocation'

Добавляем middleware для автоматического определения региона:
MIDDLEWARE_CLASSES = (...
    'django_geoip.middleware.LocationMiddleware',
    ...
)

И вуаля: request.location теперь содержит значение нашей модели MyCustomLocation для каждого пользователя.
def my_view(request):
    """ Passing location into template """
    ...
    context['location'] = request.location
    ...

Если пользователь не оказался ни в одном из этих городов, ему будет присвоен регион по-умолчанию (get_default_location).

Данный подход сильно облегчает задачу создания «региональных» сайтов, отличающихся друг от друга содержимым в зависимости от месторасположения пользователя.

Что дальше


Приложение хотя и альфа, неплохо работает у нас в продакшене (версия 0.2.2). Статус альфа намекает, что в будущем API будет меняться. Мы планируем поддерживать и развивать его дальше, в том числе реализовать определение региона не только для России, но и для остального мира. Также показалось интересной идея оптимизации поиска подходящего IP-диапазона по базе.

Исходные коды доступны на гитхабе, жду ваших комментариев.
Илья Барышев @Prophet
карма
48,4
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (32)

  • 0
    Вы, ребята, молодцы. И блог у вас классный, и распространяемые приложения очень даже.

    По поводу этого геолокатора: я правильно понял, что оно не кеширует результаты и при каждом явном обращении к request.location пытается определить, откуда пользователь?
    • 0
      По умолчанию в куке хранит, чтобы лишний раз в базу не лезть. Добавлю в доукментацию это уточнение. Спасибо
  • +1
    Имхо это велосипед, и nginx и апач имеют mod_geoip, которую можно развернуть в памяти. Зачем лазить в БД на каждый запрос? По-моему это бред и ересь.
    • 0
      GEOIP_COUNTRY_CODE and GEOIP_COUNTRY_NAME и все?
      • +2
        Не все, у GeoIP много баз.
        $ ls *dat
        GeoIPCity-532.dat GeoIP.dat GeoIPISP.dat GeoIPNetSpeed.dat GeoIPOrg.dat
        • 0
          Ясно. Спасибо за информацию.
    • 0
      На каждый запрос никто в базу не лезет, один раз для пользователя, результат геолокации для пользователя кэшируется в куке (уже писал выше). Почему в базе, а не mod_geoip, написано в статье: «потребность в создании своей модели географии, отвечающей задачам бизнес-логики». Мы сделали выбор в пользу удобства создания своих кастомных регионов на основе географии ipgeobase в ущерб скорости и пока не жалеем.
      • +2
        Это все также можно сделать стандартным модулем для nginx mod_geo и не лазить в базу, это будет гораздо быстрее.
        А минусующие — пусть приводят аргументы.

        Мое мнение — даже на каждый первый запрос лазить в базу — очень плохо, когда этого можно избежать.
        • 0
          А как сделать, чтобы mod_geoip работал с базой ipgeobase.ru?
          • 0
            wget ipgeobase.ru/files/db/Main/geo_files.tar.gz;tar xf geo_files.tar.gz;cat cidr_optim.txt |awk '{print $3$4$5" "$6";"}'
            • 0
              да и какой толк в этой базе? Хотя бы по размеру сравните с maxmind.
              • 0
                У Maxmind совсем плохо с городами в России, Украине и других постСССР. В то же время в ipgeobase вообще нет городов забугорья. Так что нужно совмещать :)
                • 0
                  Совмещать не проблема, кстати.
                  • +1
                    Так я и не говорю, что это проблема, сам делаю для своего SypexGeo. Скорее можно сказать, что качественно совмещать проблема, даже не столько проблема, сколько затратно по времени.
        • 0
          да можно и без mod_geo, сами используем бинарную базу от maxmind — шустро, просто
        • 0
          Вы правы в том, что когда можно избежать лезть в базу — лучше не лезть. А ведь может случиться так, что информация понадобится не для редиректа на поддомен, а в логике самого проекта. Простейший пример:

          <p>
          Выберите город, {{ request.user }}. Текущее значение – <b>{{ request.location }}</b>
          </p>
          


          Можно получить эту информацию из nginx? Думаю, что можно. Но костыли, применяемые при этом, едва ли будут менее позорны, чем лишний запрос и красивый код :-)
          • +1
            никаких костелей там нет

            proxy_set_header GEOIP_COUNTRY_CODE $geoip_country_code;
            


            fastcgi_param GEOIP_COUNTRY_CODE $geoip_country_code;
            


            • 0
              То есть Вы считаете, что размазать логику на два слоя — Джанги и фронтенда — это вовсе не костыль, а сделать один дополнительный запрос к БД — восьмой смертный грех? =)
              • +1
                Это вовсе не костыль, берем любой типичный виртуальный хостинг, GEOIP_COUNTRY_CODE вы видите в переменных окружения апача, никто на пэхопэ не залезает в базу данных GeoIP.dat. Кроме того, это эффективно работает с несколькими сайтами на сервере (хорошо если сервер ваш и пара сайтов, а если их на сервере 500), каждому отдельному скрипту на каждом отдельном виртуалхосте ненужно лазить ни в каких базы данных, будь они sql, текст или бинарник — все есть в переменных окружения сервера. И так все работает везде уже лет десять как, а то и больше.
                • –2
                  Не поймите меня неправильно, я не спорю о том, что nginx — великолепная штука, встроенная нересурсоёмкая геолокация из коробки — это тоже замечательно и правильно.

                  Но случай с джангой скорее исключение. Во-первых, она сама по себе достаточно прожорлива, поэтому её запускают обычно на ВДС или в облаках. Но не на виртуальных хостингах.

                  ВО-вторых, как ни крути, работа с нативными питоновскими объектами удобнее, чем две строки, получаемые из внешней среды.

                • 0
                  Чёрт возьми, а давайте помиримся? :-)

                  Надо всего лишь предложить ребятам написать бэкенд для получения гео-локации из окружения и тогда мы оба будем правы!
                  • 0
                    Я не ссорился ;)

                    Это абсолютно не нужно писать, ибо все есть в request.META
                    • 0
                      Если в приложении вам достаточно получить строку с названием города или страны — то django-geoip вам, конечно же не нужен. Я уверен, что для массы проектов request.META — более чем достаточно. Но это не наш случай и в заметке я объяснил почему.
                    • +1
                      Ну ё-моё :-)

                      В request.META будет простое текстовое значение типа RU. Да, его легко вывести в шаблон, но на этом сфера его пользы и заканчивается.

                      Совсем другое дело django-geoip: {{ request.location }} это полноценная модель, понимаете? Причём кастомизируемая. Её можно и сохранять, и расширять, и даже внешние ключи на неё вешать.

                      Я пару комментариев назад слово «костыли» по отношению к употребил именно с этой позиции: он может передать лишь текстовую строку, в то время как django-geoip оперирует полноценным питоновским объектом.

                      Для того, чтобы с тем же комфортом работать с geoip'ом инжиникса, придётся писать много кода (как раз те самые обращения к request.META). Ну, или как меня осинило, предложить ребятам сделать поддержку mod_geoip, благо, насколько я понял их код, это делается элементарно с помощью storage.
                      • 0
                        Не совсем, Storage отвечает за то, где хранится результат geoip-поиска: в куках или нигде :)
                        Не трогать БД в текущей реализации не получится, разве что изменив middleware. Постараюсь в следующей версии подумать над поддержкой mod_geoip.
                        • 0
                          Да, чего-то я дал маху с просмотром исходников наискосок.

                          А Вы лучше не над поддержкой mod_geoip подумайте, а над интерфейсом способа локации. Чтоб и в базе можно было искать, и в бинарном файле и из окружения.

                          Интерфейсы вообще штука хорошая :-)
                  • 0
                    Получать геолокацию из окружения не проблема, а как быть с тем, что нам нужна своя, пользовательская география, которую необходимо редактировать администрации сайта? Она хранится в базе обычно, поэтому там же для удобства их связывания хранятся и данные ipgeobase в нашем решении. Да, это не оптимально по производительности, зато позволяет контролировать всю бизнес-логику прямо в django.

                    Можно заморочиться и выгружать заранее подготовленные данные со своей географией в формате, который поддерживает nginx, чтобы полностью избавиться от завязки на БД. Это решение мне нравится, но пока что игра не стоит свеч для текущих проектов с их нагрузкой.
                    • 0
                      Ну так и нужно хранить пользовательскую географию в модели. А если её внезапно не оказалось, или назначать администратором, или пусть юзер при следующем заходе сам инициализирует геолокацию.

                      А уж как она выполняться будет — от настроек проекта зависит. Хоть sql-поиском в базе IpGeoBase, хоть напрямую из окружения nginx :-)

  • +2
    Ну опустим тот факт, что SQL решение будет работать значительно медленнее, чем решения с бинарными базами.
    Но зачем для хранения IP использовать BIGINT? Чтобы удвоить размер таблицы и индексов? Аналогично INT для городов и регионов, да даже в GeoIP с кучей дубликатов получается около 140 тысяч городов, регионов на порядок меньше.
    • +1
      Плюс к этому db_index на полях start_ip и end_ip создаст два отдельных индекса, а при поиске по таблице будет использоваться один. Опять же получаем таблицу большего размера, чем необходимо.
    • 0
      Вы правы, unsigned int тут хватило бы. Можно также уменьшить размер первичных ключей, бесспорно. Но зачем мелочиться, если вам действительно нужна производительность в первую очередь — не надо использовать SQL-решение :) У нас trade-off производительность меняем на удобство поддержки кастомной географии.
      • +1
        Ну пусть география у Вас кастомная, но IP то у Вас больше 4 байт не бывают (для IPv6 всё равно BIGINT не подходит)?

        К примеру, у Вас сейчас один диапазон в таблице занимает 31 байт, если туда загнать базу GeoIP, то получим около 3,5 млн. диапазонов. Я протестил на базе GeoIP, получилось 106 МБ необходимых только для данных, и еще 158 МБ на индексы, т.е. только для одной таблицы с диапазонами вам понадобится 264 МБ. При том, что бинарная база с теми же данными GeoIP весит 25 МБ, и работает значительно быстрее. Приведя в порядок типы данных таблица похудеет на 85 МБ, замечу, что при этом вы абсолютно ничего не теряете. Если же сделать грамотно, то можно в MySQL добиться где-то 40-50 МБ.

        P.S. я надеюсь Вы хоть не ищите диапазон с помощью запроса типа «ip» BETWEEN start_ip AND end_ip?

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