Вступление
Долгое время я пользовался библиотекой SxGeo от zapimir. И до недавнего времени меня всё устраивало. Устраивало до тех пор, пока не было необходимости добавлять в БД свои данные.
Не найдя в интернете упаковщика данных от SxGeo и не найдя в себе силы требовать нужный мне функционал от разработчика, было принято решение писать свой костыль. Хотя на это решение повлиял и ещё 2 недостатка используемой библиотеки:
- ограничение по количеству справочников;
- невозможность узнать интервал адресов, в который входит искомый адрес;
- отсутствие пакета в packagist.
Собственно, делюсь с вами своей разработкой.
Отличия между прототипом и моим решением:
- IPTool — это всего лишь инструмент для создания базы данных и поиска в ней, в то время, как проект SxGeo — проект, предоставляющий не только инструментарий, но и сами базы данных;
- База данных IPTool занимает больше места (т.к. первый адрес диапазона хранится полностью и занимает 4 байта, в то время, как в SxGeo только 3 байта);
- IPTool имеет только один режим — чтение данных с диска (Режим подгрузки базы в память — в планах);
- Помимо данных, IPTool возвращает диапазон IP адресов, в который входит искомый адрес;
- IPTool предусматривает методы получения данных из справочников (всех или по порядковому номеру);
- В базе данных IPTool предусмотрена возможность лицензирования самой базы данных;
- IPTool легко устанавливается с помощью Composer;
Использование
Инициализация IP Tool
/* Путь к базе данных - /path/to/iptool.database */ $iptool = new \Ddrv\Iptool\Iptool('/path/to/iptool.database');
Получение информации о базе данных
print_r($iptool->about());
Array ( [created] => 1507199627 [author] => Anonymous Author [license] => MIT [networks] => Array ( [count] => 276148 [data] => Array ( [country] => Array ( [0] => code [1] => name ) ) ) )
Поиск информации об IP адресе
print_r($iptool->find('81.32.17.89'));
Array ( [network] => Array ( [0] => 81.32.0.0 [1] => 81.48.0.0 ) [data] => Array ( [country] => Array ( [code] => es [name] => Spain ) ) )
Получить все элементы справочника
print_r($iptool->getRegister('country'));
Array ( [1] => Array ( [code] => cn [name] => China ) [2] => Array ( [code] => es [name] => Spain ) ... [N] => Array ( [code] => jp [name] => Japan ) )
Получение элемента справочника по его порядковому номеру
print_r($iptool->getRegister('country',2));
Array ( [code] => cn [name] => China ) )
Процесс создания БД более трудоёмкий, но он описан с документации, которая доступна в репозитории и в wiki GitHub'а на русском и ломаном английском.
UPD1. Сравнение скорости работы IPTool и SxGeo
Для большей достоверности результатов, я создал БД для IPTool на основе данных SxGeo
Подготовка к сравнительному тесту
$ cd /path/to/test/dir $ mkdir csv $ mkdir csv/sxgeo $ mkdir t
необходимо скопировать файлы SxGeo.php и SxGeoCity.dat в текущую директорию (/path/to/test/dir)
Установка IPTool
$ composer require ddrv/iptool:~1.0
Импорт БД SxGeo в csv файлы
<?php /* Импорт БД SxGeo в csv файлы */ include_once __DIR__.DIRECTORY_SEPARATOR.'SxGeo.php'; class ExtSxGeo extends SxGeo { public function parseBase() { $s=0; $firstIp = '0.0.0.0'; $seek = 0; $data = $this->parseCity($seek,1); $sxNet = fopen(__DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxNet.csv','w'); $sxCnt = fopen(__DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxCnt.csv','w'); $sxRgn = fopen(__DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxRgn.csv','w'); $sxCts = fopen(__DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxCts.csv','w'); $ids = [ 'cnt' => [], 'rgn' => [], 'cts' => [], ]; for ($octet=1;$octet<=223;$octet++) { $bip = pack('C',$octet); $min = $this->b_idx_arr[$octet-1]; $max = $this->b_idx_arr[$octet]; for ($b=$min; $b<=$max;$b++) { fseek($this->fh, $this->db_begin + $b * $this->block_len); $block = fread($this->fh, $this->block_len); $i = unpack('C4',$bip.substr($block,0,3)); $ip = implode('.',$i); $lastIp = long2ip(ip2long($ip)-1); $csvNet = [ $firstIp, $lastIp, $data['city']['id'], $data['region']['id'], $data['country']['id'], ]; fputcsv($sxNet,$csvNet); if (!isset($ids['cts'][$data['city']['id']])) { $ids['cts'][$data['city']['id']] = true; $csvCts = [ $data['city']['id'], $data['city']['lat'], $data['city']['lon'], $data['city']['name_ru'], $data['city']['name_en'], ]; fputcsv($sxCts,$csvCts); } if (!isset($ids['rgn'][$data['region']['id']])) { $ids['rgn'][$data['region']['id']] = true; $csvRgn = [ $data['region']['id'], $data['region']['iso'], $data['region']['name_ru'], $data['region']['name_en'], ]; fputcsv($sxRgn,$csvRgn); } if (!isset($ids['cnt'][$data['country']['id']])) { $ids['cnt'][$data['country']['id']] = true; $csvCnt = [ $data['country']['id'], $data['country']['iso'], $data['country']['lat'], $data['country']['lon'], $data['country']['name_ru'], $data['country']['name_en'], ]; fputcsv($sxCnt,$csvCnt); } $firstIp = $ip; $seek = hexdec(bin2hex(substr($block, $this->block_len - $this->id_len, $this->id_len))); $data = $this->parseCity($seek,1); } } $lastIp = '255.255.255.255'; $csvNet = [ $firstIp, $lastIp, $data['city']['id'], $data['region']['id'], $data['country']['id'], ]; fputcsv($sxNet,$csvNet); if (!isset($ids['cts'][$data['city']['id']])) { $ids['cts'][$data['city']['id']] = true; $csvCts = [ $data['city']['id'], $data['city']['lat'], $data['city']['lon'], $data['city']['name_ru'], $data['city']['name_en'], ]; fputcsv($sxCts,$csvCts); } if (!isset($ids['rgn'][$data['region']['id']])) { $ids['rgn'][$data['region']['id']] = true; $csvRgn = [ $data['region']['id'], $data['region']['iso'], $data['region']['name_ru'], $data['region']['name_en'], ]; fputcsv($sxRgn,$csvRgn); } if (!isset($ids['cnt'][$data['country']['id']])) { $ids['cnt'][$data['country']['id']] = true; $csvCnt = [ $data['country']['id'], $data['country']['iso'], $data['country']['lat'], $data['country']['lon'], $data['country']['name_ru'], $data['country']['name_en'], ]; fputcsv($sxCnt,$csvCnt); } fclose($sxNet); fclose($sxCnt); fclose($sxRgn); fclose($sxCts); } } $sxgeo = new ExtSxGeo( __DIR__.DIRECTORY_SEPARATOR.'SxGeoCity.dat',2); $sxgeo->parseBase();
Запускаем скрипт и ждём.
$ php import.php
Создание БД IPTool из полученных csv файлов
<?php /* Создание БД IPTool из полученных csv файлов */ require_once(__DIR__.DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php'); /* Используем директорию для хранения временных файлов. У скрипта должны быть права на запись в эту директорию. */ $tmpDir = __DIR__.'/t'; /* Инициализируем класс Converter. */ $converter = new \Ddrv\Iptool\Converter($tmpDir); /* Указываем путь для сохранения БД. Скрипт должен иметь права на запись этого файла. */ $dbFile = __DIR__.DIRECTORY_SEPARATOR.'iptool.sxgeo.city.dat'; /* Запоминаем в переменные пути к нужным CSV файлам. */ $sxNet = __DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxNet.csv'; $sxCnt = __DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxCnt.csv'; $sxRgn = __DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxRgn.csv'; $sxCts = __DIR__.DIRECTORY_SEPARATOR.'csv'.DIRECTORY_SEPARATOR.'sxgeo'.DIRECTORY_SEPARATOR.'sxCts.csv'; /* Устанавливаем инфорацию об авторе. */ $converter->setAuthor('Ivan Dudarev'); /* Указываем лицензию. */ $converter->setLicense('MIT'); /* Добавляем исходники в формате CSV. */ $converter->addCSV('sxNet',$sxNet); $converter->addCSV('sxCnt',$sxCnt); $converter->addCSV('sxRgn',$sxRgn); $converter->addCSV('sxCts',$sxCts); /* Описываем справочник Country. */ $country = array( 'id' => array( 'type' => 'int', 'column' => 0, ), 'iso' => array( 'type' => 'string', 'column' => 1, 'transform' => 'low', ), 'lat' => array( 'type' => 'double', 'column' => 2, ), 'lon' => array( 'type' => 'double', 'column' => 3, ), 'nameRu' => array( 'type' => 'string', 'column' => 4, ), 'nameEn' => array( 'type' => 'string', 'column' => 5, ), ); $converter->addRegister('country','sxCnt',0, $country); /* Описываем справочник Region. */ $region = array( 'id' => array( 'type' => 'int', 'column' => 0, ), 'iso' => array( 'type' => 'string', 'column' => 1, 'transform' => 'low', ), 'nameRu' => array( 'type' => 'string', 'column' => 2, ), 'nameEn' => array( 'type' => 'string', 'column' => 3, ), ); $converter->addRegister('region','sxRgn',0, $region); /* Описываем справочник City. */ $city = array( 'id' => array( 'type' => 'int', 'column' => 0, ), 'lat' => array( 'type' => 'double', 'column' => 1, ), 'lon' => array( 'type' => 'double', 'column' => 2, ), 'nameRu' => array( 'type' => 'string', 'column' => 3, ), 'nameEn' => array( 'type' => 'string', 'column' => 4, ), ); $converter->addRegister('city','sxCts',0, $city); /* Описываем диапазоны. */ $data = array( 'city' => 2, 'region' => 3, 'country' => 4, ); $converter->addNetworks('sxNet', 'ip', 0, 1, $data); $errors = $converter->getErrors(); if (!$errors) { $converter->create($dbFile); } else { print_r($errors); }
Запускаем скрипт и ждём.
$ php convert.php
Сравнение величины БД
$ ls -l *.dat ... -rw-r--r-- 1 www www 13435116 Jun 30 15:46 SxGeoCity.dat -rw-r--r-- 1 www www 33190825 Oct 12 06:40 iptool.sxgeo.city.dat ...
Объём базы IPTool больше в 3 раза (что не есть плюс)
<?php require_once(__DIR__.DIRECTORY_SEPARATOR.'Iptool.php'); require_once(__DIR__.DIRECTORY_SEPARATOR.'SxGeo.php'); $dbFile = __DIR__.DIRECTORY_SEPARATOR.'iptool.sxgeo.city.dat'; $iptool = new \Ddrv\Iptool\Iptool($dbFile); $sxgeo = new SxGeo( __DIR__.DIRECTORY_SEPARATOR.'SxGeoCity.dat',2); /* Готовим данные для теста */ $ips = []; for ($i=0;$i<100;$i++) { $ipa = []; for($octet = 0;$octet<4;$octet++) { $ipa[] = rand(0,255); } $ip = implode('.',$ipa); $ips[] = $ip; } /* IPTool */ $res = []; $t1 = microtime(true); foreach ($ips as $ip) { $res[] = $iptool->find($ip); } $t2 = microtime(true); echo 'IP Tool : '.($t2-$t1).PHP_EOL; /* SxGeo */ $res = []; $t1 = microtime(true); foreach ($ips as $ip) { $res[] = $sxgeo->getCityFull($ip); } $t2 = microtime(true); echo 'SxGeo : '.($t2-$t1).PHP_EOL;
Сравненительный тест скорости iptool-1.0.6 и SxGeo-2.2.3
$ php compare.php
Результат трёх тестов по 100 адресов
IP Tool : 0.026905059814453 SxGeo : 0.031632900238037 IP Tool : 0.025413036346436 SxGeo : 0.023004055023193 IP Tool : 0.016932010650635 SxGeo : 0.022341012954712
Результат трёх тестов по 1 адресу
IP Tool : 0.0013048648834229 SxGeo : 0.00016021728515625 IP Tool : 0.00047779083251953 SxGeo : 0.00011301040649414 IP Tool : 0.00046205520629883 SxGeo : 0.00035595893859863
UPD2. В версии 1.0.7 алгоритм поиска переведён на бинарный поиск
Сравненительный тест скорости iptool-1.0.7 и SxGeo-2.2.3
$ php compare.php
Результат трёх тестов по 100 адресов
IP Tool : 0.012892961502075 SxGeo : 0.033740043640137 IP Tool : 0.0073931217193604 SxGeo : 0.032436847686768 IP Tool : 0.0043089389801025 SxGeo : 0.028012990951538
Результат трёх тестов по 1 адресу
IP Tool : 0.0011000633239746 SxGeo : 0.0009000301361084 IP Tool : 0.00040006637573242 SxGeo : 0.00079989433288574 IP Tool : 0.00030016899108887 SxGeo : 0.00020003318786621
Вывод
Нужно работать над размером БД;
- реализовать связь между справочниками, это заметно сократит размер базы диапазонов;
- склеивать интервалы с одинаковыми данными (хотя в данной БД таковых нет, они взяты из SxGeo как есть);
- хранить начальные адреса диапазонов в виде 3х байт, как в SxGeo.
UPD 3
На некоторых проектах конвертер не нужен (база генерится в одном проекте и дублируется в дру��ие), что добавляло лишнюю зависимость в проект (pdo_sqlite). В связи с этим было принято решение разделить библиотек на 2 проекта. Ну и под шумок сменилось пространство имён.
Теперь проект живёт здесь:
Создание базы GitHub Packagist
Поиск по базе GitHub Packagist
