やばい。今日書かないと2月は1日も書いてないことになってしまう。
というわけで今日知った小ネタ。
なんか 1.8.7 の頃からできるようなんだけど今頃知った。
2.1.1 :001 > a = (0..10).to_a => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 2.1.1 :002 > a.shift(3) => [0, 1, 2] 2.1.1 :003 > a => [3, 4, 5, 6, 7, 8, 9, 10]
takatoh's blog – Learning programming languages.
やばい。今日書かないと2月は1日も書いてないことになってしまう。
というわけで今日知った小ネタ。
なんか 1.8.7 の頃からできるようなんだけど今頃知った。
2.1.1 :001 > a = (0..10).to_a => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 2.1.1 :002 > a.shift(3) => [0, 1, 2] 2.1.1 :003 > a => [3, 4, 5, 6, 7, 8, 9, 10]
今作っている Rails アプリを試しに production 環境で動かしてみたところ、css などのアセットが読み込まれない、という事態に遭遇した。ちゃんと rake assets:precompile してあるにも関わらず、だ。
いろいろとググっくてみると、次のページで答えを見つけた。
cf. Rails3 css、js 404 Not Found! 静的ファイルが読み込まれない! – bismar’s blog
config/environments/production.rb の次の行が原因らしい。
# Disable Rails's static asset server (Apache or nginx will already do this). config.serve_static_assets = false
config.serve_static_assets の値が false になっている。コメントにもあるけど要するに、production 環境では Apache や Nginx が静的ファイルを配信するだろうから Rails 自身は配信しない、ということらしい。
ともあれ、これを true に変更することで無事 css が読み込まれるようになった。
一昨日の続き。
IP アドレスから国名コードを調べることはできた。でも、JP とか US くらいならわかるけど、ほかは国名コードを出力されても、結局どこの国なんだかわからない。例えばおとといの cidr.txt の最後はこうなっている。
takatoh@nightschool $ tail cidr.txt ZW 196.43.96.0/19 ZW 196.43.199.0/24 ZW 196.44.176.0/20 ZW 196.201.1.0/24 ZW 196.220.96.0/19 ZW 196.223.26.0/24 ZW 197.157.204.0/22 ZW 197.214.120.0/21 ZW 197.221.224.0/19 ZW 197.231.212.0/22
ZW ってどこの国だ?
国名コードの一覧は例えば Wikipedia にもあるので、それを調べれば ZW がどこの国かはわかる。けど、やりたいのはそんなことじゃなくて、コマンド一発で国名を出力して欲しいんだ。
まずは国名コードと国名を対応付けるデータベースが必要だ。データベースと言ってもそんな大げさなものじゃなくて CSV ファイルでいいんだけども。とはいえ、Wikipedia のページでは表になってるので、それをスクレイピングするなんてめんどくさくてやってられない。ところが、「国名コード CSV」でググったら、あるじゃないか、CSV。Qiita で公開してくれている人がいた。ありがとう、いい人だ。
cf. 国コード一覧CSV ISO 3166-1 – Qiita
というわけで、ここのページをエディタにコピペして、country_code.csv ファイルに保存した。
データさえ準備できれば、あとは検索するスクリプトさえ書けばいい。こんなの大して考えるまでもない。ちょちょいと書いた。
# coding: utf-8 require 'csv' def main cc_table = load_country_code("country_code.csv") cc = ARGV.shift puts cc_table[cc] end def load_country_code(csv) table = {} File.open("country_code.csv", "r") do |f| f.gets f.each do |line| fields = line.parse_csv table[fields[4]] = fields[0] end end table end main
country_code.csv がスクリプトを実行するディレクトリにあることを前提としている。
さて、試してみよう。
takatoh@nightschool $ ruby cc_to_country.rb JP 日本 takatoh@nightschool $ ruby cc_to_country.rb ZW ジンバブエ
というわけで、ZW はジンバブエでした。
昨日の延長上の話。ある IP アドレスが、どのサブネットに属しているかがわかれば、あとはそのサブネットがどの国に割り当てられたものかがわかればいい。
何はともあれ、IP アドレスの国別割り当て状況がわからないと話にならない。でも、世の中良くしたもんで、データを公開してくれている人がいた。ありがたい。
このページから、cidr.txt をダウンロードした。
CIDR ってのは Classless Inter-Domain Routing の略で、サブネットマスクを使って IP アドレスの割り当てとルーティングをする仕組みのこと。また、例えば 192.168.1.0/24 というサブネットの表記を CIDR表記というようだ。簡単な説明はここ。
cidr.txt は国名コードと割り当てられているサブネットの CIDR表記の一覧になっている。冒頭の10行はこうなっている。
AD 85.94.160.0/19 AD 91.187.64.0/19 AD 109.111.96.0/19 AD 185.4.52.0/22 AD 194.158.64.0/19 AE 2.48.0.0/14 AE 5.30.0.0/15 AE 5.32.0.0/17 AE 5.38.0.0/17 AE 5.53.96.0/21
国名コードと CIDR表記のサブネットはタブ文字で区切られている。
ちなみに AD はアンドラ、AE はアラブ首長国連邦だそうだ。
cidr.txt を読み込んでハッシュに保存しておき、入力された IP アドレスが属するサブネットを探して、見つかったらその国名コードを出力する。cidr.txt が実行ディレクトリにおいてあるのを前提としている。
# coding: utf-8 def main cidr = load_cidr("cidr.txt") addr = ARGV.shift cidr.each do |subnet, country| if in_subnet?(addr, subnet) puts country break end end end def load_cidr(file) cidr = {} File.open(file) do |f| f.each do |line| co, subnet = line.chomp.split("\t") cidr[subnet] = co end end cidr end def ip_to_b(addr) addr.split(".").map{|x| sprintf("%08d", x.to_i.to_s(2).to_i)}.join("") end def in_subnet?(addr, subnet) addr_b = ip_to_b(addr) subnet_addr, mask = subnet.split("/") subnet_addr_b = ip_to_b(subnet_addr) mask = mask.to_i addr_b.slice(0...mask) == subnet_addr_b.slice(0...mask) end main
IP アドレスの属するサブネットの検索が力技だけど、まあいいか。
実行結果。
takatoh@nightschool $ ruby -rresolv -e 'puts Resolv.getaddress("blog.panicblanket.com")' 210.224.185.170 takatoh@nightschool $ ruby addr_to_co.rb 210.224.185.170 JP
というわけで、このブログの IP アドレスは日本のものだってことが分かった。
同じやり方。
# coding: utf-8 import sys def main(): cidr = load_cidr("cidr.txt") addr = sys.argv[1] for subnet, co in cidr.items(): if is_in_subnet(addr, subnet): print co break def load_cidr(file): cidr = {} f = open(file) for line in f: co, subnet = line.rstrip().split("\t") cidr[subnet] = co return cidr def ip_to_b(addr): return "".join(map(lambda x: "%08d" % int(bin(int(x))[2:]), addr.split("."))) def is_in_subnet(addr, subnet): addr_b = ip_to_b(addr) subnet_addr, mask = subnet.split("/") subnet_addr_b = ip_to_b(subnet_addr) mask = int(mask) return addr_b[:mask] == subnet_addr_b[:mask] main()
takatoh@nightschool $ python addr_to_co.py 210.224.185.170 JP
# coding: utf-8 def ip_to_b(addr) addr.split(".").map{|x| sprintf("%08d", x.to_i.to_s(2).to_i)}.join("") end def in_subnet?(addr, subnet) addr_b = ip_to_b(addr) subnet_addr, mask = subnet.split("/") subnet_addr_b = ip_to_b(subnet_addr) mask = mask.to_i addr_b.slice(0...mask) == subnet_addr_b.slice(0...mask) end addr = "192.168.1.5" subnet = "192.168.1.0/24" puts in_subnet?(addr, subnet) addr2 = "127.0.0.1" puts in_subnet?(addr2, subnet)
takatoh@nightschool $ ruby is_in_subnet.rb true false
ちょっとトリッキーなのは、ip_to_b メソッドの sprintf のところ。頭に 0 をつけて8桁にするために、2進数(の文字列)にしたものを1と0だけからなる10進数だとみなして、%08d というフォーマットに変換している。これで IPアドレスの . で区切られた4つの部分のそれぞれが8桁(言い換えると8ビット)になる。
結果は上に示したとおり。192.18.1.5 はサブネット 192.168.1.0/24 に含まれるけど、127.0.0.1 は含まれない。
やってることは Ruby 版と同じ。
# coding: utf-8 def ip_to_b(addr): return "".join(map(lambda x: "%08d" % int(bin(int(x))[2:]), addr.split("."))) def is_in_subnet(addr, subnet): addr_b = ip_to_b(addr) subnet_addr, mask = subnet.split("/") subnet_addr_b = ip_to_b(subnet_addr) mask = int(mask) return addr_b[:mask] == subnet_addr_b[:mask] addr = "192.168.1.5" subnet = "192.168.1.0/25" print is_in_subnet(addr, subnet) addr2 = "127.0.0.1" print is_in_subnet(addr2, subnet)
当然結果も同じ。
takatoh@nightschool $ python is_in_subnet.py True False
Ruby では Fixnum#to_s メソッドに 2 を引数として与えると、2進数表現の文字列になる。
こんな機能あったんだ、知らなかった。
2.1.1 :001 > 5.to_s => "5" 2.1.1 :002 > 5.to_s(2) => "101"
引数は 2 じゃなくてもいい。整数 n を与えると n を基数とした数値表現になる。
2.1.1 :003 > 5.to_s(10) => "5" 2.1.1 :004 > 15.to_s(16) => "f" 2.1.1 :005 > 36.to_s(36) => "10" 2.1.1 :006 > 36.to_s(37) ArgumentError: invalid radix 37 from (irb):6:in `to_s' from (irb):6 from /home/takatoh/.rvm/rubies/ruby-2.1.1/bin/irb:11:in `<main>'
基数36までは OK で、37になるとダメみたいだ。
一方、Python では2進数に変換する bin 関数がある。
>>> bin(5) '0b101'
頭に 0b がついてしまうので、とってやる。
>>> bin(5)[2:] '101'
ちなみに、8進数にするには oct、16進数にするには hex 関数がある。
>>> oct(15) '017' >>> hex(15) '0xf'
いろいろ調べたけど、解決法はあっさり簡単だったのでメモしておく。
前提としては、データベースへの保存は UTC、アプリでの入出力はローカルタイムで行う。
簡単なサンプルアプリを作る。
takatoh@nightschool $ rails new sampleapp takatoh@nightschool $ cd sampleapp takatoh@nightschool $ rails g scaffold items name:string release:datetime takatoh@nightschool $ rake db:migrate
サーバを起動して http://localhost:3000/items にアクセスすると下のような画面になる。
試しに一つ、データを入力してみよう。New Item リンクをクリックして新規データ入力のページに移動。
今、日本ではは午前10時51分だ。時刻が UTC で表示されているのがわかる。
データをひとつ入力してみたあとが↓これ。時刻は UTC で表示されている。
試しに、コンソールで確認してみよう。
takatoh@nightschool $ rails c Loading development environment (Rails 4.1.4) 2.1.1 :001 > Item.find(1) Item Load (0.1ms) SELECT "items".* FROM "items" WHERE "items"."id" = ? LIMIT 1 [["id", 1]] => #<Item id: 1, name: "Sampe item 1", release: "2014-11-22 01:51:00", created_at: "2014-11-22 01:51:30", updated_at: "2014-11-22 01:51:30">
release や created_at、 updated_at の値が 01:51:30 になっている。UTC とは書いていないけど、時刻からデータベースにも UTC で保存されているのがわかる。
config/application.rb に次の行を追加する。サンプルがコメントアウトされているので、その次辺りに入れればいいだろう。
config.time_zone = 'Asia/Tokyo'
書き加えたら、サーバを起動しなおして、itemsページにアクセスしてみよう。
今度は時刻が日本時間(+0900)で表示された!
じゃあ、新しいデータを入力するときはどうか。
ここも日本時間になっている。
コンソールで確かめてみよう。
Loading development environment (Rails 4.1.4) 2.1.1 :001 > item = Item.find(2) Item Load (0.1ms) SELECT "items".* FROM "items" WHERE "items"."id" = ? LIMIT 1 [["id", 2]] => #<Item id: 2, name: "SAmple item 2", release: "2014-11-22 02:07:00", created_at: "2014-11-22 02:08:15", updated_at: "2014-11-22 02:08:15"> 2.1.1 :002 > item => #<Item id: 2, name: "SAmple item 2", release: "2014-11-22 02:07:00", created_at: "2014-11-22 02:08:15", updated_at: "2014-11-22 02:08:15">
release や created_at、updated_at の時刻は 02:08:15 になっている。データベースには相変わらず UTC で保存されているのがわかる。
ちなみに、取得した Item クラスのインスタンス item からは日本時間に変換された時刻が返ってくる。
2.1.1 :003 > item.release => Sat, 22 Nov 2014 11:07:00 JST +09:00
Rails 4 をローカル時間に合わせるには、config/application.rb に1行追記するだけでいい。あとは Rails が全てうまくやってくれる。
タイトル長いな。
Time.new の7つ目の引数には、UTC からのオフセットを表す文字列を指定できる。こんなふうに。
2.1.1 :001 > Time.new(2014, 11, 20, 21, 30, 0, "-06:00") => 2014-11-20 21:30:00 -0600
上の、”-06:00″が UTC からのオフセットを表す文字列だ。これをタイムゾーンの名前から得たい、というのが今回の趣旨。また ActiveSupport を使って書いた。
require 'active_support/time' def utc_offset_str(timezone) offset = Time.now.in_time_zone(timezone).utc_offset h = offset / (60 * 60) m = (offset - h * 60 * 60) / 60 f = h > 0 ? '+' : '-' sprintf("%s%02d:%02d", f, h.abs, m) end ['Asia/Tokyo', 'America/Chicago'].each do |zone| puts zone puts utc_offset_str(zone) end
Time#utc_offset で UTC からのオフセット(秒)が得られるので、それを使って目的の文字列を作っている。実行結果は次の通り。それにしてもベタなコードだなぁ。
takatoh@nightschool $ ruby utc_offset_str.rb Asia/Tokyo +09:00 America/Chicago -06:00
おまけ。タイムゾーン名の一覧は↓ここで発見した。
cf. タイムゾーンの一覧
さらにおまけ。
ActiveSupport の in_time_zone って ‘Asia/Tokyo’ じゃなくてただの ‘Tokyo’ でも受け付けるくせに ‘JST’ だとエラーになる。
2.1.1 :001 > require 'active_support/time' => true 2.1.1 :002 > Time.now.in_time_zone('Tokyo') => Thu, 20 Nov 2014 21:44:24 JST +09:00 2.1.1 :003 > Time.now.in_time_zone('JST') ArgumentError: Invalid Timezone: JST from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/time/zones.rb:71:in `rescue in find_zone!' from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/time/zones.rb:55:in `find_zone!' from /home/takatoh/.rvm/gems/ruby-2.1.1/gems/activesupport-4.1.4/lib/active_support/core_ext/date_and_time/zones.rb:20:in `in_time_zone' from (irb):3 from /home/takatoh/.rvm/rubies/ruby-2.1.1/bin/irb:11:in `<main>'
タイトルわかりにくいな。要するにこういうこと。
2.1.1 :001 > require 'time' => true 2.1.1 :002 > now = Time.now => 2014-11-19 22:27:36 +0900 2.1.1 :003 > now2 = Time.parse(now.to_s) => 2014-11-19 22:27:36 +0900
Time.now して作ったインスタンス now をいったん文字列に変換して、Time.parse したのが now2。当然同じ時刻を示している。
2.1.1 :004 > now => 2014-11-19 22:27:36 +0900 2.1.1 :005 > now2 => 2014-11-19 22:27:36 +0900
と思いきや、== で比較してみると false が返ってくる。
2.1.1 :006 > now == now2 => false
どういうことかと、うーんとなった。
で、ググってみると、同じことしてる人がいるもんだね。
cf. 「Ruby」Timeクラスの比較 – タナカイチロウの日記
どうやら、Ruby の Time クラスは時刻を浮動小数点で持っていて、上のような操作をしてしまうと、厳密に同じ時刻を再現できないらしい。
2.1.1 :007 > now.to_f => 1416403656.075245 2.1.1 :008 > now2.to_f => 1416403656.0
なるほどね〜。こりゃ、ハマりどころだわ。
ちなみに、Time.new で作ったインスタンスだとちゃんと true が返ってくる。
2.1.1 :009 > t = Time.new(2014, 11, 19, 22, 30, 0) => 2014-11-19 22:30:00 +0900 2.1.1 :010 > t2 = Time.parse(t.to_s) => 2014-11-19 22:30:00 +0900 2.1.1 :011 > t => 2014-11-19 22:30:00 +0900 2.1.1 :012 > t2 => 2014-11-19 22:30:00 +0900 2.1.1 :013 > t == t2 => true
タイトルわかりにくいな。要するに、例えばシカゴ時間の2014年11月17日午前7時を表すオブジェクトがほしいってこと。
ActiveSupport の in_time_zone と Time#utc_offset を使って作った。
# encoding: utf-8 require 'active_support/time' def time_in_zone(timezone, year, month, day, hour = 0, min = 0, sec = 0) t = Time.new(year, month, day, hour, min, sec).utc now = Time.now offset1 = now.utc_offset offset2 = now.in_time_zone(timezone).utc_offset (t + offset1 - offset2).in_time_zone(timezone) end puts time_in_zone('America/Chicago', 2014, 11, 17, 7, 0, 0) puts time_in_zone('Asia/Tokyo', 2014, 11, 17, 7, 0, 0)
takatoh@nightschool $ ruby time_in_zone.rb 2014-11-17 07:00:00 -0600 2014-11-17 07:00:00 +0900
なんかオフセットを足したり引いたりしてるところがややこしくて時間がかかった。もっとスマートな方法はないもんかな。
Time.new の7つ目の引数に、UTC からのオフセットを表す文字列を与えてやると、同じようなことができる。
2.1.1 :001 > Time.new(2014, 11, 17, 7, 0, 0, "-06:00") => 2014-11-17 07:00:00 -0600
問題は、オフセット文字列じゃなくてタイムゾーンの名前から作りたいってことだ。どうやったらいいだろう?