Ruby 檔案 I/O 操作的詳細介紹

來源:互聯網
上載者:User

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.