CIDRブロックとccTLDの関係をデータベースにしてみる

状況説明

ひょんなことから作るはめになったのが、CIDR ブロックと ccTLD の対応表。

一個の CIDR ブロックがどの ccTLD に割り当てられたものかを知るだけなら whois でいいのだけど、ある ccTLD にいくつの IP アドレスが割り当てられ ているかを調べる必要があって、割り当て済の CIDR ブロックを列挙して ccTLD との対応を書くような「対応表」になってる必要があった。 どうせなら時間軸上の動きもわかればいいよねということで時刻情報も入れ (ようとしてちょっと泣きそうになっ)た。

データソース

CIDR ブロックを割り当てる大本は IANA で、ここから5つの RIR へ割り振 られる。5 RIR は AFRINIC, APNIC, ARIN, LACNIC, RIPE で、大雑把に 言うとアフリカ・北米・中南米とカリブ海諸国・欧州と中東の諸国を担当 [1] している。 RIR ができるより前の草創期に、直接に IBM とか Hewlett-Packerd とかに割 り当てられてるのもある。

続いて各RIRから各国のNIRに更に割り振られるわけだけれど、これは* TEAM CYMRU にあるように各 RIR が情報を出している。 再掲しておくと下のリンクの先に当日分の割り振り情報があって、一日一回更 新される(と思う)。 delegated-*-latest なファイルの位置から見て ./archive/ に過去データも ある(はず)。

ここの書式は(多分)共通で、こんな感じ。 例えば ここ に書式の説明 がある。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
2.3|arin|1467781451436|125268|19700101|20160706|-0400
arin|*|asn|*|25635|summary
arin|*|ipv4|*|57735|summary
arin|*|ipv6|*|41898|summary
arin|US|asn|1|1|20010920|assigned|abc43e5fcdb68e284085cf6f1b34834e
arin|US|asn|2|1|19910110|assigned|c3a16289a7ed6fb75fec2e256e5b5101
arin|US|asn|3|1||assigned|d98c567cda2db06e693f2b574eafe848
   <snip>
arin|US|ipv4|3.0.0.0|16777216|19880223|assigned|df4bb610f5374373d9edd182c1901c22
arin|US|ipv4|4.0.0.0|16777216|19921201|allocated|abc43e5fcdb68e284085cf6f1b34834e
arin|US|ipv4|6.0.0.0|16777216|19940201|allocated|c096bf755fee3dfb7b9046461595ebd0
   <snip>

目下の焦点は IPv4 だけれど、ASN も IPv6 もここに情報がある。

IPv4 の情報としては、9行目を「ARIN から US(の NIR)に 3.0.0.0 から始 まる 16777216 個の IPv4 アドレスを assign した」と読めば良いのだけれど、 16,777,216 個とは 2^{24} 個なので 3.0.0.0/8 ってことになる。[2]

assign と allocate の意味の違いは、多分、エンドユーザへの配布を割り当 て (assign) と呼び、そこへ至る中間段階の配布を割り振り (allocate) と呼 んでいるようだ。 JPNIC の解説 を参照。

で、まあ、これを処理していけば目的のデータベースができるというわけだ。 ところが、世の中には偉い人も居れば居るもので、nami.jp 殿が 5 RIR 分の データを毎日処理して公開してくれている。

以下では、この処理済データを有りがたく使わせていただいているが、何か問 題があればそれはすべて の責任なの で、くれぐれも nami.jp 殿にご迷惑のかからぬようにお願い致します。

では、その処理済データはどうなっているかというと、下に示すように各行に ccTLD と CIDRブロック(bitlength 表記)を TAB で区切って置いている。 約 260 の ccTLD が複数行を持っていて合計約 10 万行でおよそ2MBになるファ イル cidr.txt が一日一回作成されている。

1
2
3
4
AD    85.94.160.0/19
AD    91.187.64.0/19
AD    109.111.96.0/19
 <snip>

DBMS準備

別に MariaDB でもなんでも似たようなことはできると思うが、昔馴染みでも あり、また、 範囲型ネットワークアドレス型 が使えるということもあって、 PostgreSQL を使っている。 ただ、PostgreSQLに同梱の ネットワークアドレス型 では 検索時に index が利かない らしいので、ip4r をインストールしてこちらを使っている。 まあ、まだ動作確認なので index すら張ってなかったりするのだけれども。

というわけで、例によって ports/pkg でいう以下のパッケージをインストー ルした。

$ pkg info | grep -i postgre
ip4r-2.0                  IP address and IP range index types for PostgreSQL
postgresql95-client-9.5.2 PostgreSQL database (client)
postgresql95-server-9.5.2 The most advanced open-source database available anywhere

インストール後には PostgreSQL サーバの初期作業で

# echo "postgresql_enable=\"YES\"" >> /etc/rc.conf.local

# service postgresql-server initdb

# service postgresql-server start

とかは適当に。 アクセス制御やチューニングも必要な場合にはやっておくこと。

サーバプロセスが起動したら

$ psql postgres pgsql
psql (9.5.2)
Type "help" for help.

postgres=# create database mydb

で接続して、すかさずデータベースを作成しておく。

初回データ投入

これからデータを蓄積するテーブルはこんな感じ。

CREATE TABLE namicidrbycc (
  iprange IP4R      NOT NULL,
  cc2     TEXT      NOT NULL,
  trange  TSTZRANGE NOT NULL,
  CONSTRAINT EXCLUDE USING GIST (iprange WITH &&, trange WITH &&)
);
CREATE INDEX ON namicidrbycc(iprange);
CREATE INDEX ON namicidrbycc(cc);
CREATE INDEX ON namicidrbycc(trange);

iprange には CIDR ブロックを入れ、それを割り振られている ccTLD を cc2 に入れる。 trange は時間の範囲を保持できる型なのでいつからいつまでの情報であるか を入れておく。 iprange が重複し、かつ、trange も重複するようなレコードは許さない、と いう制約条件を入れてあるので、ある時点のデータについて考えると、ある CIDR ブロックは高々1回しか出現しない。 そして、とりあえず index を張ってみた。

このテーブルに処理済データを入れていくわけだが、多量のデータは COPY コ マンドで放り込むほうが速いので、そのため(だけ)のテーブルも作る。

CREATE TABLE namicidrbycc_tmp (
cc2 TEXT NOT NULL, iprange IP4R NOT NULL

); COPY namicidrbycc_tmp FROM ‘/path/to/cidr.txt’; INSERT INTO namicidrbycc (iprange, cc2, trange) SELECT iprange, cc2, tstzrange(${DATE}, null) FROM namicidrbycc_tmp; SELECT * FROM namicidrbycc WHERE trange @> ${DATE}; – iprange | cc2 | trange – —————–+—–+—————————– – 85.94.160.0/19 | AD | [“2016-05-01 00:00:00+00”,) – 91.187.64.0/19 | AD | [“2016-05-01 00:00:00+00”,) – 109.111.96.0/19 | AD | [“2016-05-01 00:00:00+00”,) – <snip>

${DATE} のところは適切な日付や timestamp with time zone に代えてやるべ し。‘20160501’ とか ‘2016-05-01 12:00:00’::timestamp with time zone と か、tstzrange の中は current_date でもいいし、SELECT の方は now() でも。

初回のデータ投入はこれでいいんだけれども、2回目以降は例の制約が効いて くるので素直には投入できない。

更新データ投入

すでに昨日までのデータが入っている namicidrbycc に今日(${DATE})の分の 更新データを入れる時、昨日の cidr.txt と今日の cidr.txt の diff を取っ てそれを cidrbycc-diff2sql.py の標準入力に流し込む。 通常、このレベルの割り振りならば劇的な差はないはずで、従ってデータベー スへの追加挿入削除変更が少ないはず。最初は SQL で解決しようとしてたん だけどちょっと手に負えなかったというのは秘密。

export DATE=20160501
diff data/cidr.txt.`date -v-1d -j ${DATE}0000 +%Y%m%d` data/cidr.txt.${DATE} \
| python2 bin/cidrbycc-diff2sql.py --date ${DATE}
  • cidrbycc-diff2sql.py は、–date オプションで日付 (ex. ‘20160501’) を 取って過去データの投入に対応。省略時はスクリプト実行日の日付。
  • 前段の diff の出力を入力として、既存データ側(curdb)か新規更新データ 側(newdb)かを振り分け、{cur|new}db[‘<cidr>’] = <ccTLD> な辞書を作る。
  • newdb にある cidr (煩雑なのでnewdb/cidrのように書く)が curdb にもあ る時、ccTLD だけが変わっているということなので、curdb 側の trange を 今日まで(厳密には「今日未満」)とし、新たに今日からのデータを挿入す る。
  • そうでない時は、newdb/cidr が curdb 側の各 cidr と overlap するかど うかを調べる。
  • curdb/cidr が newdb/cidr を包含する [3] 場合、curdb/cidr を 分割して newdb/cidr 部分については newdb/cctld を入れて trange も今 日からにする。その他は curdb/cidr についていた cctld を入れて trange はいじらず。
  • curdb/cidr が newdb/cidr に包含される場合は、curdb 側の trange を今 日までとして、newdb/cidr を挿入する。
  • diff 中に curdb/cidr があるが、これと合同又は包含関係にある cidr が newdb 側にない時は、curdb/cidr の trange を今日までとする。
  • スクリプトとしては、上記のような変更を行うための SQL を出力するので、 psql へ繋いで実行すれば更新データ投入ができる。
cidrbycc-diff2sql.py
#!/usr/local/bin/python2

import argparse
import datetime
from sys import stdin
import re
import ipaddress

newdb = {}
curdb = {}
replacecc2 = {}
newonly = {}
newonlyminus = {}
splitit = {}
mergeit = {}

p = argparse.ArgumentParser(description='process date with default value')
p.add_argument('--date', '-d', help='assign date as YYYYMMDD.', default=datetime.date.today())
args = p.parse_args()

for line in stdin.readlines():
  l = re.split('\s+', line.rstrip())
  if l[0] == '>':
    newdb[ipaddress.IPv4Network(l[2].decode('UTF-8'))] = l[1].decode('UTF-8')
  elif l[0] == '<':
    curdb[ipaddress.IPv4Network(l[2].decode('UTF-8'))] = l[1].decode('UTF-8')

while newdb:
  rk, rv = newdb.popitem()
  if len(curdb) == 0:
    newonly[rk] = rv

    if rk.overlaps(ipaddress.IPv4Network(u'148.59.0.0/16')):
      print 'curdb is empty', rk, rv

  else:				# curdb is not empty.
    if rk in curdb.keys():
      if rv != curdb[rk]:	# same iprange, replaced cc2.
        replacecc2[rk] = {'oldcc2':curdb[rk], 'newcc2':rv}
        curdb.pop(rk)
      else:
        raise Exception('impossible input', 'since diff-ed list expected')
    else:			# rk does not exist in curdb.
      for lk, lv in curdb.items():
        if rk.overlaps(lk):	# yes, overlaps.
          cn = rk.compare_networks(lk)
          if cn > 0:		# curdb side contains newdb side.

            if lk in splitit.keys():	# lk exists in splitit
              splitit[lk]['leaf'][rk] = rv
            else:			# new lk in splitit
              splitit[lk] = {'cc2':lv, 'leaf': {rk:rv}}

            for rk2, rv2 in newdb.items():	# search other leaf in newdb
              if lk.overlaps(rk2):
                splitit[lk]['leaf'][rk2] = rv2
                newdb.pop(rk2)
            curdb.pop(lk)
          elif cn < 0:		# curdb side is contained by newdb side.
            if rk in mergeit.keys():
              mergeit[rk]['leaf'][lk] = lv
            else:
              mergeit[rk] = {'cc2':rv, 'leaf': {lk:lv}}
            curdb.pop(lk)
          newonlyminus[rk] = rv
        else:
          newonly[rk] = rv

for k in newonlyminus.keys():
  newonly.pop(k, None)

for k, v in replacecc2.items():
  print 'update namicidrbycc set trange = tstzrange(lower(trange), \'{date}\') where iprange = \'{iprange}\' and cc2 = \'{oldcc2}\' and trange @> \'{date}\'::timestamp with time zone;\ninsert into namicidrbycc (iprange, cc2, trange) values (\'{iprange}\', \'{newcc2}\', tstzrange(\'{date}\', null));'.format(date=args.date, iprange=k, oldcc2=v['oldcc2'], newcc2=v['newcc2'])

for k, v in newonly.items():
  print 'insert into namicidrbycc (iprange, cc2, trange) values (\'{iprange}\', \'{cc2}\', tstzrange(\'{date}\', null));'.format(iprange=k, cc2=v, date=args.date)

for k, v in curdb.items():
  print 'update namicidrbycc set trange = tstzrange(lower(trange), \'{date}\') where iprange = \'{iprange}\' and cc2 = \'{cc2}\' and trange @> \'{date}\'::timestamp with time zone;'.format(iprange=k, cc2=v, date=args.date)

for k, v in splitit.items():
  print 'update namicidrbycc set trange = tstzrange(lower(trange), \'{date}\') where iprange = \'{iprange}\' and cc2 = \'{cc2}\' and trange @> \'{date}\'::timestamp with time zone;'.format(iprange=k, cc2=v['cc2'], date=args.date)
  for k2, v2 in v['leaf'].items():
    print 'insert into namicidrbycc (iprange, cc2, trange) values (\'{iprange}\', \'{cc2}\', tstzrange(\'{date}\', null));'.format(iprange=k2, cc2=v2, date=args.date)

for k, v in mergeit.items():
  for k2, v2 in v['leaf'].items():
    print 'update namicidrbycc set trange = tstzrange(lower(trange), \'{date}\') where iprange = \'{iprange}\' and cc2 = \'{cc2}\' and trange @> \'{date}\'::timestamp with time zone;'.format(iprange=k2, cc2=v2, date=args.date)
  print 'insert into namicidrbycc (iprange, cc2, trange) values (\'{iprange}\', \'{cc2}\', tstzrange(\'{date}\', null));'.format(iprange=k, cc2=v['cc2'], date=args.date)

CIDR の包含関係によっては分割したり統合したりという操作が必要になると ころがミソで、これを SQL で実現するのは少々困難であった。もちろん出来 ないということはないと思うので、単純明快な解をお持ちの方がおられました ら、是非私に教えてやってくださいませ。

試みたところの断片的な SQL を並べておく。

-- cidr matches. cc2 doesn't. terminate existings and insert with new cc2.
WITH upd AS (
  UPDATE namicidrbycc AS t1
    SET trange = tstzrange(lower(trange), current_date)
    FROM namicidrbycc_tmp AS t2
    WHERE t1.cidr = t2.cidr AND t1.cc2 != t2.cc2 AND t1.trange @> now()
  RETURNING t2.cidr, t2.cc2, tstzrange(current_date, null)
)
INSERT INTO namicidrbycc (cidr, cc2, trange) SELECT * FROM upd;
-- exists in database, but not in the new data. terminate trange.
UPDATE namicidrbycc AS t1
SET trange = tstzrange(lower(t1.trange), current_date)
WHERE NOT EXISTS
  (SELECT *
   FROM namicidrbycc_tmp AS t2
   WHERE (t1.cidr = t2.cidr OR t1.cidr >> t2.cidr OR t1.cidr << t2.cidr)
          AND t1.trange @> now())
  AND upper_inf(t1.trange)
-- not exists in db, but does in the new data. insert.
INSERT INTO namicidrbycc
  SELECT cidr, cc2, tstzrange(current_date, null)
    FROM namicidrbycc_tmp AS t1
    WHERE NOT EXISTS
      (SELECT * FROM namicidrbycc AS t2
         WHERE (t1.cidr = t2.cidr OR t1.cidr >> t2.cidr OR t1.cidr << t2.cidr)
                AND t2.trange @> now());
-- existing entry in db contains new data. split existing one and update.
WITH upd AS (
  UPDATE namicidrbycc AS t1 SET trange = tstzrange(lower(trange), current_date)
    FROM namicidrbycc_tmp AS t2
    WHERE t1.cidr >> t2.cidr AND t1.trange @> now()
  RETURNING t1.cidr AS old_cidr, t1.cc2 AS old_cc2, t1.trange, t2.cidr AS new_cidr, t2.cc2 AS new_cc2
)
INSERT INTO namicidrbycc
  SELECT devided_cidr,
    CASE
      WHEN devided_cidr = new_cidr THEN new_cc2
    ELSE
      old_cc2
    END,
    tstzrange(upper(upd1.trange), null)
    FROM upd AS upd1,
      (SELECT split_cidr(old_cidr, new_cidr) AS devided_cidr
         FROM upd AS upd2) AS sc1
         WHERE devided_cidr << old_cidr
;
CREATE OR REPLACE FUNCTION split_cidr(net cidr, exc cidr)
RETURNS SETOF cidr LANGUAGE plpgsql AS $$
DECLARE
  r cidr;
  lower cidr;
  upper cidr;
BEGIN
  IF masklen(net) >= 32 THEN RETURN; END IF;

  lower = set_masklen(net, masklen(net)+1);
  upper = set_masklen( (lower | ~ netmask(lower)) + 1, masklen(lower));

  IF exc = upper THEN
    RETURN NEXT lower;
    RETURN NEXT upper;
  ELSIF exc = lower THEN
    RETURN NEXT lower;
    RETURN NEXT upper;
  ELSIF exc << upper THEN
    RETURN NEXT lower;
    FOR r IN SELECT * from split_cidr(upper, exc)
    LOOP
      RETURN NEXT r;
    END LOOP;
  ELSE
    FOR r IN SELECT * from split_cidr(lower, exc)
    LOOP
      RETURN NEXT r;
    END LOOP;
    RETURN NEXT upper;
  END IF;
  RETURN;
END $$;
 -- newdata contains existing one(s). update existings and insert the new.
 WITH upd AS (
   UPDATE namicidrbycc as t1
    SET trange = tstzrange(lower(t1.trange), current_date)
    FROM namicidrbycc_tmp AS t2
    WHERE t1.cidr << t2.cidr AND t1.trange @> now()
  RETURNING t1.cidr AS old_cidr, t1.cc2 AS old_cc2, t1.trange AS new_trange,
    t2.cidr AS new_cidr, t2.cc2 AS new_cc2
)
INSERT INTO namicidrbycc (cidr, cc2, trange)
  SELECT DISTINCT new_cidr, new_cc2, tstzrange(current_date, null) FROM upd;

これで、一日一回データ更新をしておけば、CIDR がどの ccTLD に割り当てら れているかの履歴がデータベースに蓄積されることになる。 しかも、毎回約 10 万行が追加されるようなことにもならない(約1ヶ月の運 用後でも 105,420 行)ので、更新時に苦労する甲斐はあると思う。 最も多く更新された CIDR は 213.108.31.0/24 だが、高々2回更新であった。

1
2
3
4
5
     iprange     | cc2 |                       trange
-----------------+-----+-----------------------------------------------------
 213.108.31.0/24 | NL  | ["2016-05-13 00:00:00+00","2016-06-02 00:00:00+00")
 213.108.31.0/24 | CY  | ["2016-06-02 00:00:00+00","2016-06-10 00:00:00+00")
 213.108.31.0/24 | LI  | ["2016-06-10 00:00:00+00",)

しかし、実はまだ問題が残っていて、ARIN 支配下の US が持っている CIDR を APNIC 支配下の例えば TH へ再割り当てするような動きが時々あって、こ の時は上のスクリプトが生成する SQL では対応できずに制約に引っかかる。

どうやら、ARIN 側でデータを消すタイミングと APNIC 側でデータを入れるタ イミングの問題で、ある時点で当該 CIDR が両方の ccTLD に属するようなデー タになってしまうようだ。 例えばこんな感じ。

(2016-05-01 時点) 96.30.0.0/17 US

(2016-05-06 時点) ← 衝突発生 96.30.0.0/17 US 96.30.64.0/18 TH

(2016-05-07 時点) ← 衝突解消 96.30.0.0/18 US 96.30.64.0/18 TH

これについては diff を取る対象のファイルで 2016-05-06 の “96.30.64.0/18 TH” の行を削除することで対応した。 パターンがわかっているのでスクリプトに取り込めないということはないと思 うけれども、約一ヶ月分のデータで数回程度しか起きていないので、まあ、い いかなと。

データ利用

  • ある時点の CIDR と ccTLD の関係のリスト
SELECT iprange, cc2, trange FROM namicidrbycc
  WHERE trange @> '2016-06-01'::timestamp with time zone;
  • ある時点の ccTLD が保有する IP の数のリスト
SELECT cc2, sum(@@ iprange) AS ipcount
FROM namicidrbycc
WHERE trange @> '2016-06-01'::timestamp with time zone
GROUP BY cc2;
  • テーブルにある全ての日付について ccTLD が保有する IP の数を列挙する。
SELECT t1.date, cc2, sum(@@ t2.iprange) AS ipcount
  FROM
  (SELECT generate_series(min(lower(trange)), max(upper(trange)), '1 day')
     FROM namicidrbycc) AS t1(date)
   LEFT OUTER JOIN namicidrbycc t2
   ON trange @> t1.date::timestamp with time zone
   GROUP BY cc2, t1.date;
  • 直上の日付別 ccTLD 別 IP 保有数を毎回計算するコストを慮ってテーブル に入れておく。維持する手間が出るので、できれば VIEW にしたいところ。

    CREATE TABLE namicidrbycc_ipcount (

    date TIMESTAMP WITH TIME ZONE NOT NULL, cc2 TEXT NOT NULL, ipcount BIGINT NOT NULL

    ); CREATE INDEX ON namicidrbycc_ipcount(date); CREATE INDEX ON namicidrbycc_ipcount(cc2); CREATE INDEX ON namicidrbycc_ipcount(ipcount);

    INSERT INTO namicidrbycc_ipcount (date, cc2, ipcount)
    SELECT t1.date, cc2, sum(@@ t2.iprange) as ipcount

    FROM (SELECT generate_series(min(lower(trange)), max(upper(trange)), ‘1 day’)

    FROM namicidrbycc

    ) AS t1(date) LEFT OUTER JOIN namicidrbycc AS t2 ON trange @> t1.date::timestamp with time zone GROUP BY cc2, t1.date;

    INSERT INTO namicidrbycc_ipcount (date, cc2, ipcount)
    SELECT date, ‘total’ AS cc2, sum(ipcount) AS ipcount

    FROM namicidrbycc_ipcount GROUP BY date;”

  • ccTLD が保有する IP の数の多い方から12 ccTLD(とtotalで合計13)の保有IP数を時系列で描画する。

psql mydb pgsql -t -c "SELECT cc2, ipcount FROM namicidrbycc_ipcount WHERE date = (SELECT max(date) FROM namicidrbycc_ipcount) GROUP BY cc2,ipcount ORDER BY ipcount DESC LIMIT 13;" \
| awk ' \
    BEGIN { C[1] =  "111111A0"; \
            C[2] =  "FF0000A0"; \
            C[3] =  "00FF7FA0"; \
            C[4] =  "FF00FFA0"; \
            C[5] =  "7FFF00A0"; \
            C[6] =  "0000FFA0"; \
            C[7] =  "FF7F00A0"; \
            C[8] =  "00FFFFA0"; \
            C[9] =  "FF007FA0"; \
            C[10] = "00FF00A0"; \
            C[11] = "7F00FFA0"; \
            C[12] = "FFFF00A0"; \
            C[13] = "007FFFA0"; \
            print "rrdtool graph ipcountbycc.png --start=1462970000 --end 1465603200 --step 86400 --title \47Top 12 IP Address Holders by ccTLD\47 --vertical-label \47ipcount\47 --logarithmic --units=si --width 1200 --height 400 --watermark \47powered by daseinred.\47 \\";} \
    !/^$$/ {print "\"DEF:" $$1 "=sql//pgsql/host=127.0.0.1/dbname=daseinred/username=pgsql/password=//namicidrbycc_ipcount/extract(epoch from date)/ipcount/cc2=\47" $$1 "\47:avg:AVERAGE\" \"LINE1:" $$1 "#" C[NR] ":" $$1 "\" \\";}' \
| /usr/local/bin/bash
_images/ipcountbycc.png
  • ある時点の ccTLD が保有する IP の数をヒストグラムに描画する。下の SELECT 文の結果を ipcountbycc_histgram.py の標準入力へ流し込む。
SELECT cc2, ipcount FROM namicidrbycc_ipcount
  WHERE date = '2016-06-10'
  ORDER BY ipcount DESC;
ipcountbycc_histgram.py
#!/usr/local/bin/python2

# psql daseinred pgsql -c "select cc2, ipcount from namicidrbycc_ipcount where date = '2016-06-10' order by ipcount desc;" | bin/ipcount_stats.py 

#import argparse
#import datetime
from sys import stdin
import re
import numpy as np
import matplotlib.pyplot as plt

#p = argparse.ArgumentParser(description='ipcount to histgram')
#p.add_argument('--date', '-d', help='assign date as YYYYMMDD.', default=datetime.date.today())
#args = p.parse_args()

ipcount = {}
r = re.compile(r'^[ 	]+([A-Za-z]+).*[ 	]+([0-9]+)')

for line in stdin.readlines():
  m = r.search(line.rstrip())
  if m is not None:
    ipcount[m.group(1).decode('utf-8')] = int(m.group(2).decode('utf-8'))
ipcount.pop('total', None)

ipcv       = ipcount.values()
ipc_min    = np.min(ipcv)
ipc_max    = np.max(ipcv)
ipc_median = np.median(ipcv)
ipc_mean   = np.mean(ipcv)

fig = plt.figure()
ax1 = fig.add_subplot(211)

ax1.set_title("Distribution of IP Addresses assigned to ccTLDs")
ax1.set_ylabel("frequency")
ax1.set_xlim([0.95 * ipc_min, ipc_max / 0.95])
(n, bins, patches) = ax1.hist(ipcv, bins=48)
ax1.set_xticks((2*10**8, 4*10**8, 6*10**8, 8*10**8, 10*10**8, 12*10**8, 14*10**8, 16*10**8))
ax1.set_xticklabels(('2', '4', '6', '8', '10', '12', '14', '$16*10^8$'))
ax1.axvline(x=ipc_min, color='darkorange', label='min: {}'.format(ipc_min))
ax1.annotate('min', [ipc_min, 0.95 * np.max(n)], color='darkorange', rotation=90)
ax1.axvline(x=ipc_median, color='darkorange', label='median: {}'.format(int(ipc_median)))
ax1.annotate('median', [ipc_median, 0.95 * np.max(n)], color='darkorange', rotation=90)
ax1.axvline(x=ipc_mean, color='darkorange', label='mean: {}'.format(int(ipc_mean)))
ax1.annotate('mean', [ipc_mean, 0.95 * np.max(n)], color='darkorange', rotation=90)
ax1.axvline(x=ipc_max, color='darkorange', label='max: {}'.format(ipc_max))
ax1.annotate('max', [ipc_max, 0.95 * np.max(n)], color='darkorange', rotation=90)
for k, v in sorted(ipcount.items(), key=lambda x:x[1], reverse=True)[:3]:
  ax1.annotate(k, [v, 0.1 * np.max(n)], color='darkorange', rotation=90)
ax1.annotate('N={}\n2016/Jun/10'.format(len(ipcv)), [0.8 * ipc_max, 0.8 * np.max(n)], color='darkorange')

ax2 = fig.add_subplot(212)
ax2.set_xlabel("# of IP addresses assigned")
ax2.set_ylabel("frequency")
ax2.set_xscale("log")
ax2.set_xlim([0.95 * ipc_min, 3 * ipc_max])
(n, bins, patches) = ax2.hist(ipcv, bins=10**np.linspace(0.1, (np.log10(ipc_max) + 0.1), 48))
ax2.axvline(x=ipc_min, color='darkorange', label='min: {}'.format(ipc_min))
ax2.annotate('min', [ipc_min, 0.95 * np.max(n)], color='darkorange', rotation=90)
ax2.axvline(x=ipc_median, color='darkorange', label='median: {}'.format(int(ipc_median)))
ax2.annotate('median', [ipc_median, 0.95 * np.max(n)], color='darkorange', rotation=90)
ax2.axvline(x=ipc_mean, color='darkorange', label='mean: {}'.format(int(ipc_mean)))
ax2.annotate('mean', [ipc_mean, 0.95 * np.max(n)], color='darkorange', rotation=90)
ax2.axvline(x=ipc_max, color='darkorange', label='max: {}'.format(ipc_max))
ax2.annotate('max', [ipc_max, 0.95 * np.max(n)], color='darkorange', rotation=90)
for k, v in sorted(ipcount.items(), key=lambda x:x[1], reverse=True)[:3]:
  ax2.annotate(k, [v, 0.1 * np.max(n)], color='darkorange', rotation=90)

fig.tight_layout()
fig.savefig('ipcountbycc_histgram.png', format='png')
#plt.show()
_images/ipcount_histgram1.png

備考

  • 2016/Jul/06 ごろから Jul/15 ごろまで書いた。
[1]世界を5分割して支配している、ってちょっと燃えるかも。(厨二病
[2]必ず bit length 表記できる個数になっているかどうかはよくわからない ^^;
[3]bit length 表記なので、CIDR A, B について「重複しない」「合同」「包含する」「包含される」の関係はあっても、「一部だけ重複」という関係はない。