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.getsRuby