GeoLite2を利用して、hiveでIPアドレスに国や地域情報を補完する

# はじめに
ユーザーのログに記録されているIPアドレスを用いて、国や地域情報が補完できれば、地域別の分析が可能となる。無料でダウンロードできるMaxMind社のGeoLite2を利用して、hiveにてIPアドレスに国や地域情報を補完するやり方を紹介する。

# GeoLite2ダウンロード
MaxMind社 (https://www.maxmind.com) にて、GeoLite2のバイナリファイルとcsvファイルが用意されている。今回の手順ではcsvファイルをダウンロードし、hiveのテーブルにデータをロードして利用する。

- 今回はこれをダウンロード
    - GeoLite2-City-CSV_20200211.zip
- 利用ファイル
    - GeoLite2-City-Blocks-IPv4.csv ・・・IP - 地域のマッピングデータ
    - GeoLite2-City-Locations-ja.csv ・・・地域コードのマスタデータ

# データ加工
`GeoLite2-City-Blocks-IPv4.csv` は `1.0.64.0/23` のようにCIDR表記となっており、hiveで利用しやすいように from_ip, to_ip にデータ加工する。また、from_ip, to_ipのレンジでselectした場合に少ないデータ量だったとしても処理が重くて動かない。そこで [google](https://cloudplatform.googleblog.com/2014/03/geoip-geolocation-with-google-bigquery.html) で紹介されているやり方を参考に ClassBカラムを追加し、論理パーティションのイメージで、アクセスするデータ量を減らすことで対応する。

```
■例1
1.0.64.0/23
↓
class_b 1.0.0.0, from 1.0.64.0, to 1.0.65.255

■例2
1.48.0.0/15
↓
class_b 1.48.0.0, from 1.48.0.0, to 1.48.255.255
class_b 1.49.0.0, from 1.49.0.0, to 1.49.255.255
```

----
※[geoip2-csv-converter](https://github.com/maxmind/geoip2-csv-converter/releases) ・・・オフィシャルで用意されているscriptはclass_bカラムが対応されていないので、自前で加工scriptを用意する。

<details>
  <summary>★自前のデータ加工script★ (geolite2_city_ipv4_mask_blocks.pl) </summary><div>

```
#!/usr/local/bin/perl

use strict;
use warnings;

my $class_b_mask = 16;
my $class_b_num = 65536;
my $class_b_maskbit = &bits2netmask_int($class_b_mask);

while(<STDIN>) {
  chomp();

  # network,geoname_id,registered_country_geoname_id,represented_country_geoname_id,is_anonymous_proxy,is_satellite_provider,postal_code,latitude,longitude,accuracy_radius
  # ex: 1.0.64.0/23,1862415,1861060,,0,0,730-0000,34.4000,132.4500,1
  # ex: 1.48.0.0/15,1809445,1814991,,0,0,,26.5833,106.7167,50
  my @rec  = split(/,/);
  if ($rec[0] eq "network"){
    next; # skip header
  }

  my @network = split(/\//, $rec[0]); # network
  my $ip      = &str2num($network[0]);
  my $mask    = $network[1];
  my $maskbit = &bits2netmask_int($mask);

  my $from_ip = &get_network($ip, $maskbit);
  my $to_ip   = &get_broadcast($ip, $maskbit);
  my $diff = $to_ip - $from_ip + 1;

  if ($diff > $class_b_num) {
    # 1.48.0.0/15
    # -->
    # class_b 1.48.0.0, from 1.48.0.0, to 1.48.255.255
    # class_b 1.49.0.0, from 1.49.0.0, to 1.49.255.255
    #
    # ex:
    # in : 1.48.0.0/15,1809445,1814991,,0,0,,26.5833,106.7167,50
    # out: 19922944,19922944,19988479,1.48.0.0/15,1809445,1814991,,0,0,,26.5833,106.7167,50
    # out: 19988480,19988480,20054015,1.48.0.0/15,1809445,1814991,,0,0,,26.5833,106.7167,50

    my $tmp_from_ip = $from_ip;
    my $tmp_to_ip = $to_ip;
    my $tmp_diff = $diff;
    my $tmp_class_b;
    while ($tmp_diff > 0) {
      $tmp_from_ip = &get_network($tmp_from_ip, $class_b_maskbit);
      $tmp_to_ip = &get_broadcast($tmp_from_ip, $class_b_maskbit);
      $tmp_diff = $tmp_diff - 65536;
      $tmp_class_b = &get_network($tmp_from_ip, $class_b_maskbit);
      print "$tmp_class_b,$tmp_from_ip,$tmp_to_ip," . join(",", @rec) ."\n";
      $tmp_from_ip = $tmp_to_ip + 1;
    }
  } else {
    # 1.0.64.0/23
    # -->
    # class_b 1.0.0.0, from 1.0.64.0, to 1.0.65.255
    #
    # ex:
    # in : 1.0.64.0/23,1862415,1861060,,0,0,730-0000,34.4000,132.4500,1
    # out: 16777216,16793600,16794111,1.0.64.0/23,1862415,1861060,,0,0,730-0000,34.4000,132.4500,1

    my $class_b = &get_network($from_ip, $class_b_maskbit);
    print "$class_b,$from_ip,$to_ip," . join(",", @rec) ."\n";
  }
}

sub str2num($) {
    my ($str) = @_ ;
    my $num = 0;

    if ($str !~ /([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/) {
      # die "Cannot parse IP addr: $str ";
    }
    $num = $1;
    $num *= 0x0100;
    $num += $2;
    $num *= 0x0100;
    $num += $3;
    $num *= 0x0100;
    $num += $4;
    return $num;
}

sub num2str($) {
    my ($num) = @_ ;
    return sprintf("%d.%d.%d.%d",
                 ($num & 0xff000000) >> 24,
                 ($num & 0x00ff0000) >> 16,
                 ($num & 0x0000ff00) >> 8,
                 ($num & 0x000000ff));
}

sub bits2netmask_int($) {
    my ($bit) = @_ ;
    my $nm;
    $nm = ~((1 << 32 - $bit) - 1);
    return $nm;
}

sub get_network($$) {
    my ($ipa, $netmask) = @_;
    return ($ipa & $netmask);
}

sub get_broadcast($$) {
    my ($network, $netmask) = @_;
    return ($network | ~$netmask);
}

exit;
```
</div></details>

----

```
■加工実施コマンド
$ cat GeoLite2-City-CSV_20200211/GeoLite2-City-Blocks-IPv4.csv | perl geolite2_city_ipv4_mask_blocks.pl > GeoLite2-City-Blocks-IPv4-with-ranges.csv
$ gzip GeoLite2-City-Blocks-IPv4-with-ranges.csv
```

# hiveテーブルへのロード
以下のファイルをhadoopのGWサーバへアップする。

```
■IP - 地域のマッピングデータ
GeoLite2-City-Blocks-IPv4-with-ranges.csv.gz
※加工したclass_bカラム付きのcsvファイル

■地域コードのマスタデータ
$ gzip GeoLite2-City-Locations-ja.csv
GeoLite2-City-Locations-ja.csv.gz
```

テーブル作成＋データロード

```
■IP - 地域のマッピングデータ
create table if not exists XXXXX.geolite2_city_blocks_ipv4(
 class_b                    bigint
 , from_ip                  bigint
 , to_ip                    bigint
 , network                  string
 , geoname_id               string
 , registered_country_geoname_id    string
 , represented_country_geoname_id   string
 , is_anonymous_proxy       string
 , is_satellite_provider    string
 , postal_code              string
 , latitude                 string
 , longitude                string
 , accuracy_radius          string
)
row format delimited fields terminated by ',' lines terminated by '\n'
stored as textfile
location '/user/XXXXX/geolite2_city_blocks_ipv4'
;
load data local inpath 'GeoLite2-City-Blocks-IPv4-with-ranges.csv.gz' overwrite into table XXXXX.geolite2_city_blocks_ipv4;

■地域コードのマスタデータ
create table if not exists XXXXX.geolite2_city_locations_ja(
 geoname_id                 string
 , locale_code              string
 , continent_code           string
 , continent_name           string
 , country_iso_code         string
 , country_name             string
 , subdivision_1_iso_code   string
 , subdivision_1_name       string
 , subdivision_2_iso_code   string
 , subdivision_2_name       string
 , city_name                string
 , metro_code               string
 , time_zone                string
 , is_in_european_union     string
)
row format delimited fields terminated by ',' lines terminated by '\n'
stored as textfile
location '/user/XXXXX/geolite2_city_locations_ja'
;
load data local inpath 'GeoLite2-City-Locations-ja.csv.gz' overwrite into table XXXXX.geolite2_city_locations_ja;
```

# hiveでIPアドレスに国や地域情報を補完する
```
with raw as (
 select '1.33.38.96' as ip_str
 union all
 select '27.94.192.173' as ip_str
)
, x1 as (
 select
  ip_str
  ,cast(
   cast(split(ip_str, '\\.')[0] as int) * pow(2,24)
   + cast(split(ip_str, '\\.')[1] as int) * pow(2,16)
   + cast(split(ip_str, '\\.')[2] as int) * pow(2,8)
   + cast(split(ip_str, '\\.')[3] as int) * pow(2,0) as int) as ip_integer
 from raw
)
, x2 as (
 select
  ip_str
  ,ip_integer
  ,(ip_integer & ~(cast(((1 * power(2,32 - 16)) - 1)as int))) as class_b
 from x1
)
, x3 as (
 select
  r.ip_str
  ,g.geoname_id
 from x2 r, XXXXX.geolite2_city_blocks_ipv4 g
 where r.class_b = g.class_b
 and from_ip <= ip_integer and to_ip >= ip_integer
)
 select
  r.ip_str
  ,m.continent_name
  ,m.country_name
  ,m.city_name
  ,m.time_zone
 from x3 r, XXXXX.geolite2_city_locations_ja m
 where r.geoname_id = m.geoname_id
;

OK
r.ip_str        m.continent_name        m.country_name  m.city_name     m.time_zone
1.33.38.96      "アジア"        "日本"  "横浜市"        Asia/Tokyo
27.94.192.173   "アジア"        "日本"  "墨田区"        Asia/Tokyo
Time taken: 47.897 seconds, Fetched: 2 row(s)
hive>
```
★補足
該当のIPアドレスから /16のClassBのネットワークアドレスを計算して利用している。

★問題点
範囲を条件にした外部結合がhiveではサポートされていないので、geolite2_city_blocks_ipv4に存在しているIPアドレスのみしかselectできない。この問題の対応版を後述する。

# hiveでIPアドレスに国や地域情報を補完する（外部結合対応版）
```
with raw as (
 select '1.33.38.96' as ip_str
 union all
 select '27.94.192.173' as ip_str
 union all
 select '224.1.255.1' as ip_str --->データ無しのIP
)
, x1 as (
 select
  ip_str
  ,cast(
   cast(split(ip_str, '\\.')[0] as int) * pow(2,24)
   + cast(split(ip_str, '\\.')[1] as int) * pow(2,16)
   + cast(split(ip_str, '\\.')[2] as int) * pow(2,8)
   + cast(split(ip_str, '\\.')[3] as int) * pow(2,0) as int) as ip_integer
 from raw
)
, x2 as (
 select
  ip_str
  ,ip_integer
  ,(ip_integer & ~(cast(((1 * power(2,32 - 16)) - 1)as int))) as class_b
 from x1
)
, geo as (
 select
  distinct
  r.ip_str
  ,g.geoname_id
 from x2 r, XXXXX.geolite2_city_blocks_ipv4 g
 where r.class_b = g.class_b
 and from_ip <= ip_integer and to_ip >= ip_integer
)
, x3 as (
 select
  r.ip_str
  ,g.geoname_id
 from x2 r left outer join geo g
  on r.ip_str = g.ip_str
)
 select
  r.ip_str
  ,m.continent_name
  ,m.country_name
  ,m.city_name
  ,m.time_zone
 from x3 r left outer join XXXXX.geolite2_city_locations_ja m
  on r.geoname_id = m.geoname_id
;

OK
r.ip_str        m.continent_name        m.country_name  m.city_name     m.time_zone
224.1.255.1     NULL    NULL    NULL    NULL
1.33.38.96      "アジア"        "日本"  "横浜市"        Asia/Tokyo
27.94.192.173   "アジア"        "日本"  "墨田区"        Asia/Tokyo
Time taken: 33.036 seconds, Fetched: 3 row(s)
hive>
```
★補足
1IPずつの、IPアドレス＋地域補完した、geoの中間クエリを作成し、それと外部結合することで対応している。