Ruby 画像ダウンロード編 (9) open について その 2 【計算数学 I】

更新日時:

open について (2)

さて、open に戻ってきました。以前学んだことを要約すると以下のようになります。

  • Kernel.#open にパスの書かれた文字列を渡すと、そのパスにあるファイルを開く。
  • require 'open-uri' をすると、Kernel.#open が再定義される。URL の書かれた文字列を渡すと、その URL にあるファイルを開く。
  • 持ってくる画像は、読み取り専用オプション(デフォルト)で開く。
  • 書き込むファイルは、書き込み可能オプションのうち、'w' で開く。

保留されていたことは、どのようにして Ruby でファイルを取り扱うかということでした。結論を書くと以下のようになります。

  • Kernel.#open は、ブロックを受け取ることができる。また、そのブロックに、変数を1つ渡すことができる。
  • この変数が、開いたファイルを司るオブジェクトである。これは、正確には、IO クラスのインスタンスである。

だからブロックの説明が必要だったわけです。

open へのブロックの渡し方、IO インスタンスの write と read

ここで、具体例を挙げましょう。次のプログラムの空欄を埋めて、保存しましょう。

require 'open-uri'

file = # ここにパスを書く。
url = "http://utmsks.github.io/images/2016pchardware.jpg"

open(file, 'w') {|output|
  open(url) {|input|
    output.write(input.read)
  }
}

file の部分には、保存したい jpg ファイルのフルパスを書いてください。ダウンロードしたい場所のパスですから、どこでもよいです。

参考:ターミナルで、保存したいディレクトリまで移動します。その後、コマンド pwd で、そのディレクトリのフルパスが分かります。ディレクトリを直接 file に文字列として書くのではなく、jpg ファイルのフルパスを書きましょう。

例: pwd で /Users/TA/Desktop と出てきて、そこに pic.jpg という名前で保存したいならば、 file = "/Users/TA/Desktop/pic.jpg" とします。受講生の皆さんの環境は、それぞれ違うわけですから、この例を直接写してもうまくいきません。

このプログラムの説明に入りましょう。今までの説明を理解すれば、以下のことまではわかるでしょう。

  • open(file, 'w') {|output| ..... } で、file で指定したファイルを、書き込み可能オプション 'w' で開いています(もしなかったら、作ってから開いています)。そのファイルを司るオブジェクト(IOクラスのインスタンス)が、変数 output に渡されています。
  • open(url) {|input| ..... } で、url で指定したファイルをインターネット経由で持ってきて、読み取り専用で「開いて」います。そのファイルを司るオブジェクト( IO クラスのインスタンス)が、変数 input に渡されています。

ですから、問題は writeread です。順番に説明します。

  • IO クラスのインスタンスメソッド read は、ファイルの中身を文字列で返します。
  • IO クラスのインスタンスメソッド write は、引数として渡された文字列を、ファイルに書き込みます。

画像だって、ファイルとして見れば結局は文字列です。だから read で読み込んで、write で書き込めばよいです。この部分を正確な言い方をすると、IO.read は画像ファイルであろうがなんだろうがファイルをバイナリとして取り扱い String クラスのインスタンスとして返します。IO.write はバイナリとして引数の String クラスのインスタンスを書き込みます。

だから、output.write(input.read) で、文字列を経由するけれども、「input のファイルの内容を output のファイルに、そのまま書き込んでいる」という意味になります。 上記プログラムを実行すればどうなるかはやってみればわかるでしょう。

ブロックで渡す利点

直接は関係ないのですが、open をブロックで取り扱う利点をお話します。 上で取り扱ったプログラムの最後の 5 行の処理を、ブロックを使わずに書くと以下のようになります。

output = open(file, 'w')
input = open(url)
output.write(input.read)
output.close
input.close

これは、「ファイルを保存する別のやり方」ではなく、「全く同じこと」をしています。 つまり open はブロックを使わなくても書くことができます。しかしその場合は、使い終わったファイルを close しなくてはなりません。

「しなくてはなりません」という意味は以下のとおりです。このパラグラフでは、open をブロックを使わないで書いた場合を議論します。 OS が同時に開けるファイルの数には制限があります。この数はきっかりいくつと決まっているわけではありません。例えば、使用可能なメモリの容量が影響します。 ゆえに Ruby 処理系が同時に開けるファイル数にも制限があり、1 つの Ruby プログラムが開けるファイル数にも制限があります。 IO クラスのインスタンスを close しないと、開いた状態が継続します。そのまま沢山開くと、やがて制限数に到達するかもしれません。 例えば、先ほどダウンロードした jpg ファイル(同じファイル)をひたすら開き続ける動作を私の手元の Ruby 処理系でやってみました。 249 個開いたところで Too many open files @ rb_sysopen というエラーが返ってきました。(推奨されないので、具体的なプログラムは略) ですから、必要なくなった IO オブジェクトは close しなくてはなりません。 もちろん、プログラム全体が終了した時点で、閉じられていない IO インスタンスが新たに問題を引き起こすことはありません。 ですから、このプログラムですと、仮に close しなくても問題はないでしょう。そういう意味では「しなくてはなりません」は強い言い方です。 しかし、例えばこのプログラムを close せずに書いたとして、その場はうまくいったと仮定します。 そのあとで、これをループしたくなったとします。すると、close しない IO インスタンスが、ループした分だけ生成され、 「開いたまま」でいます。すると、Too many open files というエラーが出て止まります。 そして実は、「これをループしたくなった」こそが、これからやろうとすることなのです。 ですから、数が少なくうまく動いたからといって、close をしなくてもよいという理屈にはなりません。 「今はいいかもしれないけど、将来に渡って問題が生じないように、保守性を高めておく」のが大事です。 今回の場合、保守性の問題ではなく、単なる間違いに含めて良いように思われる、原始的な現象です。

さて、 open をブロックを使って書いた場合、ブロックを抜けるところで、ブロックに渡された変数は自動的に close されます。 ですから、この手の問題は生じません。まとめると、

  • File.open や Kernel.#open で使い終わった IO インスタンスは、必ず close しなければならない。
  • ただし、ブロックに渡された変数は、ブロックを抜けるところで自動的に close される。 となります。

演習

まず、次のプログラムを書いて適当なファイル名をつけて保存しましょう。

require "open-uri"

url = "http://utmsks.github.io/images/2016pchardware.jpg"

input = open(url)

for i in 0..10000
  puts i
  file = "pic/#{i}.jpg"
  output = open(file, 'w')
  output.write(input.read)
end

問 1. このプログラムをいきなり Ruby 処理系で実行するとエラーが出ます。なぜでしょうか? この問いに対する次の誤答を訂正しましょう。 誤答「プログラムを保存したディレクトリに、pic というディレクトリがなかったから」

問 2. 前問の「下準備」を踏まえて、このプログラムを実行します。このプログラムは画像を何個保存しているでしょうか? また、インターネット上から画像を何回ダウンロードしているでしょうか? ヒント:前者は実行結果を見ればわかります。後者を直接確かめるのは、この教材の知識だけでは難しいかもしれません。しかし数理のネットワークでこれを実行しても起こられないという事実からも納得いくでしょう。

問 3. このプログラムは、input.closeoutput.close もせず、正常に終了します。なぜでしょうか? ヒント:そもそも「close を書いていない」という問題があるのは片方だけです。このプログラムが正常に終了する時点で、もう片方の問題はそもそも「ない」ことがわかります。なぜないのでしょうか? 上で述べたことから類推を働かせてください。直接の理由は書いていません。

ナビゲーション

この教材は、東京大学理学部数学科専門科目「計算数学 I」のために執筆されたものです。 このサイトに掲載する際に、記事を分けてあります。 他の回はRuby 画像ダウンロード編 一覧 #ks1-ruby-downloadから御覧ください。

Ruby 入門 (計算数学実習資料集)には他の TA が書いた教材があります。

コメントする