Ruby 對待檔案與 I/O 操作也是物件導向的。
Ruby 的 I/O 系統
IO 類處理所有的輸入與輸出資料流。
IO 類
IO 對象表示可讀可寫的到磁碟檔案,鍵盤,螢幕或裝置的串連。
程式啟動以後會自動化佈建 STDERR,STDIN,STDOUT 這些常量。STD 表示 Standard,ERR 是 Error,IN 是 Input,OUT 是 Output。
標準的輸入,輸出,還有錯誤流都封裝到了 IO 的執行個體裡面。做個實驗:
>> STDERR.class
=> IO
>> STDERR.puts("problem!")
problem!
=> nil
>> STDERR.write("problem!\n")
problem!
=> 9
STDERR 是一個 IO 對象。如果一個 IO 對象開放寫入,你可以在它上面調用 puts,你想 puts 的東西會寫入到 IO 對象的輸出資料流裡。IO 對象還有 print 與 write 方法。 write 到 IO 對象的東西不會自動添加分行符號,返回的值是寫入的位元組數。
作為可枚舉的 IO 對象
想要枚舉的話,必須得有一個 each 方法,這樣才能迭代。迭代 IO 對象的時候會根據 $/ 這個變數。預設這個變數的值是一個分行符號: \n
>> STDIN.each {|line| p line}
this is line 1
"this is line 1\n"
this is line 2
"this is line 2\n"
all separated by $/, which is a newline character
"all separated by $/, which is a newline character\n"
改一下全域變數 $/ 的值:
>> $/ = "NEXT"
=> "NEXT"
>> STDIN.each {|line| p line}
First line
NEXT
"First line\nNEXT"
Next line
where "line" really means
until we see... NEXT
"\nNext line\nwhere \"line\" really means\nuntil we see... NEXT"
$/ 決定了 IO 對象怎麼 each 。因為 IO 可以枚舉,所以你可以對它執行其它的枚舉操作。
>> STDIN.select {|line| line =~ /\A[A-Z]/ }
We're only interested in
lines that begin with
Uppercase letters
^D
=> ["We're only interested in\n", "Uppercase letters\n"]
>> STDIN.map {|line| line.reverse }
senil esehT
terces a niatnoc
.egassem
^D
=> ["\nThese lines", "\ncontain a secret", "\nmessage."]
Stdin,Stdout,Stderr
Ruby 認為所有的輸入來自鍵盤,所有的輸出都會放到終端。puts,gets 會在 STDOUT 與 STDIN 上操作。
如果你想使用 STDERR 作為輸出,你得明確的說明一下:
if broken?
STDERR.puts "There's a problem!"
end
除了這三個常量,Ruby 還提供了三個全域變數:$stdin,$stdout,$stderr 。
標準 I/O 全域變數
STDIN 與 $stdin 的主要區別是,你不能重新分配值給常量,但是你可以為變數重新分配值。變數可以讓你修改預設 I/O 流行為,而且不會影響原始流。
比如你想把輸出放到一個檔案裡,包含 standard out 還有 standard error 。把下面代碼儲存到一個 rb 檔案裡:
record = File.open("./tmp/record", "w")
old_stdout = $stdout
$stdout = record
$stderr = $stdout
puts "this is a record"
z = 10/0
首頁是開啟你想寫的檔案,然後把當前的 $stdout 儲存到一個變數裡,重新定義了 $stdout,讓它作為 record 。$stderr 設定成了讓它等於 $stdout 。現在,任何 puts 的結果都會寫入到 /tmp/record 檔案裡,因為 puts 會輸出到 $stdout 。$stderr 輸出也會放到檔案裡,因為我們也把 $stderr 分配給了檔案控制代碼。
在項目的目錄建立一個 tmp/record 檔案,然後運行一下,再開啟 record 檔案看一下:
this is a record
demo.rb:6:in `/': divided by 0 (ZeroDivisionError)
from demo.rb:6:in `<main>'
全域變數允許你控制流程的去向。
鍵盤輸入
大部分的鍵盤輸入都是用 gets 與 getc 完成的。gets 返回輸入的行,getc 返回一個字元。gets 需要你明確的給輸出資料流起個名字。
line = gets
char = STDIN.getc
輸入會被緩衝,你得按下斷行符號。
因為某些原因,你把 $stdin 設定成了鍵盤以外的東西,你仍然可以使用 STDIN 作為 gets 的接收者來讀取鍵盤的輸入:
line = STDIN.gets
檔案操作基礎
Ruby 內建的 File 類可以處理檔案。File 是 IO 的一個子類,所以它可以共用 IO 對象的一些屬性,不過 File 類添加並且修改了某些行為。
讀檔案
我們可以每次讀取檔案的一個位元組,也可以指定每次讀取的位元組數,或者也可以每次讀取一行,行是用 $/ 變數的值區分的。
先建立一個檔案對象,最簡單的方法是使用 File.new,把檔案名稱交給這個構造器,假設要讀取的檔案已經存在,我們會得到一個開放讀取的檔案控制代碼。
建立一個檔案,名字是 ticket2.rb,把它放在 code 目錄的下面:
class Ticket
def initialize(venue, date)
@venue = venue
@date = date
end
def price=(price)
@price = price
end
def venue
@venue
end
def date
@date
end
def price
@price
end
end
試一下:
>> f = File.new("code/ticket2.rb")
=> #<File:code/ticket2.rb>
使用檔案執行個體可以讀取檔案。read 方法讀取整個檔案的內容:
>> f.read
=> "class Ticket\n def initialize(venue, date)\n @venue = venue\n @date = date\n end\n\n def price=(price)\n @price = price\n end\n\n def venue\n @venue\n end\n\n def date\n @date\n end\n\n def price\n @price\n end\nend\n"
讀取 line-based 檔案
用 gets 方法讀取下一行:
>> f = File.new("code/ticket2.rb")
=> #<File:code/ticket2.rb>
>> f.gets
=> "class Ticket\n"
>> f.gets
=> " def initialize(venue, date)\n"
>> f.gets
=> " @venue = venue\n"
readline 跟 gets 一樣可以一行一行的讀檔案,不同的地方是到了檔案的結尾,gets 返回 nil,readline 會報錯。
再這樣試試:
>> f.read
=> " @date = date\n end\n\n def price=(price)\n @price = price\n end\n\n def venue\n @venue\n end\n\n def date\n @date\n end\n\n def price\n @price\n end\nend\n"
>> f.gets
=> nil
>> f.readline
EOFError: end of file reached
from (irb):14:in `readline'
from (irb):14
from /usr/local/bin/irb:11:in `<main>'
用 readlines 可以讀取整個檔案的所有的行,把它們放到一個 array 裡。rewind 可以把 File 對象的內部位置指標移動到檔案的開始:
>> f.rewind
=> 0
>> f.readlines
=> ["class Ticket\n", " def initialize(venue, date)\n", " @venue = venue\n", " @date = date\n", " end\n", "\n", " def price=(price)\n", " @price = price\n", " end\n", "\n", " def venue\n", " @venue\n", " end\n", "\n", " def date\n", " @date\n", " end\n", "\n", " def price\n", " @price\n", " end\n", "end\n"]
File 對象可枚舉。不用把整個檔案全讀到記憶體裡,我們可以使用 each 一行一行的讀:
>> f.each {|line| puts "下一行:#{line}"}
下一行:class Ticket
下一行: def initialize(venue, date)
讀取 byte 與 character-based 檔案
getc 方法讀取與返迴文件的一個字元:
>> f.getc
=> "c"
ungetc:
>> f.getc
=> "c"
>> f.ungetc("X")
=> nil
>> f.gets
=> "Xlass Ticket\n"
getbyte 方法。一個字元是用一個或多個位元組表示的,這取決於字元的編碼。
>> f.getc
=> nil
>> f.readchar
EOFError: end of file reached
>> f.getbyte
=> nil
>> f.readbyte
EOFError: end of file reached
檢索與查詢檔案位置
檔案對象的 pos 屬性與 seek 方法可以改變內部指標的位置。
pos
>> f.rewind
=> 0
>> f.pos
=> 0
>> f.gets
=> "class Ticket\n"
>> f.pos
=> 13
把指標放到指定的位置:
>> f.pos = 10
=> 10
>> f.gets
=> "et\n"
seek
seek 方法可以把檔案的位置指標移動到新的地方。
f.seek(20, IO::SEEK_SET)
f.seek(15, IO::SEEK_CUR)
f.seek(-10, IO::SEEK_END)
第一行檢索到 20 位元組。第二行檢索到當前位置往後的 15 位元組。第三行檢查檔案結尾往前的 10 個位元組。IO::SEEK_SET 是可選的,可以直接 f.seek(20),f.pos = 20 。
用 File 類方法讀檔案
File.read 與 File.readlines。
full_text = File.read("myfile.txt")
lines_of_text = File.readlines("myfile.txt")
第一行得到一個字串,裡面是檔案的整個內容。第二行得到的是一個數組,裡面的項目是檔案的每行內容。這兩個方法會自動開啟與關閉檔案。
寫檔案
puts,print,write。w 表示檔案的寫入模式,把它作為 File.new 的第二個參數,可以建立檔案,如果檔案已經存在會覆蓋裡面的內容。a 表示追加模式,檔案不存在,使用追加模式也會建立檔案。
做個實驗就明白了:
>> f = File.new("data.out", "w")
=> #<File:data.out>
>> f.puts "相見時難別亦難"
=> nil
>> f.close
=> nil
>> puts File.read("data.out")
相見時難別亦難
=> nil
>> f = File.new("data.out", "a")
=> #<File:data.out>
>> f.puts "東風無力百花殘"
=> nil
>> f.close
=> nil
>> puts File.read("data.out")
相見時難別亦難
東風無力百花殘
=> nil
代碼塊劃分檔案操作的範圍
使用 File.new 建立 File 對象有一點不好,就是完事以後你得自己關掉檔案。另一種方法,可以使用 File.open,再給它提供個代碼塊。代碼塊可以接收 File 對象作為它的唯一參數。代碼塊結束以後,檔案對象會自動關閉。
先建立一個檔案,名字是 records.txt,內容是:
Pablo Casals|Catalan|cello|1876-1973
Jascha Heifetz|Russian-American|violin|1901-1988
Emanuel Feuermann|Austrian-American|cello|1902-1942
下面代碼放到一個 rb 檔案裡:
File.open("records.txt") do |f|
while record = f.gets
name, nationality, instrument, dates = record.chomp.split('|')
puts "#{name} (#{dates}), who was #{nationality},
played #{instrument}. "
end
end
執行的結果是:
Pablo Casals (1876-1973), who was Catalan,
played cello.
Jascha Heifetz (1901-1988), who was Russian-American,
played violin.
Emanuel Feuermann (1902-1942), who was Austrian-American,
played cello.
檔案的可枚舉性
用 each 代替 while:
File.open("records.txt") do |f|
f.each do |record|
name, nationality, instrument, dates = record.chomp.split('|')
puts "#{name} (#{dates}), who was #{nationality},
played #{instrument}. "
end
end
實驗:
# Sample record in members.txt:
# David Black male 55
count = 0
total_ages = File.readlines("members.txt").inject(0) do |total,line|
count += 1
fields = line.split
age = fields[3].to_i
total + age
end
puts "Average age of group: #{total_ages / count}."
實驗:
count = 0
total_ages = File.open("members.txt") do |f|
f.inject(0) do |total,line|
count += 1
fields = line.split
age = fields[3].to_i
total + age
end
end
puts "Average age of group: #{total_ages / count}."
檔案 I/O 異常與錯誤
檔案相關的錯誤一般都在 Errno 命名空間下:Errno::EACCES,許可權。Errno::ENOENT,no such entity,沒有檔案或目錄。Errno::EISDIR,目錄,開啟的東西不是檔案而是目錄。
>> File.open("no_file_with_this_name")
Errno::ENOENT: No such file or directory @ rb_sysopen - no_file_with_this_name
from (irb):23:in `initialize'
from (irb):23:in `open'
from (irb):23
from /usr/local/bin/irb:11:in `<main>'
>> f = File.open("/tmp")
=> #<File:/tmp>
>> f.gets
Errno::EISDIR: Is a directory @ io_fillbuf - fd:10 /tmp
from (irb):25:in `gets'
from (irb):25
from /usr/local/bin/irb:11:in `<main>'
>> File.open("/var/root")
Errno::EACCES: Permission denied @ rb_sysopen - /var/root
from (irb):26:in `initialize'
from (irb):26:in `open'
from (irb):26
from /usr/local/bin/irb:11:in `<main>'
查詢 IO 與檔案對象
IO 類提供了一些查詢方法,File 類又添加了一些。
從 File 類與 Filetest 模組那裡擷取資訊
File 與 Filetest 提供的查詢方法可以讓你瞭解很多關於檔案的資訊。
檔案是否存在
>> FileTest.exist?("/usr/local/src/ruby/README")
=> false
目錄?檔案?還是捷徑?
FileTest.directory?("/home/users/dblack/info")
FileTest.file?("/home/users/dblack/info")
FileTest.symlink?("/home/users/dblack/info")
blockdev?,pipe?,chardev?,socket?
可讀?可寫?可執行?
FileTest.readable?("/tmp")
FileTest.writable?("/tmp")
FileTest.executable?("/home/users/dblack/setup")
檔案多大?
FileTest.size("/home/users/dblack/setup")
FileTest.zero?("/tmp/tempfile")
File::Stat
兩種方法:
>> File::Stat.new("code/ticket2.rb")
=> #<File::Stat dev=0x1000002, ino=234708237, mode=0100644, nlink=1, uid=501, gid=20, rdev=0x0, size=223, blksize=4096, blocks=8, atime=2016-09-14 14:42:03 +0800, mtime=2016-09-14 14:16:29 +0800, ctime=2016-09-14 14:16:29 +0800, birthtime=2016-09-14 14:16:28 +0800>
>> File.open("code/ticket2.rb") {|f| f.stat}
=> #<File::Stat dev=0x1000002, ino=234708237, mode=0100644, nlink=1, uid=501, gid=20, rdev=0x0, size=223, blksize=4096, blocks=8, atime=2016-09-14 14:42:03 +0800, mtime=2016-09-14 14:16:29 +0800, ctime=2016-09-14 14:16:29 +0800, birthtime=2016-09-14 14:16:28 +0800>
>>
用 Dir 類處理目錄
>> d = Dir.new("./node_modules/mocha")
=> #<Dir:./node_modules/mocha>
讀取目錄
entries 方法,或 glob (不顯示隱藏條目)。
entries 方法
>> d.entries
=> [".", "..", "bin", "bower.json", "browser-entry.js", "CHANGELOG.md", "images", "index.js", "lib", "LICENSE", "mocha.css", "mocha.js", "package.json", "README.md"]
或者使用類方法:
>> Dir.entries("./node_modules/mocha")
=> [".", "..", "bin", "bower.json", "browser-entry.js", "CHANGELOG.md", "images", "index.js", "lib", "LICENSE", "mocha.css", "mocha.js", "package.json", "README.md"]
檔案尺寸,不包含隱藏的檔案,就是用點開頭的檔案,把下面代碼放到一個檔案裡再執行一下:
d = Dir.new("./node_modules/mocha")
entries = d.entries
entries.delete_if {|entry| entry =~ /^\./ }
entries.map! {|entry| File.join(d.path, entry) }
entries.delete_if {|entry| !File.file?(entry) }
print "Total bytes: "
puts entries.inject(0) {|total, entry| total + File.size(entry) }
結果:
Total bytes: 520610
glob
可以做類似這樣的事情:
ls *.js
rm *.?xt
for f in [A-Z]*
*表示任意數量的字元,?表示一個任一字元。
使用 Dir.glob 與 Dir.[ ],方括弧版本的方法允許你使用 index 風格的文法:
>> Dir["node_modules/mocha/*.js"]
=> ["node_modules/mocha/browser-entry.js", "node_modules/mocha/index.js", "node_modules/mocha/mocha.js"]
glob 方法可以添加一個或多個標記參數來控制一些行為:
Dir.glob("info*") # []
Dir.glob("info", File::FNM_CASEFOLD # ["Info", "INFORMATION"]
FNM_DOTMATCH,在結果裡包含點開頭的檔案。
使用兩個標記:
>> Dir.glob("*info*")
=> []
>> Dir.glob("*info*", File::FNM_DOTMATCH)
=> [".information"]
>> Dir.glob("*info*", File::FNM_DOTMATCH | File::FNM_CASEFOLD)
=> [".information", ".INFO", "Info"]
處理與查詢目錄
mkdir:建立目錄,chdir:更改工作目錄,rmdir:刪除目錄。
newdir = "/tmp/newdir"
newfile = "newfile"
Dir.mkdir(newdir)
Dir.chdir(newdir) do
File.open(newfile, "w") do |f|
f.puts "新目錄裡的示範檔案"
end
puts "目前的目錄:#{Dir.pwd}"
puts "列表:"
p Dir.entries(".")
File.unlink(newfile)
end
Dir.rmdir(newdir)
print "#{newdir} 還存在嗎?"
if File.exist?(newdir)
puts "yes"
else
puts "no"
end
結果是:
目前的目錄:/private/tmp/newdir
列表:
[".", "..", "newfile"]
/tmp/newdir 還存在嗎?no
標準庫裡的檔案工具
FileUtils 模組
複製,移動,刪除檔案
>> require 'fileutils'
=> true
>> FileUtils.cp("demo.rb", "demo.rb.bak")
=> nil
>> FileUtils.mkdir("backup")
=> ["backup"]
>> FileUtils.cp(["demo.rb.bak"], "backup")
=> ["demo.rb.bak"]
>> Dir["backup/*"]
=> ["backup/demo.rb.bak"]
FileUtils.mv
FileUtils.rm
FileUtils.rm_rf
DryRun
FileUtils::DryRun.rm_rf
FileUtils::NoWrite.rm
Pathname 類
>> require 'pathname'
=> true
>> path = Pathname.new("/Users/xiaoxue/desktop/test1.rb")
=> #<Pathname:/Users/xiaoxue/desktop/test1.rb>
basename
>> path.basename
=> #<Pathname:test1.rb>
>> puts path.basename
test1.rb
=> nil
dirname
>> path.dirname
=> #<Pathname:/Users/xiaoxue/desktop>
extname
>> path.extname
=> ".rb"
ascend
>> path.ascend do |dir|
?> puts "next level up: #{dir}"
>> end
next level up: /Users/xiaoxue/desktop/test1.rb
next level up: /Users/xiaoxue/desktop
next level up: /Users/xiaoxue
next level up: /Users
next level up: /
=> nil
>> path = Pathname.new("/Users/xiaoxue/desktop/test1.rb")
=> #<Pathname:/Users/xiaoxue/desktop/test1.rb>
>> path.ascend do |dir|
?> puts "Ascended to #{dir.basename}"
>> end
Ascended to test1.rb
Ascended to desktop
Ascended to xiaoxue
Ascended to Users
Ascended to /
=> nil
StringIO 類
把字串當 IO 對象。檢索,倒回 ...
比如你有個模組可以取消檔案裡的注釋,讀取檔案除了注釋的內容再把它寫入到另一個檔案:
module DeCommenter
def self.decomment(infile, outfile, comment_re = /\A\s*#/)
infile.each do |inline|
outfile.print inline unless inline =~ comment_re
end
end
end
DeCommenter.decomment 需要兩個開放的檔案控制代碼,一個可以讀,一個可以寫。Regex確定輸入的每行是不是注釋。不是注釋的行會被寫入到輸出的檔案裡。
使用方法:
File.open("myprogram.rb") do |inf|
File.open("myprogram.rb.out", "w") do |outf|
DeCommenter.decomment(inf, outf)
end
end
使用真檔案測試
你想使用真的檔案測試檔案的輸入輸出,可以用一下 Ruby 的 tempfile 類。
require 'tempfile'
建立臨時檔案:
tf = Tempfile.new("my_temp_file").
require 'stringio'
require_relative 'decommenter'
string <<EOM
# this is comment.
this is not a comment.
# this is.
# so is this.
this is also not a comment.
EOM
infile = StringIO.new(string)
outfile = StringIO.new("")
DeCommenter.decomment(infile, outfile)
puts "test succeeded" if outfile.string == <<EOM
this is not a comment.
this is also not a comment.
EOM
open-uri 庫
使用 http,https 擷取資訊。
require 'open-uri'
rubypage = open("http://rubycentral.org")
puts rubypage.gets