Ruby 文件 I/O 操作的详细介绍

作者:袖梨 2022-06-25

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 1n"
this is line 2
"this is line 2n"
all separated by $/, which is a newline character
"all separated by $/, which is a newline charactern"

改一下全局变量 $/ 的值:

>> $/ = "NEXT"
=> "NEXT"
>> STDIN.each {|line| p line}
First line
NEXT
"First linenNEXT"
Next line
where "line" really means
until we see... NEXT
"nNext linenwhere "line" really meansnuntil 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 inn", "Uppercase lettersn"]
>> 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 `

'

全局变量允许你控制流的去向。

键盘输入

大部分的键盘输入都是用 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")
=> #

使用文件实例可以读取文件。read 方法读取整个文件的内容:

>> f.read
=> "class Ticketn def initialize(venue, date)n @venue = venuen @date = daten endnn def price=(price)n @price = pricen endnn def venuen @venuen endnn def daten @daten endnn def pricen @pricen endnendn"
读取 line-based 文件

用 gets 方法读取下一行:

>> f = File.new("code/ticket2.rb")
=> #
>> f.gets
=> "class Ticketn"
>> f.gets
=> " def initialize(venue, date)n"
>> f.gets
=> " @venue = venuen"
readline 跟 gets 一样可以一行一行的读文件,不同的地方是到了文件的结尾,gets 返回 nil,readline 会报错。

再这样试试:

>> f.read
=> " @date = daten endnn def price=(price)n @price = pricen endnn def venuen @venuen endnn def daten @daten endnn def pricen @pricen endnendn"
>> 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 `

'
用 readlines 可以读取整个文件的所有的行,把它们放到一个 array 里。rewind 可以把 File 对象的内部位置指针移动到文件的开始:

>> f.rewind
=> 0
>> f.readlines
=> ["class Ticketn", " def initialize(venue, date)n", " @venue = venuen", " @date = daten", " endn", "n", " def price=(price)n", " @price = pricen", " endn", "n", " def venuen", " @venuen", " endn", "n", " def daten", " @daten", " endn", "n", " def pricen", " @pricen", " endn", "endn"]
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 Ticketn"
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 Ticketn"
>> f.pos
=> 13
把指针放到指定的位置:

>> f.pos = 10
=> 10
>> f.gets
=> "etn"
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")
=> #
>> f.puts "相见时难别亦难"
=> nil
>> f.close
=> nil
>> puts File.read("data.out")
相见时难别亦难
=> nil
>> f = File.new("data.out", "a")
=> #
>> 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 `

'
>> f = File.open("/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 `
'
>> 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 `
'
查询 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.open("code/ticket2.rb") {|f| f.stat}
=> #
>>
用 Dir 类处理目录

>> d = Dir.new("./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")
=> #
basename

>> path.basename
=> #
>> puts path.basename
test1.rb
=> nil
dirname

>> path.dirname
=> #
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")
=> #
>> 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 = /As*#/)
    infile.each do |inline|
      outfile.print inline unless inline =~ comment_re
    end
  end
end
DeCommenter.decomment 需要两个开放的文件句柄,一个可以读,一个可以写。正则表达式确定输入的每行是不是注释。不是注释的行会被写入到输出的文件里。

使用方法:

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 < # 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 == < 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

相关文章

精彩推荐