読者です 読者をやめる 読者になる 読者になる

けちゃぶろぐ

Golang とか Ruby とか Vim とか……。

Go で簡単なクローラーを書いてみた

Golang

最近、会社で非同期処理が辛い的な話をよくします。

個人的には Go を推しているので、せっかくだから簡単なクローラーを Go で書いてみました。

作ったやつ

  • 起点となるURLを指定する。
  • 取得した HTML の a タグで指定してあるURLを次に参照する。
  • 同じURLは一度だけ参照。
  • Webページの取得は非同期に。
  • クロールする深さを指定できる

Webクローラー

解説

goroutine の制御

基本的に、メインの goroutine (main 関数) と、Webページにアクセスしてスクレイピングする goroutine (Crowl 関数) の2種類の goroutine でやり取りを行います。

goroutine の間は、3種類のチャンネルを使用して連携しています。

リクエスト

リクエストは、リクエストチャンネルに追加します。

メインの goroutine でリクエストをチャンネルから取り出し、Crawl 関数を goroutine として実行しています。

この時、クロールの深さと URL を参照したかどうかのチェックを行っています。

クロール処理

func Crawl(url string, depth int, ch *Channels) {
    defer func() { ch.quit <- 0 }()

    // WebページからURLを取得
    urls, err := Fetch(url)

    // 結果送信
    ch.res <- Result{
        url: url,
        err: err,
    }

    if err == nil {
        for _, url := range urls {
            // 新しいリクエスト送信
            ch.req <- Request{
                url:   url,
                depth: depth - 1,
            }
        }
    }
}

まず、指定されたURLのWebページを取得し、その中のURLを取得します。

取得した結果(成功 or エラー)を、結果用のチャンネルに追加します。

また、Webページ内のURLが取得出来た場合は、新しいリクエストとしてそれぞれチャンネルに追加します。 ここで、クロールの深さをデクリメントします。

終了時に、処理終了の通知をチャンネルに追加します。

Web ページの取得とスクレイピング

func Fetch(u string) (urls []string, err error) {
    baseUrl, err := url.Parse(u)
    if err != nil {
        return
    }

    resp, err := http.Get(baseUrl.String())
    if err != nil {
        return
    }
    defer resp.Body.Close()

    doc, err := goquery.NewDocumentFromReader(resp.Body)
    if err != nil {
        return
    }

    urls = make([]string, 0)
    doc.Find("a").Each(func(_ int, s *goquery.Selection) {
        href, exists := s.Attr("href")
        if exists {
            reqUrl, err := baseUrl.Parse(href)
            if err == nil {
                urls = append(urls, reqUrl.String())
            }
        }
    })

    return
}

スクレイピングは、goquery を使ってみました。jQuery 使うような感覚で簡単に要素を抽出できて便利です。

goquery

チャンネルの制御

リクエスト、結果、終了通知の3つのチャンネルは、メインの goroutine で制御しています。

   // ワーカーの数
    wc := 0

    done := false
    for !done {
        select {
        case res := <-chs.res:
            if res.err == nil {
                fmt.Printf("Success %s\n", res.url)
            } else {
                fmt.Fprintf(os.Stderr, "Error %s\n%v\n", res.url, res.err)
            }
        case req := <-chs.req:
            if req.depth == 0 {
                break
            }

            if urlMap[req.url] {
                // 取得済み
                break
            }
            urlMap[req.url] = true

            wc++
            go Crawl(req.url, req.depth, chs)
        case <-chs.quit:
            wc--
            if wc == 0 {
                done = true
            }
        }
    }

また、リクエストを取得して goroutine を実行する際にワーカー数をインクリメント、結果通知を受け取った場合にワーカー数をデクリメントしています。

ワーカー数が 0 になったら、終了です。

Ruby 2.3.0-preview1 がリリースされたので新機能を試してみた

Ruby

ちょっと前に、Ruby 2.3.0 の最初のプレビュー版である、Ruby 2.3.0-preview1 がリリースされました

新機能が追加されているようなので、NEWS に書いてあることをかいつまんで、実際に試してみました。

Frozen String Literal Pragma

新しいマジックコメントまたはコマンドライン引数を指定することで、ソースコード中の全ての文字列リテラルを freeze するというもの。

Ruby 3 では全ての文字列リテラルが immutable (frozen) になるそうなので、2.3.0 では先行してそれを有効にできるようなったようです。

# string-literal-frozen.rb
a = "foo"
b = "foo"

puts a.object_id
puts b.object_id

従来通りだと、別のオブジェクトとなります。

$ ruby string-literal-frozen.rb
70124014993580
70124014993560

マジックコメントで

# frozen_string_literal: true

を指定すると、同じ文字列のリテラルは同じオブジェクトになります。

$ ruby string-literal-frozen.rb
70095964209440
70095964209440

マジックコメントをつけずに、コマンドライン引き数で --enable=frozen-string-literal を指定することもできます。

$ ruby --enable=frozen-string-literal string-literal-frozen.rb
70287689548120
70287689548120

freeze されているので、文字列を破壊的に変更するようなメソッド呼び出しは、当然エラーになります。

foo = "foo"
foo.upcase!
# can't modify frozen String (RuntimeError)

なので、必要に応じて、.dup を呼び出して複製するか、String.newインスタンスを生成する必要があります。

foo = "foo".dup
foo.upcase!

bar = String.new
bar << 'Ruby'

Safe navigation operator

メソッドをコールするときに &. というオペレーターを使う、新しいシンタックスが追加されています。

Rebuild.fm #118 で、「ぼっちオペレーター」とか言われてました。なるほど。

object&.foo のようにメソッド呼び出しをした際に、objectnil でなければメソッドがコールされますが、objectnil の場合は nil になります。

これまでだと、

buz = nil
if foo != nil && foo.bar != nil
  buz = foo.bar.buz
end

みたいに書かないといけなかったのが、

buz = foo&.bar&.buz

だけでよくなります。便利ですね。

Array#dig, Hash#dig

ArrayHash に、dig というメソッドが追加されました。

ArrayHash がネストされている場合に、深い階層にある要素を取り出すのに使います。

a = [[[1, 2, 3]]]
num = a.dig(0, 0, 1)
# => 2
num = a.dig(0, 1, 1)
# => nil

h = {foo: {bar: {buz: 'qux'}}}
str = h.dig(:foo, :bar, :buz)
# => "qux"
str = h.dig(:hoge, :bar, :buz)
# => nil

ぼっちオペレーターもそうでしたが、このメソッドを使うと、途中の階層の要素が存在するかどうかのチェックをしなくても良くなります。

Array#bsearch_index

配列の要素を二分探索するメソッドとして、Array#bsearch がありますが、このメソッドは戻り値として、見つかった要素を返します(見つからなかった場合は nil を返します)。

Array#bsearch_index は、要素を返すのではなくインデックスを返します。見つからなかった場合は nil を返します。

a = ['foo', 'bar', 'buz']

a.bsearch {|s| s =~ /b/}
# => "bar"

a.bsearch_index {|s| s =~ /b/}
# => 1

Enumerable#grep_v

もともと存在する Enumerable#grep は、パラメーターと要素を === で比較し、マッチした要素の配列を返しますが、新しく追加された Enumerable#grep_vマッチしなかった要素の配列を返します。つまり、grep の -v オプションです。Unix っぽいですね。

a = ['foo', 'bar', 'buz', 'qux']

a.grep(/b/)
# => ["bar", "buz"]

a.grep_v(/b/)
# => ["foo", "qux"]

Enumerable#chunk_while

Enumerable#slice_when は、隣り合う要素が前方から順に指定されたブロックに渡され、ブロック内で評価した結果が偽になるところでチャンクを切ります。メソッドは、チャンク分けされた要素を持つ Enumerator が戻り値として返します。

Enumerable#chunk_while は、既存の Enumerable#slice_when の逆バージョンです。

a = [1, 2, 4, 9, 10, 11, 12, 15, 16, 19, 20, 21]

a.slice_when {|i, j| i + 1 != j}.to_a
# => [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]

a.chunk_while {|i, j| i + 1 == j}.to_a
# => [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]

Hash#fetch_values

Hash#fetch_values は、パラメーターで複数のキーを指定して、各キーの値を配列で返します。

キーが存在しない場合は、ブロックで評価された値を使用します。ブロックを指定しなかった場合は KeyError になります。

Hash#values_atHash#fetch を合体させたみたいな感じです。

h = {a: 1, b: 2, c: 3}

h.fetch_values(:a, :c)
# => [1, 3]

h.fetch_values(:a, :d)
# => KeyError: key not found: :d

h.fetch_values(:a, :d) {|key| 0}
# => [1, 0]

Hash#<=, Hash#<, Hash#>=, Hash#>

2つのハッシュを比較する演算子が追加されました。

なかなか言葉で説明しづらい……。

{a: 1, b: 2} >= {a: 1}
# => true

{a: 1, b: 2} >= {a: 2}
# => false

{a: 1, b: 2} >= {a: 1, b:1}
# => false

{a: 1, b: 2} >= {a: 1, b:2}
# => true

{a: 1, b: 2} > {a: 1}
# => true

{a: 1, b: 2} > {a: 2}
# => false

{a: 1, b: 2} > {a: 1, b:2}
# => false

こんな感じです。

Hash#to_proc

Hash#to_proc は、ハッシュのキーをパラメーターとして受け取り、その値を返す Proc になります。存在しないキーの場合は nil が返ります。

これにより、ブロックを受け取るメソッドに、Hash を渡すことができるようになります。

h = {a: 1, b: 2, c: 3}
[:a, :b, :c].map(&h)
# => [1, 2, 3]

Numeric#positive?, Numeric#negative?

Numeric#positive? は数値が正の値なら true、それ以外なら false を返します。

Numeric#negative? は数値が負の値なら true、それ以外なら false を返します。

Unix プロセスと Docker の罠

Ruby Docker

プロセスの親子関係

Unix 系 OS では、プロセスに親子関係があります。

子プロセスを生成する方法として、fork が挙げられます。 fork は、プロセスのコピーを生成するシステムコールで、コピーされた新しいプロセスを子プロセス、fork の呼び出し元のプロセスを親プロセスと呼びます。

Ruby では Kernel.fork を呼び出すことで fork を実行できます。

if fork
  # 親プロセス
else
  # 子プロセス
end

または、

fork do
  # 子プロセス
  # ブロックの末尾で子プロセスは終了する
end

プロセスの子プロセスが更に子プロセスを生成して……というように、プロセスは家系図のように枝分かれしていきます。 プロセスには、自分を生み出した親プロセスが必ず存在します。

ps コマンドを使って、親プロセスを確認することができます。

$ ps -ho ppid,pid,comm

また、Ruby では、Process.ppid で親プロセスの PID を取得できます。

一番最初のプロセス

すべてのプロセスには親がいますが一つだけ例外があります。

すべてのプロセスの先祖となる init プロセスです。

init プロセスはカーネル起動時に生成され、PID は 1 になります。init の親プロセスは存在せず、親 PID として 0 が設定されます。

孤児プロセス

子プロセスよりも先に親プロセスが死んだとき、その子プロセスは道連れにはされません。

孤児となった子プロセスは init プロセスによって引き取られ、親が死んでも子は生き続けます。

子プロセスの結果を取得する

子プロセスが死んだとき、その終了した結果(終了コード)を親プロセスは知ることができます。

親プロセスはいつでも子プロセスを監視しているわけではないので、子プロセスが死んだ時、その情報は一旦キューに格納されます。

親プロセスがキューから子プロセスの終了情報を読取ると、キューはクリアされ、子プロセスの痕跡は消え去ります。

Ruby では Process.wait を使用すると、子プロセスの終了を待機することができます。 また、Process.wait2 を使用すると子プロセスの終了結果も取得できます。

fork do
  sleep 3
end

pid, status = Process.wait2
puts status.exitstatus

ゾンビプロセス

親プロセスが子プロセスの終了情報をキューから取り出さない限り、子プロセスの情報はシステムに残り続けます。

これがゾンビプロセスです。

ps コマンドで、ゾンビプロセスかどうか確認することができます。

$ ps -ho pid,stat,comm

ゾンビプロセスの場合、STAT に Z と表示されます。

ゾンビプロセスを残さないために、確実に Process.wait などを呼び出すか、Process.detach を呼び出して終了の監視を任せる必要があります。

Docker の罠

Docker のコンテナとして起動したプロセスにおいても、子プロセスを生成することができます。 子プロセスは孫プロセスを起動することができます。

親、子、孫の3プロセスが起動している状態で、親や子が孫より先に死んだ時、なにが起きるでしょうか?

Docker コンテナ内でない通常のプロセスであれば、親や子が死んでも、孫は init に引き取られ生き続けます。 ですが、Docker コンテナ内では異なる動きをします。

まず、親が死んだ場合。

よくある想定としては、docker stop コマンドを呼び出した時などで、親プロセスに SIGTERM が送信されます。 通常、SIGTERM を受け取った親プロセスは終了処理を実行して終了します。このとき、子プロセスを正しく管理しているのであれば、子プロセスを終了させるための処理を行うでしょう。子プロセスは孫プロセスを終了させるかもしれません。

しかし、子プロセスを終了させずに放置した場合、子プロセスや孫プロセスは問答無用で殺されます。 親が死んでも生き続ける子や孫を、Docker さんは認めません。

つぎに、親が死なずに子が死んだ場合。

子プロセスが孫を生成するだけ生成してとっとと死んだ場合などが考えられるでしょうか。 期待する動きとしては、孤児となった孫プロセスを、init プロセスが引き取る、と言った感じでしょうか。

しかし。 Docker コンテナ内では init が存在しません。なので Docker さんは、孫プロセスの引き取り手として、親プロセスを選びます。

これで安心……のように見えますが、ここに罠があります。 Docker さんが孫プロセスの引き取り手として親プロセスを選びましたが、その親プロセスはそのことを知らされません。

親プロセスが孫プロセスが生きてることをしらない状態で孫が死んだ時。孫はゾンビとなるのです。

ゾンビに安らかな眠りを与えるために

勝手に孫を養子にされた親は、孫の死を知ることができないのでしょうか?

実は、子が死んだ時、親プロセスは死亡通知を受け取ることができます。 その死亡通知は、SIGCHLD というシグナルとして送信されます。 このシグナルは、勝手に養子にされた孫が死んだ時にも送信されます。

つまり、親が死亡通知を受け取るための手続きをすることで、子供の最後を看取り、ゾンビ化させずに安らかなる眠りを与えることができるのです。

Ruby では以下のようにすると実現できます。

Signal.trap(:SIGCHLD) do
  begin
    while pid = Process.wait(-1, Process::WNOHANG)
      puts "Process #{pid} is dead."
    end
  rescue Errno::ECHILD
  end
end

不幸はゾンビを生まないために。 Docker コンテナ内でサブプロセスを起動する場合、これらのことに留意してアプリケーションを開発する必要があります。

サンプルコード

いろいろ試すためのサンプルコードを書いてみました。

docker-zombie

# ゾンビを生み出す
$ ruby parent.rb

# ゾンビを生み出さない
$ ruby parent.rb -c

# Docker Image ビルド
$ docker build -t docker-zombie .

# Docker コンテナー内でゾンビを生み出す
$ docker run -it --rm docker-zombie

# Docker コンテナー内でゾンビを生み出さない
$ docker run -it --rm docker-zombie -c

参考書籍

なるほどUnixプロセス ― Rubyで学ぶUnixの基礎

Ubuntu に rbenv で Ruby をインストールする

Ruby Ubuntu

グローバルな感じでインストールする方法。

いつも手順探すので覚書。

必要なパッケージをインストールします。

$ sudo apt-get install build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl git

rbenv 用のグループを作成し、自分のユーザー(下記ではketchup)をグループに追加します。

$ sudo groupadd rbenv
$ sudo gpasswd -a ketchup rbenv

rbenv を /usr/local にインストールします。

$ cd /usr/local
$ sudo git clone https://github.com/sstephenson/rbenv.git rbenv
$ sudo git clone https://github.com/sstephenson/ruby-build.git rbenv/plugins/ruby-build
$ sudo chgrp -R rbenv rbenv
$ sudo chmod -R g+rwxXs rbenv

rbenv の環境変数とか初期化するためのスクリプトを作成します。

$ sudo vi /etc/profile.d/rbenv.sh
export RBENV_ROOT="/usr/local/rbenv"
export PATH="/usr/local/rbenv/bin:$PATH"
eval "$(rbenv init -)"

sudo でも Ruby 使えるように設定します。

$ sudo visudo
Defaults        !secure_path
Defaults        env_keep = "RBENV_ROOT PATH"

あとは、rbenv で好きなバージョンの Ruby をインストールします。

$ rbenv install 2.1.2
$ rbenv global 2.1.2

OS X の zsh で Unicode の正規化がやっかいなので

zsh OS X

OS Xファイルシステムは HFS+ なんですが、こいつがファイル名を NFD で正規化した Unicode として保存しています。

これがやっかいで、zsh がファイル名を読み出すときに、NFD で正規化された状態で読み出しちゃって、濁音や半濁音を含むファイル名を表示したり補完したりする場合に、正しく動作してくれません。

zsh が読み出したファイル名を NFC で正規化してくれればいいんですが、安定版の最新バージョン 5.0.5 は未対応です。

ですが、zshリポジトリを見てみると、NFC で正規化されている修正が入っています。

というわけで、ファイル名を NFC で正規化してくれる部分だけリポジトリから取り出して、パッチを作ってみました。

zsh 5.0.5 を OS X の NFD に対応させるパッチ

いつも Homebrew で zsh をインストールしているので、このパッチを適用するカスタム Formula もついでに作ってみました。

kechako/homebrew-zsh

brew tap で Formula リポジトリを追加すれば簡単にインストールできます。

brew tap kechako/zsh
brew install zsh-custom

OpenSSL で自己認証局とか自己証明書とか作る

OpenSSL

OpenSSLの脆弱性 で騒がしい今日この頃。

そんな中、会社でオレオレ証明書の話題が出たので、openssl でちょっと作ってみました。

自己認証局

自己認証局秘密鍵を作成します。

openssl genrsa -out my-ca.key 2048

自己認証局の証明書を作成します。

openssl req -x509 -new -key my-ca.key -out my-ca.cer -days 365 -subj /CN="My CA"

自己証明書

自己認証局から署名された、サーバー証明書を作成します。

まずは秘密鍵を作成します。

openssl genrsa -out server.key 2048

証明書署名要求(CSR)を作成します。

openssl req -new -out server.csr -key server.key -subj /CN="myhost"

myhost は、サーバーの FQDN を指定します。

-subj 以降を省略すると、対話的に情報を入力できます。

あとは、自己認証局で署名して、サーバー証明書を作成します。

openssl x509 -req -in server.csr -out server.cer -CAkey my-ca.key -CA my-ca.cer -days 365

あとは、Webサーバーに、サーバー証明書秘密鍵サーバー証明書を指定したりして遊びます。

zsh はじめました。

OS X zsh

以前からちょっと気になっていた zsh

これまで bash でやってきたけど、ちょっくら移行してみることにしました。

まずはインストール

OS X はデフォルトで zsh が入ってるみたいです。Mavericks だと 5.0.2 です。

とりあえずこいつは無視して Homebrew でインストールします。

「だって新しいものが好きだからー!」

$ brew install zsh

ちなみに、Homebrew だと 5.0.5 が入りました。

ついでに、zsh-completions も入れときます。

$ brew install zsh-completions
続きを読む