5〜6万でAWSのような自宅クラウドサービスを作る(Webサーバー構築編2)

前回から少し記事が飛びましたが、続いてルーターを設定してWebサーバーを公開していきましょう。

まずはブラウザからルーターの設定画面にアクセスします。
ルータの種類によって細かい設定方法は違うと思うのですが、Webサーバーを公開する方法は大体一緒だと思います。

まずは、ifconfigコマンドで、自分のパソコンのローカルIPを調べます。
私のサーバーの場合は、「192.168.10.9」に設定されています。

ifconfig

この場合は、一番最後の数値を1にしてアクセスします。
今回は、「192.168.10.1」にアクセスしてみます!

すると、こんな感じのページにたどり着きます。

f:id:u651601f:20131112231940p:plain


その後、左のタブから詳細設定を選択し、ポートマッピングの設定画面に移ります。

f:id:u651601f:20131112232028p:plain


そして、公開したいサーバーのIPアドレスをポートマッピングに登録します。

f:id:u651601f:20131112232114p:plain


普通にルーターを使ってたら、サーバーにアクセスしようとするとルーターが遮断してしまうので、ポートマッピングを設定して遮断されないようにする感じですね。


これでサーバーが公開されたのですが、2XX.XXX.XXX.XみたいにIPアドレスを指定しないとアクセス出来ないのは面倒なので、http://www.google.comのようにドメインでアクセスできるようにしましょう。

はじめに登録したVALUE DOMAINで、サーバーの固定IPを登録します。

VALUE DOMAINにログインすると、このようながメニューになります。
DNSレコード/URL転送の変更」を選択してください。

f:id:u651601f:20131112233029p:plain


そして、ASAHIネットから与えられた固定IPと、VALUEドメインを以下のように入力し、保存ボタンをクリックして完了です。

f:id:u651601f:20131112233356p:plain

そしてドメインをブラウザから打ち込んでアクセスすると、きちんとアクセスできると思います!

日経Linuxを効果的に読むために、あなたにおくる5つの本

今朝、宝塚の本屋さんを覗いて見ると、日経Linuxの最新号が出ていました。
その中の記事に、「AWSのようなクラウドを作る」といった記事も掲載されていました。

日経 Linux (リナックス) 2013年 12月号

日経 Linux (リナックス) 2013年 12月号

まさに、僕がこのブログで伝えようとしていたことが数ページで書かれていてびっくりしましたw
雑誌は初心者向けに書かれていたのですが、正直初めてサーバーに触れる人には理解をするのが難しいんじゃないかなってくらいギュッと詰まった内容になっています。


雑誌の内容は、「サーバー超マスター100ステップ」となっており、このコンテンツには

  • 超入門
  • 初級
  • 中級
  • 上級
  • 超上級

といった感じでステージ分けしてあります。

そこで、この日経Linux 12月号を効果的に読むために、ステージ別に合わせて僕のオススメの本を紹介しようと思います。

超入門ステージ

このステージでは、サーバーにOSをインストールしてセキュリティーの設定をするといった感じに進んで行きます。
この時にTCP/IPに関する専門用語が飛び出してくるので、すこしTCP/IPについての知見を深めておいたほうがいいと思います。

ただ、辞書的なTCP/IPの本を読んでも面白く無いので、実際に自分のコンピュータに送られてくるTCP/IP関係のデータを見ながら学んでいくと効果的な学習が期待されると思います。
その際にオススメな本がこちらです。

実践 パケット解析 第2版 ―Wiresharkを使ったトラブルシューティング

実践 パケット解析 第2版 ―Wiresharkを使ったトラブルシューティング

Wiresharkというツールを使って、実際に自分のパソコンに送られてくるTCP/IPのデータを見てみよう!って趣旨の本です。
私も大学に入った当初はネットワークの知識は皆無だったので、この本を読みながらパケット解析をしてTCP/IPについて学びました。

この本をひと通り学んだら、この後のステージで出てくる専門用語は8割カバーできるので、雑誌を読む前に一読する価値はあると思います。

入門ステージ

ここではUbuntuのアプリケーションをインストールして使う方法についてまとめてあります。例えば、動画サーバーの作り方や、Ubuntuの各種設定方法について書いてあります。

したがって、この辺を効率よく学ぶなら、Ubuntu関連の本をサッと眺めておくと良いでしょう

この本は、お家にある古いパソコンを、最新のOSを使って高性能なパソコンにしよう!といった趣旨の内容です。これも僕が1回生の頃に読んだ雑誌なのでUbuntuに関する情報は少し古いですが、UbuntuなどのLinuxに触れるいい機会を頂きました。
これでUbuntuの操作方法を覚えられただけでなく、家にある機械をすべてLinux化させてしまおう!という欲望が未だに残っていますw

中級ステージ

中級ステージでは、クラウドアプリケーションを構築してみんなと共有しようといった内容が紹介されています。

ここではSSHFTPなどのサーバー関連ソフトの知識が必要になってきます。インストールして使えたらいいや〜って方は、この雑誌の通りにUbuntuを操作すればいいと思いますが、きちんと理解して使いこなしたい方には「Ubuntu Server 実践バイブル」をオススメします。

私は普段CentOSばっかり使っているので、そこまで詳しくこの本を読んだわけではありませんが、この本でUbuntuサーバーのアプリケーションの使い方はひと通り学べます。

もし、CentOSを使って勉強されたい方は、こちらの本から読んでみるといいかと思います。

上級ステージ

正直この辺をきちんと理解できるレベルになれば、LPIC Level 3の試験も少し勉強したら合格できると思います。なので、きちんと学びたい方はLPICの参考書を本屋さんから探すのがいいと思います。

ある程度操作方法だけ理解したい方にはこちらを。

サーバ・インフラ構築・運用完全ガイド ~Linux/FreeBSD/Solaris/HP-UX/AIX/WindowsServerマルチ対応

サーバ・インフラ構築・運用完全ガイド ~Linux/FreeBSD/Solaris/HP-UX/AIX/WindowsServerマルチ対応

技術評論社が出版している本なので、ある程度深いところまで知識を身に付けることが出来ますが、正直かなり読みにくいです。

最低でも1ヶ月はかかると思うので、日経Linuxを読むために読む本ってよりは、日経Linuxを読んでからこの本を読んだほうが得策かもしれませんね。

超上級ステージ

最終ステージでは、OpenStackなどを使ったクラウドシステムの構築について紹介してあります。
ざっと読んでみると、ほとんどOpenStackの内容ばっかりなので、とりあえずOpenStackの本を紹介しておきます。

オープンクラウド入門 CloudStack、OpenStack、OpenFlow、激化するクラウドの覇権争い (Next Publishing(Cloudシリーズ))

オープンクラウド入門 CloudStack、OpenStack、OpenFlow、激化するクラウドの覇権争い (Next Publishing(Cloudシリーズ))

この本ではOpenStackとCloudStackの2種類のアプリケーションについて解説してあるのですが、
今後このブログでも紹介していく予定の「CloudStack」のほうが個人的に好きなので、CloudStackの方も紹介しておきます。

CloudStack徹底入門

CloudStack徹底入門



以上が、日経Linux 12月号を読む際に、合わせて読むと良いと思った本です。
題名では5冊と言っていたのですが、余裕で5冊を超えて紹介してしまいました^^;
これらの本をすべて読むと、次の日経Linuxが出版されてしまうので、自分のステージに合わせて本をチョイスして、自分のステージ以外の部分は流し読みするのがいいかと思います。

剃ったヒゲで車の模型を作るには、どのくらいのコストがかかるか?

今夜は眠れないので、くだらないことをたくさん考えてしまいます。


過去の自分の記念写真を見てると、ファッション感覚でヒゲを生やしている時と、
不潔だから剃ってしまってる時があります。


写真を見てて思いました。


「私から離脱したヒゲたちを集合させて、何かを作ることは出来ないか?」


そこで、試しに「車の模型」を作成する思考実験をしてみたいと思います。


まずは、1ヶ月あたりにゴミとして処理されていくヒゲの全長を計算します。
私は3日に1回ほどヒゲを剃ります。
大体4ミリまで伸びたら剃ります。
したがって、

1ヶ月のヒゲ全長 = 365/12(1ヶ月の平均日数) × 1/3(1日辺りのヒゲを剃る回数) × 4mm(一本あたりのヒゲの長さ) × 20本/cm^2(単位面積:cm^2辺りのヒゲの量) × 7cm^2(ヒゲが散布されている領域)

⇛ 全長 = 5677.8mm/月

私の顎には、縦2cm×横7cmの三角形上にヒゲが分布されていたので、そこからヒゲ領域の面積を導出しました。


さて1ヶ月あたりのヒゲの全長がわかったので、次はヒゲの体積を出したいと思います。

そのためにはヒゲの断面積の情報が必要なのですが、Googleで「ヒゲ 断面図」「Wikipedia ヒゲ」、「Phillips シェーバー 構造」と調べても、全く情報が手に入りませんでした。
しかたがないので、髪の毛とヒゲの断面積がだいたい同じという前提で計算します。

髪の毛の断面積はこちらのサイトを参考にしました。
毛と毛包の構造について

1ヶ月で集まるヒゲの全体積 = 5677.8mm/月 × 0.05mm*0.05mm(ヒゲの断面積) = 14.1945mm^3/月

ということで、1ヶ月に集まるヒゲの体積は約14.2mm^3であることがわかりました。


さて、ここから車の模型を作るためにかかる時間を考えてみましょう。

ここで作成する模型の車種は、とりあえず「フォルクスワーゲン ビートル」にします(さっきまでコナンのマジキチSSを見てたので、アガサ博士が乗ってる車を使うことにした)

ビートルの体積は、下のサイトを参考にすると8.5~10.3m^3らしいですね。
とりあえず間をとって9.5m^3辺りで計算してみます。
,車種サイズ一覧表 | ガラスコーティングのIIC

模型を作成するのにかかる月数 = 9.5*10^9 mm^3(ビートルの体積) ÷ 14.1945mm^3/月 ≒ 6.69*10^8ヶ月 = 5.575*10^7年

したがって、ヒゲでビートルの模型を作成するのにかかる年月は、約55,750,000、つまり5600万年ほどかかるみたいですw キリストでも骨が折れるレベルですね〜

ただし、これは僕一人だけではたいへんなので、日本人の男性の方が全員協力してくれたらどのくらいの期間で完成するのでしょうか?


現在日本の成人は1億人ほど。 そのうち約半分が成人男性なので、約5000万人がヒゲを生やす成人男性です。


おお! これだけの人間が協力してくれるなんて頼もしい!


ただし、全員きちんと協力してくれることは期待していません。頑張っても全体の2割のヒゲしか集まらないでしょう。(パレートによる20:80の法則より)

そこで、全体の2割、つまり1000万人が協力してくれたときの場合を計算してみましょう。

集まるまでの月 = 6.69*10^8ヶ月 / 10^7 = 66.9ヶ月 ≒ 5年と7ヶ月


国全体による総プロジェクトの結果、5年半でヒゲによるフォルクスワーゲン ビートルが完成することがわかりました!

もしヒゲで車の模型を作りたいのならば、8年間の見積もりを立て、3年で成人男性総勢を動員し、5年でひたすらヒゲを集めることをオススメします。

喫煙者の1日を観察する

私は毎日1箱タバコを吸う、世間から嫌われし喫煙者なのですが、
年間のタバコのコストなどを計算してみようと思いました。

だいたい1箱440円で1日間吸うのだから、普通に考えて440×365円で計算できるのですが、
これはただのフェルミ推定を行っているだけでツマラナイです。

なので、Rubyを使って1時間ほどかけて喫煙者の一日をシミュレーションするプログラムを作成しましたw
簡単な喫煙者モデルと、喫煙者の一日のモデルを作成してシミュレートしただけなので、細かい説明は省きます。

#喫煙者クラス
class Smoker
	attr :smoking_time #累計喫煙時間
	attr :tobacco #現在所持しているタバコの本数
	attr :tobacco_total #今まで吸ったタバコの本数
	attr :cost #タバコにかけた金額の合計
	attr :interval_average #次のタバコを吸うまでの平均時間(分)
	attr :interval #のタバコを吸うまでの時間(分)

	def initialize(average)
		@smoking_time = 0
		@tobacco = 0
		@tobacco_total = 0
		@cost = 0
		@interval_average = average
	end

	#我慢できないのでタバコを吸う
	def smoke
		#次にタバコを吸うまでの時間(分)
		@interval = 60 * (Random.rand(60) + 1) / @interval_average 
		charge_tobacco() unless @tobacco > 0
		@tobacco -= 1
		@smoking_time += 10 #10分間喫煙タイム
		@tobacco_total += 1 #また彼の体内に有害なニコチン・タールが注入されてしまった
	end

	#タバコが無くなったので買いに行く
	def charge_tobacco
		@cost += 440
		@tobacco += 20
	end
end

#喫煙者の1日
class SmokersOneDay
	attr :smoker #喫煙者
	attr :rest_time #喫煙者の1日の残り時間

	def initialize(smoker, rest)
		@smoker = smoker
		@rest_time = rest
	end

	#喫煙者の1日を進める
	def tick
		@rest_time = (@rest_time > @smoker.interval) ? (@rest_time - @smoker.interval) : 0
		@smoker.smoke
		@rest_time = (@rest_time > 10) ? (@rest_time - 10) : 0
	end
end

#時間変換
def to_hour(time)
	return 8 + time/60
end
def to_minute(time)
	return time % 60
end

#喫煙者を作成する
smoker = Smoker.new(45)

#あなたの一日を観察させてほしいとお願いした
#その依頼中に彼は1本タバコを吸っていた
smoker.smoke

#彼から承諾を得たのでレポート用紙を用意する
f = open("smoker_report.txt", "w")

#彼の1年後を楽しみに待つとしよう
365.times do |n|
	#喫煙者の一日が始まる
        one_day = SmokersOneDay.new(smoker, 15*60)

	#早送りで喫煙者の一日をご覧いただこう
	f.write("#{n+1}日目...\n")
	while one_day.rest_time > 0
		one_day.tick
		#観察調査をレポートにまとめる
		now_time = 15*60 - one_day.rest_time #現在の時刻
		hour = to_hour(now_time)
		minute = to_minute(now_time)
		report = "#{hour}:#{minute}, #{smoker.tobacco_total}\n"
		f.write(report)
	end
end

#さて、彼は1年間でいくらのタバコ代を浪費したのだろう?
p "年間のタバコ代"
p smoker.cost
p "年間喫煙時間"
p smoker.smoking_time / 60
p "年間タール摂取量"
p smoker.tobacco * 8

#最後にレポート用紙を上司に提出する
f.close

#これにて喫煙者の調査を終了する

レポートの内容がこちら(一部抜き出し)

21:8, 36
21:42, 37
23:0, 38
3日目...
9:18, 39
10:12, 40
10:50, 41
12:14, 42
13:28, 43
14:43, 44
16:7, 45
16:50, 46
17:14, 47
18:6, 48
19:6, 49
19:26, 50
20:29, 51
21:19, 52
22:31, 53
23:0, 54
4日目...
8:54, 55
9:34, 56

Rubyって話し言葉みたいなんだから、物語チックにコメントを書いてみようと思ったので、
プログラム中のコメントをふざけさせて頂きましたw

実行結果した結果、1年間のタバコ代は148720円とのことでした(ランダムシミュレーションなので、実行ごとに多少の誤差は発生します)
これは最初に計算した、440×365=160600円とかなり近い計算になります。

ただしフェルミ推定の方は、かなりアバウトな計算になるのでシミュレーション結果の方の金額が現実的であると思います。


今回のシミュレーションでわかったことは、普通にタバコを吸ってるだけで
年間iMacを1台買い換えれるくらいのお金をドブに捨ててるわけですね。

ただ一番びっくりしたのが、1年間の内タバコを吸ってる時間が1000時間を超えること。
つまり、1年間のうち1ヶ月以上を喫煙時間で消費してしまってるのですね。
タバコを吸ってると他のオフィスの人とコミュニケーションが取れるから、営業で案件を取ってくる際にやりやすいと思ってたのですが、えらくコストの掛かるコミュニケーションだなって思いましたw

以上。

「あいまいな哲学」か「決定的な哲学」か?

昨日は寝不足でひどく疲れてたのか、今日起きて自分のメモノートを確認してみると哲学に関する考察が殴り書きされていたので、自分の思考についてまとめてみようと思います。

もはやサーバーのこと全く関係ないですが。

「曖昧な哲学」(= P) か、「決定的な哲学」(= Q) か?

(§) PかQのどちらが正しいかについて考察するにあたって、正当性を「人類の発展に寄与するかどうか」の観点で行うとする

(i) 人類の発展とは如何なるものか。そこで人類の発展史を抽象的に捉えてみるとしよう。よく学問の進歩のためには過去の叡智を批判的に捉えることが重要と一般的に言及される。そこで人類の発展を「過去の定理を変更する」こと、つまり創造的破壊こそ人類の発展であると狭義する。なぜそのように定義するのかというと、ある恒久的な定理が存在するとすると、その定理が扱う分野を完全に停滞させてしまうからである。

R したがって、ある時点tにおいてQ(t)が成り立ち、それをQ(t')で変更するとする。(Q(t)の否定でなくとも) ⇛ 「人類の発展に寄与した」

このときRについて考える。Rの推論が真であるならば、Q(t) = Rのときはどのようになるのだろうか?
Qを変更することが「人類の発展」となるならば、Q=Rのとき、Rを変更することも「人類の発展」となってしまう。したがって、Rからは自己矛盾が発生するため、R推論の真偽は決定的でない。


(ii) Rではなく次のSについて考えてみる。

S ある時点でP(t)が成り立ち、それをP(t')で変更する ⇛ 「人類の発展に寄与した」

このようなSを考慮する事自体おかしなことは自明である。なぜならば、Pというのは「非決定的」なため、P自体の真偽がtにおいて特定できない。したがって、Sも非決定的である。

(i), (ii)より、(§)や「定理の創造的破壊が人類の発展に寄与する」という視点からはP, Qの正当性を決定出来ない。したがって、私は哲学を今回言及した「前提」で行っている者に述べたい。

あなたがたが鼻高々と学問している哲学というものは、本当に人類の進展に寄与しているのか?
もう少しそのことについても考えても良いのではないのか。あなたが哲学者であるのならば。



以上がノートに書いてあった内容ですww

Webサーバーのログ情報を視覚化する

サーバーエンジニアをやってると、シェルスクリプトでログ情報を編集してホームページなどのアクセスランクを作ることは多いと思いますが、経営者やデザイナーなどエンジニア以外の人にとっては、その(ほとんど生な)データは見ても情報を見抜きにくい上、面白くないのでアイデアがわきにくいと思います。


そこで、このつまらないサーバーログをProcessingを使ってわかりやすくネットワークグラフとして表示するプログラムを紹介します。


グラフの描画に関しては「ビジュアライジング・データ」を参考に作成しました。

ビジュアライジング・データ ―Processingによる情報視覚化手法

ビジュアライジング・データ ―Processingによる情報視覚化手法



まずは、サーバーのログをProcessingで扱いやすいように整形します。

#!/bin/bash
log=/var/log/nginx/access.log  #path to log file

# except the acccess history of me
# then, shape it, and save as a text
cat $log | grep -v 192.168.10.3 | cut -d ' ' -f 6 | sort | uniq -c | sort -r | grep .html | sed -r 's/.html//g' > access_rank.txt


実行結果は以下のようになります。

...
      2  company message
      2  company index
      2  company companyinfo
      1  update character
      1  sitemap
      1  site introduction index
      1  service
      1  ricruit
...


今回は最近作成したNginxのサーバーのログファイルを整形したので、情報量が少なかったのです。


さて、この空白で区切られたデータを表示するプログラムを紹介していきます。

ネットワークを構成するためには「ノード」と「エッジ」が必要になります。今回は「ノード」がHTMLページ、「エッジ」が「ノード」つなぐための「線」になります。

まずは、この2つをクラスによって定義していきます。

class Node {
  float x, y;
  float dx, dy;
  boolean fixed;
  String label;
  int count;
  
  Node(String label) {
    this.label = label;
    x = random(width);
    y = random(height);
  }
  
  void relax() {
    float ddx = 0;
    float ddy = 0;
    
    for (int j = 0; j < nodeCount; j++) {
      Node n = nodes[j];
      if (n != this) {
        float vx = x - n.x;
        float vy = y - n.y;
        float lensq = vx * vx + vy * vy;
        if (lensq == 0) {
          ddx += random(1);
          ddy += random(1);
        } else if (lensq < 100*100) {
          ddx += vx / lensq;
          ddy += vy / lensq;
        }
      }
    }
    float dlen = mag(ddx, ddy) / 2;
    if (dlen > 0) {
      dx += ddx / dlen;
      dy += ddy / dlen;
    }
  }
  
  void update() {
    if (!fixed) {
      x += constrain(dx, -5, 5);
      y += constrain(dy, -5, 5);
      
      x = constrain(x, 0, width);
      y = constrain(y, 0, height);
    }
    dx /= 2;
    dy /= 2;
  }
  
  void draw() {
    if (fixed) {
      fill(nodeColor);
      stroke(0);
      strokeWeight(0.5);
     
      String content = label + " " + count;
      float w = textWidth(content) + 10;
      float h = textAscent() + textDescent() + 4;
      ellipse(x, y, w*pow(1.06, count-1), h*pow(1.06, count+1));
      
      fill(0);
      textAlign(CENTER, CENTER);
      text(content, x, y);
    } else {
      fill(nodeColor);
      stroke(0);
      strokeWeight(0.5);
      ellipse(x, y, sqrt(count)*10, sqrt(count)*10);
    }
  }
  
  void increment() {
    count++;
  }
}

Node findNode(String label) {
  label = label.toLowerCase();
  Node n = (Node) nodeTable.get(label);
  if (n == null) {
    return addNode(label);
  }
  return n;
}

Node addNode(String label) {
  Node n = new Node(label);
  if (nodeCount == nodes.length) {
    nodes = (Node[]) expand(nodes);
  }
  nodeTable.put(label, n);
  nodes[nodeCount++] = n;
  return n;
}

class Edge {
  Node from;
  Node to;
  float len;
  int count;
  
  Edge(Node from, Node to) {
    this.from = from;
    this.to = to;
    this.len = 50;
  }
  
  void relax() {
    float vx = to.x - from.x;
    float vy = to.y - from.y;
    float d = mag(vx, vy);
    if (d > 0) {
      float f = (len -d) / (d * 3);
      float dx = f * vx;
      float dy = f * vy;
      to.dx += dx;
      to.dy += dy;
      from.dx -= dx;
      from.dy -= dy;
    }
  }
  
  void draw() {
    stroke(edgeColor);
    strokeWeight(0.35);
    line(from.x, from.y, to.x, to.y);
  }
  
  void increment() {
      count++;
    }
}

void addEdge(String fromLabel, String toLabel) {
  Node from = findNode(fromLabel);
  Node to = findNode(toLabel);
  from.increment();
  to.increment();
  
  // check whether the Edge have already existed.
  for (int i = 0; i < edgeCount; i++) {
     if (edges[i].from == from && edges[i].to == to) {
         edges[i].increment();
         return;
     }
  }
  
  Edge e = new Edge(from, to);
  e.increment();
  if (edgeCount == edges.length) {
    edges = (Edge[]) expand(edges);
  }
  edges[edgeCount++] = e;
}

クラス内のメゾッドにrelax(), update(), draw()があり、これらがグラフを上手く表示するようにエッジやノードの位置を調節するそうです。詳しいことは他の文献を参考にしろとのことでした。
また、ビジュアライジング・データには載っていませんでしたが、アクセス数の多いページのノードを大きく表示するように自身で改良しました。

ゲーム開発のための物理シミュレーション入門―Physics for Game Developers

ゲーム開発のための物理シミュレーション入門―Physics for Game Developers


さて、続いてエッジに先ほどのデータを追加していきます。

void loadData() {
  reader = createReader("/path/to/access_rank.txt"); // 先ほどシェルスクリプトで整形したデータへのパス
  try {
    String line = reader.readLine();
    while (line != null) {
      String[] columns = split(line, ' ');
      String fromEdge = INDEX;
      int num = 0;
      for (String word : columns) {
        int count = 0;
        if (!word.isEmpty() && isNumeric(word)) {
          num = Integer.parseInt(word);
        } else if (!word.isEmpty() && !word.toLowerCase().contains(INDEX) && num != 0) {
          for (int i = 0; i < num; i++)
            addEdge(fromEdge, word);
          fromEdge = word;
        }
      }
    line = reader.readLine();
    }
  } catch (IOException e) {
    e.printStackTrace();
  }
}

public static boolean isNumeric(String str)  
{  
  try  
  {  
    Integer d = Integer.parseInt(str);  
  }  
  catch(NumberFormatException nfe)  
  {  
    return false;  
  }  
  return true;  
}

これはProcessingのsetup()関数で呼び出されます。一行ごとにページヘのパスを調べてネットワークを構成します。


プログラムの大まかな構成は上記のような感じです。あとはマウスがクリックされた時の振る舞いや、ノードの色などを決めたりするだけなので、その辺りは読み飛ばしていってもらったらいいかと思います。

最後にプログラムの全体を掲載しておきます。

BufferedReader reader;

int nodeCount;
Node[] nodes = new Node[100];
HashMap nodeTable = new HashMap();

Node selection;

int edgeCount;
Edge[] edges = new Edge[500];

static final color nodeColor = #F0C070;
static final color selectColor = #FF3030;
static final color fixedColor = #FF8080;
static final color edgeColor = #000000;

PFont font;
static final String INDEX = "index";

void setup() {
  size(1000, 600);
  loadData();
  font = createFont("SansSerif", 10);
  textFont(font);
  smooth();
}

void loadData() {
  reader = createReader("/path/to/access_rank.txt");
  try {
    String line = reader.readLine();
    while (line != null) {
      String[] columns = split(line, ' ');
      String fromEdge = INDEX;
      int num = 0;
      for (String word : columns) {
        int count = 0;
        if (!word.isEmpty() && isNumeric(word)) {
          num = Integer.parseInt(word);
        } else if (!word.isEmpty() && !word.toLowerCase().contains(INDEX) && num != 0) {
          for (int i = 0; i < num; i++)
            addEdge(fromEdge, word);
          fromEdge = word;
        }
      }
    line = reader.readLine();
    }
  } catch (IOException e) {
    e.printStackTrace();
  }
}

public static boolean isNumeric(String str)  
{  
  try  
  {  
    Integer d = Integer.parseInt(str);  
  }  
  catch(NumberFormatException nfe)  
  {  
    return false;  
  }  
  return true;  
}

void draw() {
  background(255);
  
  for (int i = 0; i < edgeCount; i++)
    edges[i].relax();
  for (int i = 0; i < nodeCount; i++)
    nodes[i].relax();
  for (int i = 0; i < nodeCount; i++)
    nodes[i].update();
  for (int i = 0; i < edgeCount; i++)
    edges[i].draw();
  for (int i = 0; i < nodeCount; i++)
    nodes[i].draw();
}

void mousePressed() {
  float closest = 20;
  for (int i = 0; i < nodeCount; i++) {
    Node n = nodes[i];
    float d = dist(mouseX, mouseY, n.x, n.y);
    if (d < closest) {
      selection = n;
      closest = d;
    }
  }
  if (selection != null) {
    if (mouseButton == LEFT) {
      selection.fixed = true;
    } else if (mouseButton == RIGHT) {
      selection.fixed = false;
    }
  }
}

void mouseDragged() {
  if (selection != null) {
    selection.x = mouseX;
    selection.y = mouseY;
  }
}

void mouseRelesed() {
  selection = null;
}

class Node {
  float x, y;
  float dx, dy;
  boolean fixed;
  String label;
  int count;
  
  Node(String label) {
    this.label = label;
    x = random(width);
    y = random(height);
  }
  
  void relax() {
    float ddx = 0;
    float ddy = 0;
    
    for (int j = 0; j < nodeCount; j++) {
      Node n = nodes[j];
      if (n != this) {
        float vx = x - n.x;
        float vy = y - n.y;
        float lensq = vx * vx + vy * vy;
        if (lensq == 0) {
          ddx += random(1);
          ddy += random(1);
        } else if (lensq < 100*100) {
          ddx += vx / lensq;
          ddy += vy / lensq;
        }
      }
    }
    float dlen = mag(ddx, ddy) / 2;
    if (dlen > 0) {
      dx += ddx / dlen;
      dy += ddy / dlen;
    }
  }
  
  void update() {
    if (!fixed) {
      x += constrain(dx, -5, 5);
      y += constrain(dy, -5, 5);
      
      x = constrain(x, 0, width);
      y = constrain(y, 0, height);
    }
    dx /= 2;
    dy /= 2;
  }
  
  void draw() {
    if (fixed) {
      fill(nodeColor);
      stroke(0);
      strokeWeight(0.5);
     
      String content = label + " " + count;
      float w = textWidth(content) + 10;
      float h = textAscent() + textDescent() + 4;
      ellipse(x, y, w*pow(1.06, count-1), h*pow(1.06, count+1));
      
      fill(0);
      textAlign(CENTER, CENTER);
      text(content, x, y);
    } else {
      fill(nodeColor);
      stroke(0);
      strokeWeight(0.5);
      ellipse(x, y, sqrt(count)*10, sqrt(count)*10);
    }
  }
  
  void increment() {
    count++;
  }
}

Node findNode(String label) {
  label = label.toLowerCase();
  Node n = (Node) nodeTable.get(label);
  if (n == null) {
    return addNode(label);
  }
  return n;
}

Node addNode(String label) {
  Node n = new Node(label);
  if (nodeCount == nodes.length) {
    nodes = (Node[]) expand(nodes);
  }
  nodeTable.put(label, n);
  nodes[nodeCount++] = n;
  return n;
}

class Edge {
  Node from;
  Node to;
  float len;
  int count;
  
  Edge(Node from, Node to) {
    this.from = from;
    this.to = to;
    this.len = 50;
  }
  
  void relax() {
    float vx = to.x - from.x;
    float vy = to.y - from.y;
    float d = mag(vx, vy);
    if (d > 0) {
      float f = (len -d) / (d * 3);
      float dx = f * vx;
      float dy = f * vy;
      to.dx += dx;
      to.dy += dy;
      from.dx -= dx;
      from.dy -= dy;
    }
  }
  
  void draw() {
    stroke(edgeColor);
    strokeWeight(0.35);
    line(from.x, from.y, to.x, to.y);
  }
  
  void increment() {
      count++;
    }
}

void addEdge(String fromLabel, String toLabel) {
  Node from = findNode(fromLabel);
  Node to = findNode(toLabel);
  from.increment();
  to.increment();
  
  // check whether the Edge have already existed.
  for (int i = 0; i < edgeCount; i++) {
     if (edges[i].from == from && edges[i].to == to) {
         edges[i].increment();
         return;
     }
  }
  
  Edge e = new Edge(from, to);
  e.increment();
  if (edgeCount == edges.length) {
    edges = (Edge[]) expand(edges);
  }
  edges[edgeCount++] = e;
}


そして、実行した結果がこちらです。
f:id:u651601f:20131109170829p:plain

やっぱりデータが少ないので、少しさびしい印象を与えますね^^;
今回はホームページのアクセスログを解析しました。なのでノードの数は少なくて観やすかったと思います。(ページの数が50を超えるような大規模なホームページの場合はかなり見難いと思いますが)
このように、ノード数が少ないデータを可視化したいのなら、Processingでネットワークグラフを作成してみるといいかと思います。