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 需要两个开放的文件句柄,一个可以读,一个可以写。正则表达式确定输入的每行是不是注释。不是注释的行会被写入到输出的文件里。
使用方法:
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