プログラミングでは、何らかのデータを読み出したり、書き出したりすることが数多くあります。
データの保存先は、データベースが用いられることが一般的ですが、まとまった文章や構造化されていないデータなどはファイルで保存されることも多くあります。
そのため、ファイル操作はプログラミングを学習する中で重要項目の1つとなります。
本記事では、Rubyでよく使われるファイル操作について解説します。
プログラミング初心者の方の学習や、忘れてしまった方の復習として、参考にしていただければ幸いです。
記載しているプログラムは、Ruby2.7.1を使って動作確認をしています。
Rubyのファイル操作
Rubyのファイル操作として、以下の内容を採り上げます。
- ファイル操作を扱うクラス
- ファイルの読み込み
- ファイルの書き込み
- ファイルの削除
- ファイルの存在確認
- ファイルのコピー
- ディレクトリの一覧取得
- ディレクトリの作成
- ディレクトリの削除
ファイル操作を扱うクラス
Rubyのファイル操作は、組込みライブラリの1つである File クラスや Dir クラスを使うことでほとんどの操作を行うことができます。
Rubyは同じ処理を複数の方法で実現できるようにしていることが多いですが、ファイルの操作も Kernel クラスを使って行うこともできます。
また、FileUtils モジュールにはファイルを扱う便利な操作が用意されています。
本記事では File クラスと Dir クラスを使ったファイル操作を中心に解説していきます。
ファイルの読み込み
ファイルを読み込むには、Fileクラスのopenに読み込みモードを指定してファイルを開き、 read を使って読み込みます。
begin File.open("./files/testfile.txt", "r") do |file| puts file.read end rescue => e puts e.message end
上記のプログラムでは、filesディレクトリのtestfile.txtファイルを読み込んで、内容をコンソールに出力しています。
ファイルが存在しないなどにより、読み込みに失敗した場合は、例外が発生し、rescue文で例外情報をコンソールに出力しています。
この方法では、ファイルの中身を一度にすべて読みこんでしまうため、大きなファイルだと時間がかかったり、メモリを大量に消費してしまいます。
ファイルを1行ずつ読み込むには、Fileクラスの gets を使います。
begin File.open("./files/testfile.txt", "r") do |file| puts file.gets end rescue => e puts e.message end
上記のプログラムでは、getsを呼出す毎に1行ずつ取得されます。同様の処理を行うものとして readline を使うこともできます。
ファイルの書き込み
ファイルの書き込みは、新規にファイルを作成して書き込む方法と、既存のファイルに追記する方法があります。
新規ファイルへの書き込み
新規にファイルを作成して書き込むには、Fileクラスのopenに書き込みモードを指定してファイルを開き、 puts を使って書き込みます。
print や write などでも同様の処理が可能です。
begin File.open("./files/writefile.txt", "w") do |file| file.puts("新規にファイルを作成するテスト") end rescue => e puts e.message end
上記のプログラムでは、filesディレクトリにwritefile.txtというファイルを作成し、その中に「新規にファイルを作成するテスト」という文字列を書き込んでいます。
実行後にファイルが作成されますが、毎回新規にファイルを作成するため、何度実行してもファイルの中身は同じ結果になります。
既存ファイルへの追記
既存のファイルへ追記するには、Fileクラスのopenに追記モードを指定してファイルを開き、puts を使って書き込みます。
begin File.open("./files/writefile.txt", "a") do |file| file.puts("既存にファイルに追記するテスト") end rescue => e puts e.message end
上記のプログラムでは、filesディレクトリのwritefile.txtというファイルに、「既存ファイルに追記するテスト」という文字列を追記しています。
既存ファイルが存在しない場合は新規にファイルを作成します。
ファイルの削除
ファイルを削除するには、Fileクラスの delete を使います。unlink でも同様の処理が行なえます。
戻り値として削除したファイル数を返します。
begin n = File.delete("./files/writefile.txt") puts n # -> 1 rescue => e puts e end
上記のプログラムでは、filesディレクトリのwritefile.txtというファイルを削除します。
ファイルが存在しない場合は、例外が発生します。
ファイルの存在確認
ファイルが存在するかどうかを確認するには、Fileクラスの exist? を使います。
puts File.exist?("./files/testfile.txt") # -> true puts File.exist?("./files/notexist.txt") # -> false puts File.exist?("./files") # -> true
exsit?は、ファイルが存在する場合はtrueを返し、存在しない場合はfalseを返します。
ディレクトリに対しても存在確認をすることができます。
ファイルかディレクトリかを判別するには、Fileクラスの file? または directory? で確認します。
puts File.file?("./files/testfile.txt") # -> true puts File.file?("./files") # -> false puts File.directory?("./files/testfile.txt") # -> false puts File.directory?("./files") # -> true
ファイルのコピー
ファイルをコピーするには、FileUtilsクラスの copy を使います。cpでも同じ処理をすることができます。
require 'fileutils' begin FileUtils.copy("./files/testfile.txt", "./files/copyfile.txt") rescue => e puts e end
上記のプログラムでは、filesディレクトリにcopyfile.txtを作成しますが、すでに同ファイルが存在している場合は、上書いてコピーします。
デフォルトではファイルの内容はコピーされますが、作成日時や変更日時などのメタデータはコピーしません。これらもコピーするにはオプションを指定します。
begin FileUtils.copy("./files/testfile.txt", "./files/copyfile2.txt", :preserve => true) rescue => e puts e end
ディレクトリの一覧取得
ディレクトリのファイル一覧を取得するには、Dirクラスの children を使います。
childrenはディレクトリに存在するファイルとディレクトリを配列で返します。
begin s = Dir.children("./files") puts s.inspect # -> ["testfile.txt", "testdir", "copyfile.txt", "copyfile2.txt"] rescue => e puts e end
上記のプログラムでは、filesディレクトリのファイルとディレクトリが取得され、その配下のファイルやディレクトリは取得されません。
配下のすべてのファイルやディレクトリを取得するには、Dirクラスの glob を使います。
ワイルドカード指定出来るので、取得対象を絞り込むこともできます。
begin s = Dir.glob("./files/**/*") puts s.inspect rescue => e puts e end
ディレクトリの作成
ディレクトリを新規に作成するには、Dirクラスの mkdir を使います。
begin n = Dir.mkdir("./files/testdir1") puts n # -> 0 rescue => e puts e end
上記のプログラムでは、filesディレクトリの配下にtestdir1ディレクトリを作成しています。
作成に成功すると0を返します。
すでにtestdir1ディレクトリが存在する場合にはエラーとなります。
複数階層のディレクトリを一気に作成するには、FileUtilsクラスの makedirs を使います。
すでにディレクトリが存在している場合でもエラーになりません。
ディレクトリ名の文字列を配列で返します。
begin a = FileUtils.makedirs("./files/testdir2/testdir2") puts a # -> ./files/testdir2/testdir2 rescue => e puts e end
プログラムの中では、一時的にファイルを扱うためにディレクトリを作成したい場合があります。
プログラムの特性にもよりますが、基本的には他のプログラムや処理で使うディレクトリとは異なる専用のディレクトリを作るのが一般的です。
そのような用途の場合には、ランダムな名称でディレクトリを作成してくれる tmpdirモジュールの Dir.tmpdir を使います。
require 'tmpdir' begin Dir.mktmpdir("prefix-", "./files/") {|dir| puts dir # -> ./files/prefix-20201029-30047-yogrqd } rescue => e puts e end
第1引数で接頭辞となる文字列を指定し、第2引数で作成するディレクトリを指定します。
戻り値として、作成したディレクトリを返します。
上記プログラムのようにブロックで作成した場合は、ブロックの最後に作成した一時ディレクトリが自動的に削除されます。
ディレクトリの削除
ディレクトリを削除するには、Dirクラスの rmdir を使います。delete、unlinkでも同様の処理が行なえます。
削除に成功すると0を返します。
begin n = Dir.rmdir("./files/testdir1") puts n # -> 0 rescue => e puts e end
削除対象のディレクトリに、ファイルやディレクトリが存在する場合はエラーとなります。
ディレクトリとファイルを一括で削除したい場合は、FileUtilsクラスの remove_entiry_secure を使います。
同様の処理が可能な、rm_r や remove_entry には脆弱性が存在するため、使用しないことが推奨されています。
begin FileUtils.remove_entry_secure("./files/testdir3") rescue => e puts e end
上記のプログラムでは、testdir3ディレクトリを配下のファイルやディレクトリとともに一括で削除します。
デフォルトでは削除処理に失敗すると例外が発生しますが、第2引数にtrueを指定すると発生した例外を抑制することができます。
複数の処理で1つのファイル操作を行う場合は要注意
ファイルはデータを保存しておく場所として使いやすいのですが、複数のプロセスやスレッドなどから同じファイルを操作しようとすると思わぬことが発生することがあるので注意が必要です。
僕がこれまでに携わったプロジェクトでも以下のようなことがありました。
- 複数のプロセスから同一のログファイルに書き込んだらファイルが壊れた
- 参照しようとしたら別の処理が先に消していた
- あるサーバが作成したファイルを別のサーバから参照しようとしたら、存在するはずのファイルが見えない状態だった
これらは、設計ミスやOSのファイル管理方法を理解出来ていないことが原因で発生したものでした。
ファイルはOSと密接に関わっているため、ローカル環境では上手く動いても、別の環境では意図通りに動かないといったことがあります。
ファイルを扱う際は、設計時点で問題ないかをきちんと議論することをおすすめします。
今回はRubyのファイル操作について解説しました。
以上、参考になれば幸いです。
コメント