ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

open-uri の close とか

rubyopen-uriopen で、サイトをスクレイピングするプログラムを見かけたのだけれど、「close 呼ばなくていいの?」と思って色々調べたのでメモしておきます。結論としては、ブロック渡した時は呼ばなくていいけど基本的には呼ぶ。

調べたのは ruby 2.3 なので古くなるかもです、がそんなに変わらなそうな部分?

ブロック渡した時は呼ばなくていい

open メソッドの実装を見てみます。

> require 'open-uri'
=> true
> method(:open).source_location
=> ["/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/open-uri.rb", 29]

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L29-L39

  def open(name, *rest, &block) # :doc:
    if name.respond_to?(:open)
      name.open(*rest, &block)
    elsif name.respond_to?(:to_str) &&
          %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name &&
          (uri = URI.parse(name)).respond_to?(:open)
      uri.open(*rest, &block)
    else
      open_uri_original_open(name, *rest, &block)
    end
  end

今回は引数として url 文字列の見渡すので、引数 name は URL として解釈され、URI#open が呼ばれるみたいです。そこだけ切り出して実行してみます。

> (uri = URI.parse('https://ushumpei.hatenablog.com')).respond_to?(:open)
=> true
> uri
=> #<URI::HTTPS https://ushumpei.hatenablog.com>
> uri.method(:open).source_location
=> ["/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/open-uri.rb", 716]

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L716-L718

OpenURI::OpenRead#open が呼ばれるみたい。その中から OpenURI.open_uri が呼ばれている。

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L132-L166 (抜粋)

    if block_given?
      begin
        yield io
      ensure
        if io.respond_to? :close!
          io.close! # Tempfile
        else
          io.close if !io.closed?
        end
      end
    else
      io
    end

153 行目で block_given? によりブロックが与えられていた時は close を呼ぶように実装されている (ローンパターンか)

なのでブロック渡せば close は明示的に呼ばなくてもいいことがわかりました。

気になったコード

html = open('https://ushumpei.hatenablog.com').read

え、これ絶対 close されないのでは?と思ったので lsof で開きっぱなしなことを確認しました。確認コマンドとその意味は以下の感じになります。

$ lsof -c ruby | grep open-uri | wc -l
  • ruby が開いているファイルを見たいので -c ruby でコマンド名を指定しました。
  • 一時ファイル名に open-uri が付いていたのでそれで絞り込み
  • 行数取得

該当コード実行前の lsof -c ruby | grep open-uri | wc -l0

該当コード実行 (irb は開きっぱなし)

> require 'open-uri'
=> true
> 10.times { |n| open('https://ushumpei.hatenablog.com').read }
=> 10

該当コード実行後の lsof -c ruby | grep open-uri | wc -l10irb を終了するとファイル数は 0 に戻りました。

ブロック渡した時は irb 開きっぱなしの状態でもファイル数は 0 のままです。

(注意: open-uriopen した時に、lsofopen-uri がひっついていている行が出力される、という事象は、ソースコードレベルでは確認しきれていません。ちょっとお粗末になり申し訳ないですが、「多分 open 時に作成された開かれているファイルの行だ」という予想に基づいている、と断っておかなければいけません)

まとめ

  • open(XXX).readopen(XXX) { |f| f.read } とかに変えよう
  • lsof って list open files なんですね。ポート使っているプロセス調べるコマンドかと思っていた。
  • というか linux が全てをファイルという形で抽象化しているアレのアレなのか。(よく知らない)
  • そういえば open って |ls とかでパイプつけたコマンド渡すと実行できたりしてアレらしいので、OpenURI.open_uri 呼んじゃった方がいいっぽい。

追記 2019/04/07

  • 可読性微妙かもだけど、 open(XXX).read の書き換え open(XXX, &:read) もいけるのか、 ruby すごい