SHOYAN BLOG

I am a pragmatic programmer.

Webサイトのメタタグを取得するsite-parserをつくった

Webサイトのメタデータを抽出するツールを作成しました。
https://github.com/shoyan/site-parser

使い方

  1. git clone git@github.com:shoyan/site-parser.git
  2. cd site-parser
  3. bundle install
  4. site.csv をテキストエディタで編集
  5. ruby site-parser.rb

site.csv にパースしたいURLを記入して ruby site-parse.rb とすればメタデータが表示されます。

1
2
3
4
5
6
7
8
9
10
11
$ ruby site-parser.rb
"http://www.yahoo.co.jp/"
[
    [0] {
                             "url" => "http://www.yahoo.co.jp/",
                           "title" => "Yahoo! JAPAN",
                     "description" => "日本最大級のポータルサイト。検索、オークション、ニュース、メール、コミュニティ、ショッピング、など80以上のサービスを展開。あなたの生活をより豊かにする「ライフ・エンジ ン」を目指していきます。",
                          "robots" => "noodp",
        "google-site-verification" => "fsLMOiigp5fIpCDMEVodQnQC7jIY1K3UXW5QkQcBmVs"
    }
]

また、サーバーを起動してAPIとして利用することもできます。

サーバーを起動

1
2
3
4
5
$ ruby server.rb
== Sinatra (v1.4.7) has taken the stage on 4567 for development with backup from Thin
Thin web server (v1.6.4 codename Gob Bluth)
Maximum connections set to 1024
Listening on 0.0.0.0:4567, CTRL+C to stop

APIを実行

1
2
$ curl http://localhost:4567 -X POST -d "url=http://www.yahoo.co.jp/"
=> {"url":"http://www.yahoo.co.jp/","title":"Yahoo! JAPAN","description":"日本最大級のポータルサイト。検索、オークション、ニュース、メール、コミュニティ、ショッピング、など80以上のサービスを展開。あなたの生活をより豊かにする「ライフ・エンジン」を目指していきます。","robots":"noodp","google-site-verification":"fsLMOiigp5fIpCDMEVodQnQC7jIY1K3UXW5QkQcBmVs"}

その他

内部的にはNokogiriを使ってhtmlをパースしています。

文字化けして内容がみれない場合があったので、以下のワークアラウンドをいれました。

1
2
html = URI.parse(url).read
node = Nokogiri::HTML(html.toutf8, nil, 'utf-8')

参考資料

以下が参考になりました。ありがとうございます。

Sinatraのレスポンスの設定とストリーミングヘルパー

通常はルーティングブロックの戻り値にセットした文字列がbodyにセットされます。
任意の評価フローでbodyをセットしたい場合はどうすればいいでしょうか。
Sinatraには任意の評価フローでbodyをセットできる、bodyヘルパーが用意されています。

1
2
3
4
5
6
7
get '/foo' do
  body "bar"
end

after do
  puts body
end

Status Codeを設定するstatusヘルパー、headerを設定するheadersヘルパーも用意されています。

1
2
3
4
5
6
7
get '/foo' do
  status 418
  headers \
    "Allow"   => "BREW, POST, GET, PROPFIND, WHEN",
    "Refresh" => "Refresh: 20; http://www.ietf.org/rfc/rfc2324.txt"
  body "I'm a tea pot!"
end

引数の伴わないbody, status, headersは現在の値を確認するために使えます。

Streamingヘルパー

streamヘルパーを使うとおもしろいことができます。
レスポンスボディの部分を生成している段階でデータを送信することができます。
この仕組みを使ってストリーミングAPIを実装することもできます。

1
2
3
4
5
6
7
8
9
get '/' do
  stream do |out|
    out << "It's gonna be legen -\n"
    sleep 0.5
    out << " (wait for it) \n"
    sleep 1
    out << "- dary!\n"
  end
end

以下は、ストリーミングAPIのサンプルです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# app.rb
# long polling

set :server, :thin
connections = []

get '/subscribe' do
  # register a client's interest in server events
  stream(:keep_open) do |out|
    connections << out
    # purge dead connections
    connections.reject!(&:closed?)
  end
end

post '/message' do
  connections.each do |out|
    # notify client that a new message has arrived
    out << params['message'] << "\n"

    # indicate client to connect again
    out.close
  end

  # acknowledge
  "message received"
end

ストリーミングはThinやRainbowsなどのイベント型サーバーでないと動かないので注意が必要です。

サーバーを起動させてみます。

1
2
$ gem insatall thin
$ ruby app.rb

別のターミナルを開いて、以下を実行します。

1
$ curl http://localhost:4567/subscribe

何もレスポンスがありませんが、接続が維持された状態となります。

別のタブを開いて以下の実行してみましょう。

1
$ curl http://localhost:4567/message -X POST -d "message=hello"

すると、subscribeのほうにhelloと表示されて接続が切れます。
これは、/messageのout.closeを実行しているからです。
試しに、out.closeをコメントアウトしてサーバーを起動すると接続維持されたままになります。

Rubyでhttp通信をするhttp Gem

rubyでhttp通信をする際に便利なhttp gemというものがあるので紹介します。
https://github.com/httprb/http

Install

1
$ gem install http

getしてみる

1
2
3
4
5
6
7
$ pry
$ require http'
$ response = HTTP.get("https://github.com")
$ response.status
=> 200
$ response.body.to_s
=> "<html><head><meta http-equiv=\"content-type\" content=..."

http メソッドを指定する

以下のように指定します。

1
2
3
4
5
>> response = HTTP.post('https://restapi.com/objects')
>> response = HTTP.put('https://restapi.com/put')
>> response = HTTP.patch('https://restapi.com/put')
>> response = HTTP.delete('https://restapi.com/put')
>> response = HTTP.head('https://restapi.com/put')

クエリストリングを指定する

以下のように paramsオプションで指定します。

1
2
3
4
5
6
$ get = HTTP.get("http://example.com/resource", :params => {:foo => "bar"})
=> "#<HTTP::Response/1.1 404 Not …"

# 以下のように生成されたuriを確認できます
$ get.uri
=> #<HTTP::URI:0x007fce62a33aa8 URI:http://example.com/resource?foo=bar>

bodyを指定する

formオプションに設定します。
URLエンコードされたパラメーターが設定されます。

1
HTTP.post("http://example.com/resource", :form => {:foo => "42"})

生データを渡したいときは、bodyオプションを使います。

1
HTTP.post("http://example.com/resource", :body => "foo=42&bar=baz")

ネストしたbodyの渡し方

ネストした配列を渡したい場合はjsonを使って以下のように指定できます。

1
HTTP.put("http://example.com/resource", :params => {foo: "A"}, json: { bar: { hoge: "ok!"} })

Basic認証に対応する

以下のように設定ができます。

1
http = HTTP.basic_auth(user: 'user_name', pass: 'password')

SSL証明書の検証を無効にする

開発環境の場合などでSSL証明書の検証を無視したい場合は以下のように設定できます。

1
2
3
ctx = OpenSSL::SSL::SSLContext.new
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
HTTP.get("https://example.com", :ssl_context => ctx)

NAT・NAPT・ポートフォワーディングの違いとは

ネットワークの話しによくNATという言葉が出てくるので整理した。
また、NAPT・ポートフォワーディングについても理解しておいたほうが理解が深まると思ったのでまとめておいた。

NATとは

NAT(Network Address Translation)はIPアドレスを変換する技術である。
具体的にはプライベートIPアドレスをグローバルIPアドレスに変換する、もしくは、グローバルIPアドレスをプライベートIPアドレスに変換する。

なぜNATが必要か

グローバルIPアドレスを節約するためである。
インターネットに接続するにはグローバルIPアドレスが必要である。
プライベートIPアドレスではインターネットに接続することができないという決まりになっている。

インターネットに接続するには、PC全てにグローバルIPを割り振るのが最も単純な方法である。
しかし、全てにグローバルIPアドレスを割り振っていてはすぐにIPアドレスは枯渇してしまう。

では、どうするか。
LANからインターネットに通信するときにプライベートIPアドレスをグローバルIPアドレスに変換してやればいいのではないか。
それを実現するのがNATである。

具体的にはLANからインターネットに通信するときは、ヘッダの送信元IPをグローバルIPに書き換える。
インターネットからLANに通信するときは、ヘッダの宛先IPをプライベートIPに書き換える。

NATがやることはこれだけである(多分)。

しかし、NATには大きな問題がある。
複数のクライアントPCがある場合、NATはどのプライベートIPに変換すればいいかわからないのだ。

その問題を解決するのが、NAPT(Network Address Port Translation)である。

NAPTとは

複数のクライアントPCがある場合、NATはどのプライベートIPに変換すればいいかわからない。
その問題を解決するのがNAPTである。

NAPTはIPアドレスに加えてポートも管理する。
このポートを使ってクライアントPCを特定する。
例えば、PC(A)の場合は通信のポート番号を100、PC(B)の場合は通信のポート番号を200とする。
すると、通信先のサーバーから異なったポート番号宛にデータが戻って来る。
ポート番号100に戻ってきたデータはPC(A)に戻してやればいいわけである。

NAPTはIPマスカレードとも呼ばれる。

NATの弊害

NATを使うとインターネットからLANへの通信ができないという問題がある。
インターネットからの通信の場合、どのクライアントPCのプライベートIPアドレスに変換すればいいかがわからないため、クライアントPCに通信が届かないのだ。
これは、セキュリティ的にみると安全だ。
NATの内側にいるクライアントPCには外部からの通信が届かないからである。

外部からの通信を届けたい場合はどうすればいいか。
ポートフォワーディングを使えばいい。

ポートフォワーディングとは

インターネットから特定のポート番号宛に通信が届いたときにあらかじめ設定しておいたLAN側の機器にパケットを転送する機能である。
例えば、宛先ポートが80宛のパケットは、LAN側の192.168.1.1の機器へ転送するといった設定ができる。
このようにあらかじめ転送するポートとクライアントPCのIPアドレスを登録しておくことで外部からの通信が行えるようになる。

ポートフォワーディングは静的IPマスカレードとも呼ばれる。

おすすめの書籍

ネットワークについては様々な書籍があるが私は以下の書籍をおすすめする。

ネットワークはなぜつながるのか

ネットワークの概要についてわかりやすく解説してある。初学者にお勧め。

マスタリングTCP/IP 入門編

ネットワークの理論についてわかりやすく解説してある。知人のネットワークの専門家もこの書籍をおすすめしていた。

参考文献

DockerコンテナにChefを流してみた

Chefのレシピを書くとき、Dockerコンテナにレシピを流せると気軽に確認ができてよいなと思い、DockerコンテナにChefを流せるようにしてみました。

リポジトリを作ったので参考にどうぞ。
https://github.com/shoyan/nginx-and-ruby-on-docker

Install

DockerとChefをインストールしている必要があるので、以下からインストールしてください。

Docker

Chef

DockerコンテナにChefを流す

まずは、Docker QuickStart TerminalをクリックしてDockerを起動します(アプリケーションにあります)。

Dockerを起動したらNginxとrubyをインストールするCookbookを実行します。

1
2
$ git clone https://github.com/shoyan/nginx-and-ruby-on-docker
$ cd nginx-and-ruby-on-docker

以下のコマンドでimageをbuildします。

1
$ docker build -t nginx-and-ruby .

imageを確認

1
2
3
$  docker images
REPOSITORY             TAG                            IMAGE ID            CREATED             SIZE
nginx-and-ruby         latest                         6b4d5602119f        36 seconds ago      382.2 MB

Dockerのコンテナを起動します。
image_idは docker imagesのIMAGE IDを指定します。
今回の場合は6b4d5602119fです(場合により変わります)。

1
$ docker run --privileged -d --name nginx-and-ruby image_id /sbin/init

–privilegedはCentOS7のimageでserviceコマンドを起動するおまじないです。
Dockerでsystemctlでserviceが起動できない

起動したDockerのコンテナにログインします。

1
$ docker exec -it nginx-and-ruby bash

ログインしたらDockerコンテナでChefを実行します。

1
# chef-client -z -j nodes/bootstrap.json -c client.rb

Dockerを使うとChefを実行→コンテナを消す→真っさらな状態からもう一度Chefを流すというサイクルが気軽にできるので便利です。

もう一度真っさらな状態のコンテナを作りたいときは、以下のようにします。

1
2
3
4
5
6
7
8
9
$ doker ps
CONTAINER ID        IMAGE                  COMMAND              CREATED             STATUS                        PORTS                                           NAMES
17a11c4b6ce8        6b4d5602119f           "/sbin/init"         2 hours ago         Exited (137) 12 seconds ago                                                   nginx-and-ruby

# Dockerコンテナを消す
$ docker rm -f 17a11c4b6ce8

# dockerコンテナを起動する
$ docker run --privileged -d --name nginx-and-ruby image_id /sbin/init

https://github.com/shoyan/nginx-and-ruby-on-docker