Geo определение по IP

Возникла задача, нужно определять город, в котором находится пользователь, пройдемся по таким сервисам:

API Яндекс Локатор - не подходит, отдает не город:

curl -H "Content-Type: application/json" \
    -X POST "https://locator.api.maps.yandex.ru/v1/locate?apikey=YOUR_API_KEY" \
    -d '{"ip":[{"address":"1.2.3.4"}]}'
результат: {"location":{"point":{"lon":37.6177,"lat":55.755863},"accuracy":100000}}

не знаю что не так, но например зная координаты (latitude и longitude) API Геокодер Яндекса отдает информацию. 

dadata.ru/api/iplocate/ - не подходит, о некоторых не знает:

curl -X GET   -H "Accept: application/json"   -H "Authorization: Token XXX"   http://suggestions.dadata.ru/suggestions/api/4_1/rs/iplocate/address?ip=102.222.79.255
результат: {"location":null}

тоже самое, dadata.ru/api/geolocate/ может рассказать про город, если ему скормить координаты.

Поехали дальше 

Есть ряд крупных сервисов, которые предоставляют базы данных IP адресов/сетей с привязкой к Гео.

maxmind

Один из них является maxmind.com (попробовать онлайн).

Данный сервис разрешает скачивать базу только тем, кто зарегистрировался.

Он предоставляет 2 версии - бесплатную и платную (вот отличие Lite от не Lite).

Бесплатная версия бывает двух видов:

  1. mmdb - не содержит latitude и longitude, но этот формат по сути является готовой базой данных
  2. csv - содержит latitude и longitude (по которым можно более точно определить пользователя)

Самый простой способ испробуем.

1. скачаем mmdb файл по ссылке:

curl -O -J -L -u YOUR_ACCOUNT_ID:YOUR_LICENSE_KEY "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=YOUR_LICENSE_KEY&suffix=tar.gz"

2. устанавливаем php-класс работы с mmdb - файлом:

composer require geoip2/geoip2

3. тестируем:

$reader = new Reader('/app/GeoLite2-City.mmdb');
$record = $reader->city('72.14.201.24');
echo $record->city->names['ru'] ?? '-';

Итог:

  1. плохая точность определения
  2. работает очень быстро и главное линейно (10 тыс. запросов за 5 секунд), пример:
$result = [];
$faker = Factory::create();
$i = 0;
while($i < 10_000) {
    $i++;
    try {
        $record = $reader->city($faker->ipv4());
        $result[] = ($record->city->names['ru'] ?? '-'). PHP_EOL;
    } catch (AddressNotFoundException $exception) {
        continue; // странные IP-адреса, надо исследовать, база плохая или подсунут нереальный IP-адрес
    }
}

Кстати, geonameid это ID с сайта geonames.org представляющий ИД-локации (может быть континентом, страной, городом, регионом и т.д. - детальнее.

Второй способ

Через файл csv:

curl -O -J -L -u YOUR_ACCOUNT_ID:YOUR_LICENSE_KEY "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City-CSV&license_key=YOUR_LICENSE_KEY&suffix=zip"

О нем пока ничего не скажу, кроме того, что наверняка точность определения у него такая же, как у первого бесплатного варианта.

А еще есть:

ip2location

Почти тоже самое, и не смотря на то, что обновляется 1 раз в месяц, немного плюсы:

Cделать на своем сайте страницу определения Гео по IP с использованием Symfony можно так:


use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class ApiController extends AbstractController
{
    #[Route('/api/ip')]
    public function ip(Request $request): Response
    {
        $form = $this->createFormBuilder()
            ->add('ip', TextType::class, [
                'attr' => [
                    'class' => 'form-control'
                ]
            ])
            ->add('submit', SubmitType::class, [
                'attr' => [
                    'class' => 'btn btn-primary'
                ]
            ])
            ->getForm();
        $ip = trim($_POST['form']['ip'] ?? '');
        if (!empty($ip)) {
            $db = new \IP2Location\Database ('/app/IP2LOCATION-LITE-DB5.BIN', \IP2Location\Database::FILE_IO);
            $records = $db->lookup($ip, \IP2Location\Database::ALL);
            return $this->render('common/ip.html.twig', [
                'form' => $form->createView(),
                'records' => empty($records) ? null : $records,
            ]);
        }
        return $this->render('common/ip.html.twig', [
            'form' => $form->createView()
        ]);
    }
}

и шаблон:

{% extends 'base.html.twig' %}

{% block body %}
    <style>
        h3, p, form {
            padding-left: 25px;
        }
    </style>

    <h3>IP2Location Symfony 4 Demo</h3>

    <p>This demo uses <a href="/api/redirect?key=c2405108a3c42aa9105c7b77f4631eb3a52af17d&to=https%3A%2F%2Flite.ip2location.com%2Fdatabase%2Fip-country-region-city-latitude-longitude-zipcode-timezone" target="_blank">IP2Location DB11 LITE BIN Data</a> for geolocation lookup. You may download the BIN Data at <a href="/api/redirect?key=9f173c4979a35b2c634389bc7f5e55eb44f897fe&to=https%3A%2F%2Flite.ip2location.com" target="_blank">https://lite.ip2location.com</a> (Free edition) or <a href="/api/redirect?key=025c7beceb1999c58cafe2c144c25a717a257259&to=https%3A%2F%2Fwww.ip2location.com" target="_blank">https://www.ip2location.com</a> (Commercial edition)</p>

    {% if form is defined %}
        <form id="form" action="" method="POST">
            <table>
                <tr>
                    <td>IP Address: </td>
                    <td style="padding-left:10px">{{ form_widget(form.ip) }}</td>
                    <td style="padding-left:10px"><input class="btn btn-primary" type="submit" value="Submit" name="submit"/></td>
                </tr>
            </table>
        </form>
    {% endif %}
    <br/>

    {% if records is defined %}
        <div>
            <table class="table table-bordered table-striped">
                <tbody>
                <tr>
                    <td>IP Number: </td>
                    <td>{{ records.ipNumber }}</td>
                </tr>
                <tr>
                    <td>IP Version: </td>
                    <td>{{ records.ipVersion }}</td>
                </tr>
                <tr>
                    <td>IP Address: </td>
                    <td>{{ records.ipAddress }}</td>
                </tr>
                <tr>
                    <td>Country Code: </td>
                    <td>{{ records.countryCode }}</td>
                </tr>
                <tr>
                    <td>Country Name: </td>
                    <td>{{ records.countryName }}</td>
                </tr>
                <tr>
                    <td>Region Name: </td>
                    <td>{{ records.regionName }}</td>
                </tr>
                <tr>
                    <td>City Name: </td>
                    <td>{{ records.cityName }}</td>
                </tr>
                <tr>
                    <td>Time Zone: </td>
                    <td>{{ records.timeZone }}</td>
                </tr>
                <tr>
                    <td>ZIP Code: </td>
                    <td>{{ records.zipCode }}</td>
                </tr>
                <tr>
                    <td>Latitude: </td>
                    <td>{{ records.latitude }}</td>
                </tr>
                <tr>
                    <td>Longitude: </td>
                    <td>{{ records.longitude }}</td>
                </tr>
                <tr>
                    <td>Area Code: </td>
                    <td>{{ records.areaCode }}</td>
                </tr>
                <tr>
                    <td>IDD Code: </td>
                    <td>{{ records.iddCode }}</td>
                </tr>
                <tr>
                    <td>Weather Station Code: </td>
                    <td>{{ records.weatherStationCode }}</td>
                </tr>
                <tr>
                    <td>Weather Station Name: </td>
                    <td>{{ records.weatherStationName }}</td>
                </tr>
                <tr>
                    <td>MCC: </td>
                    <td>{{ records.mcc }}</td>
                </tr>
                <tr>
                    <td>MNC: </td>
                    <td>{{ records.mnc }}</td>
                </tr>
                <tr>
                    <td>Mobile Carrier: </td>
                    <td>{{ records.mobileCarrierName }}</td>
                </tr>
                <tr>
                    <td>Usage Type: </td>
                    <td>{{ records.usageType }}</td>
                </tr>
                <tr>
                    <td>Elevation: </td>
                    <td>{{ records.elevation }}</td>
                </tr>
                <tr>
                    <td>Net Speed: </td>
                    <td>{{ records.netSpeed }}</td>
                </tr>
                <tr>
                    <td>Domain Name: </td>
                    <td>{{ records.domainName }}</td>
                </tr>
                <tr>
                    <td>ISP Name: </td>
                    <td>{{ records.isp }}</td>
                </tr>
                </tbody>
            </table>
        </div>
    {% endif %}
{% endblock %}

Определение по координатам

Как мы видим, видимо из-за того, что координаты городов меняются редко, определение по IP и координатам разделяют.

geonames

Сервис download.geonames.org предоставляет нам список городов каждой страны с их координатами, в котором:

  1. geonameid — уникальный идентификатор в базе данных geonames.
  2. name — название географической точки (utf8) varchar(200).
  3. asciiname — название географической точки в виде простых символов ASCII, varchar(200)
  4. alternatenames — альтернативные имена, разделенные запятыми, имена ascii автоматически транслитерируются, атрибут удобства из таблицы альтернативных имен, varchar(10000)
  5. latitude — широта в десятичных градусах (wgs84)
  6. longitude — долгота в десятичных градусах (wgs84)
  7. feature class — класс объектов (тип объекта - например, P для населенных пунктов), char(1)
  8. feature code — код функции varchar(10) (например PPL и PPLS это "a city, town, village, or other agglomeration of buildings where people live and work")
  9. country code — код страны: ISO-3166 2-буквенный код страны, 2 символа
  10. cc2: альтернативные коды стран, разделенные запятыми, двухбуквенный код страны ISO-3166, 200 символов
  11. admin1 code : fipscode (может быть изменен на iso code), см. исключения ниже, см. файл admin1Codes.txt для отображения имен этого кода; varchar(20)
  12. admin2 code : код второго административного деления, округа в США, см. файл admin2Codes.txt; varchar(80)
  13. admin3 code : код административного деления третьего уровня, varchar(20)
  14. admin4 code : код административного деления четвертого уровня, varchar(20)
  15. population - население: bigint (8-байтовое целое)
  16. elevation - высота: в метрах, целое число
  17. dem: цифровая модель рельефа, srtm3 или gtopo30, средняя высота области 3''x3'' (примерно 90 м x 90 м) или 30''x30'' (примерно 900 м x 900 м) в метрах, целое число. srtm обработано cgiar/ciat.
  18. timezone : идентификатор часового пояса IANA (см. файл timeZone.txt) varchar(40)
  19. modification date - дата изменения: дата последнего изменения в формате гггг-ММ-дд

Все эти поля расписаны внизу страницы https://download.geonames.org/export/dump/ 

Удивительно

База geonames.org показала, удивительную вещь - город может быть представлен не один, а несколько раз.

Город Москва может иметь несколько geonameid в файле RU.txt из архива https://download.geonames.org/export/dump/RU.zip по нескольким причинам, связанным с тем, как база данных GeoNames структурирует и классифицирует географические объекты. Вот основные причины:

1. Разные типы объектов с одинаковым названием

В GeoNames один и тот же топоним (например, "Москва") может быть привязан к разным географическим объектам, каждый из которых имеет свой уникальный geonameid. Например:

  • Административная единица: Москва как субъект федерации (город федерального значения).
  • Населённый пункт: Москва как конкретный город (столица).
  • Исторические или альтернативные объекты: Возможно, есть записи для старых названий или частей города, которые сохраняются в базе.

Пример:

  • geonameid = 524901 — это Москва как столица России (тип объекта PPLC — "столица политической единицы").
  • Другой geonameid может обозначать, например, реку Москва или район, связанный с этим названием.

2. Река Москва

Кроме города, в России есть река Москва (Москва-река), которая протекает через город и имеет своё собственное название. В RU.txt она может быть представлена отдельной записью с уникальным geonameid, так как это объект другого типа (H — гидрографический объект, например, река).

Пример:

  • geonameid = 524925 — река Москва (тип STM — "река").

3. Дублирующиеся названия в разных регионах

В России есть несколько населённых пунктов с названием "Москва", помимо столицы. Например, деревни, посёлки или другие небольшие объекты в разных областях могут называться "Москва" в честь столицы или по историческим причинам. Каждый из них получает свой geonameid.

Пример:

  • Деревня Москва в Тверской области или другом регионе может иметь отдельный geonameid.

4. Поле alternatenames и вариации названий

В RU.txt поле alternatenames содержит список альтернативных названий для одного объекта, но если в базе данных GeoNames разные записи имеют "Москва" в name или alternatenames, это может привести к путанице при поиске. Однако основная причина нескольких geonameid — это всё же разные объекты, а не просто вариации названия одного и того же объекта.

5. Иерархия и уточнения

GeoNames иногда создаёт отдельные записи для разных аспектов одного объекта:

  • Москва как центр города.
  • Москва как административная единица (субъект федерации).
  • Москва как агломерация или историческая территория.

Эти записи могут пересекаться по названию, но различаться по координатам, типу объекта (feature code) или другим параметрам.

Пример записей:

Столица: PPLC — столица, координаты центра Москвы.

524901 Москва Moskva Moscow,Москва́,Мoskva 55.75222 37.61556 P PPLC RU ...

Река: STM — река, немного другие координаты.

524925 Москва Moskva Moskva River,Москва-река 55.75 37.61667 H STM RU ...

Деревня: PPL — населённый пункт, координаты где-то вне столицы.

524930 Москва Moskva Moskva 56.12345 38.98765 P PPL RU ...

Проблема одинаковых имен

Поля feature class и feature code очень занятная вещь, но в своей задаче я не буду брать их в расчет и если база мне будет возвращать Москву, то я просто буду считать, что это столица Москва, а не деревня или река (ведь у меня задача простая, определить место того, кто зашел на сайт и в 99% процентов точность будет достаточной, ведь с деревни Москва на мой сайт скорее всего никто не зайдет, а купаясь в реке точно никто не зайдет).

Проблема неверных данных

Оказалось, что база сервиса geonames имеет недостоверные данных, а выяснил это так - с помощью базы ip2location для IP= получил координаты 57.4977, 39.7458, в базе geonames смотрю близлежащие и вижу:

6643231 Ust’yanovo Ust'yanovo  57.17892 40.36569

проверяю 57.17892 40.36569 в Яндексе и он говорит, что это:

Советская улица, 84, рабочий посёлок Некрасовское, Ярославская область, 152260

ладно, думаю, а вдруг Устьяново это какой-то поселок в в Ярославской области, проверяю в интернете, но нет, такого нет.

Итог по 

База сервиса geonames не подходит по двум причинам:

  1. в теории часто имеет очень неточные данные
  2. избыточна для моего сервиса

Итоговое решение

  1. использую базу ip-адресов с координатами (качаю IP2LOCATION-LITE-DB5.BIN с сервиса ip2location.com
  2. использую координаты всех городов России из интернета (а в моем случае даже не всех), например тут
  3. по IP нахожу координаты, по координатам нахожу город, вуаля.

Всякие полезности: 1 - 2 - 3 - 4


11.09.2021 07:08

Комментарии

Комментариев пока нет.


Copyrights © 2007 - 2026 YaPro.Ru