Вступление
Долгое время я пользовался библиотекой 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