# **第 14 章 字符串类**
正如在 第 4 章中说明的那样,Ruby 的字符串都是 `String` 类的对象。本章将详细介绍 `String` 类。
-
**字符串的创建**
列举各种创建字符串的方法。
-
**获取字符串的长度**
介绍获取字符串长度的方法,说明“字符数”与“字节数”的不同。
-
**字符串的索引**
与数组一样,也是通过索引进行字符串操作,在这一部分中我们会介绍字符串中索引的 用法。
-
**字符串的连接与分割**
介绍连接字符串的方法 `+`、`<<`、`concat`,分割字符串的方法 `split`。
-
**字符串的比较**
介绍判断字符串是否相等的方法、以及在进行字符串排序时如何比较字符串的“大小”。
-
**换行符的使用方法**
介绍字符串中换行符的使用方法。
-
**字符串的检索与置换**
介绍检索与置换字符串的方法。
-
**字符串与数组的共通方法**
字符串与数组中有不少作用相似的同名方法,在这一部分中我们会集中介绍这些方法。
-
**日语字符编码的转换**
介绍如何对日语字符串进行必要的字符编码转换。
### **14.1 字符串的创建**
最简单的字符创建方法就是把字符的集合用 `" "` 或者 `' '` 括起来并直接写到程序中。
~~~
str1 = "这也是字符串"
str2 = ' 那也是字符串'
~~~
> **注** 在脚本中使用日语时需要注意编码(encoding)问题,相关内容请参考 19.2 节 1。
1在脚本中使用中文时也同样需要注意编码问题。——译者注
在第 1 章中,我们已经简单地介绍了用 `" "` 与用 `' '` 创建字符串的不同点。而除此以 外,使用 `" "` 时还可以执行用 `#{}` 括起来的 Ruby 式子,并将执行结果嵌入到字符串中。这个 `#{}` 就称为内嵌表达式(embedded expressions)。
~~~
moji = "字符串"
str1 = "那也是#{moji}"
p str1 #=> "那也是字符串"
str2 = ' 那也是#{moji}'
p str2 #=> "那也是\#{moji}"
~~~
使用 `" "` 时,可以显示使用 `\`转义的特殊字符(表 14.1)。
**表 14.1 使用 \ 转义的特殊字符**
<table border="1" data-line-num="53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71" width="90%"><thead><tr><th> <p class="表头单元格">特殊字符</p> </th> <th> <p class="表头单元格">意义</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>\t</code></p> </td> <td> <p class="表格单元格">水平制表符(0x09)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\n</code></p> </td> <td> <p class="表格单元格">换行符(0x0a)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\r</code></p> </td> <td> <p class="表格单元格">回车(0x0d)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\f</code></p> </td> <td> <p class="表格单元格">换页(0x0c)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\b</code></p> </td> <td> <p class="表格单元格">退格(0x08)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\a</code></p> </td> <td> <p class="表格单元格">响铃(0x07)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\e</code></p> </td> <td> <p class="表格单元格">溢出(0x1b)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\s</code></p> </td> <td> <p class="表格单元格">空格(0x20)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\v</code></p> </td> <td> <p class="表格单元格">垂直制表符(0x0b)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\<em>nnn</em></code></p> </td> <td> <p class="表格单元格">8 进制表示方式(<em>n</em> 为 0~ 7)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\X<em>nn</em></code></p> </td> <td> <p class="表格单元格">16 进制表示方式(<em>n</em> 为 0 ~ 9、a ~ f、A ~ F)</p> </td> </tr><tr><td> <p class="表格单元格"><code>\C</code><em>x</em>、<code>\C</code>-<em>x</em></p> </td> <td> <p class="表格单元格"><code>Control</code> + <em>x</em></p> </td> </tr><tr><td> <p class="表格单元格"><code>\M</code>-<em>x</em></p> </td> <td> <p class="表格单元格"><code>Meta(Alt)</code> + <em>x</em></p> </td> </tr><tr><td> <p class="表格单元格"><code>\M</code>-\C-<em>x</em></p> </td> <td> <p class="表格单元格"><code>Meta(Alt)</code> + <code>Control</code> + <em>x</em></p> </td> </tr><tr><td> <p class="表格单元格">\<em>x</em></p> </td> <td> <p class="表格单元格">表示 <em>x</em> 字符本身(<em>x</em> 为除以上字符外的字符)</p> </td> </tr><tr><td> <p class="表格单元格">\<em>Unnnn</em></p> </td> <td> <p class="表格单元格">Unicode 字符的 16 进制表示方式(<em>n</em> 为 0 ~ 9、a ~ f、A ~ F)</p> </td> </tr></tbody></table>
下面我们来看看不用 `" "`、`' '` 时应如何创建字符串。
### **14.1.1 使用 %Q 与 %q**
当创建包含 `"` 或者 `'` 的字符串时,比起使用 `\"`、`\'` 进行转义,使用 `%Q` 或者 `%q` 会更简单。
~~~
desc = %Q{Ruby 的字符串中也可以使用'' 和""。}
str = %q|Ruby said, 'Hello world!'|
~~~
使用 `%Q` 相当于用 `" "` 创建字符串,使用 `%q` 则相当于用 `' '` 创建字符串。
### **14.1.2 使用 Here Document**
Here Document 是源自于 Unix 的 shell 的一种程序写法,使用 `<<` 来创建字符串。创建包含换行的长字符串时用这个方法是最简单的。
**<<"结束标识符"
字符串内容
结束标识符**
`<<` 后面的结束标识符可以用 `" "` 括着的字符串或者用 `' '` 括着的字符串来定义。用 `" "` 括住的字符串中可以使用转义字符和内嵌表达式,而用 `' '` 括住的字符串则不会做任何特殊处理,只会原封不动地显示。另外,使用既没有 `" "` 也没有 `' '` 的字符串时,则会被认为是用 `" "` 创建的字符串。
由于 Here Document 整体就是字符串的字面量,因此可以被赋值给变量,也可以作为方法的参数。
我们一般将 `EOF` 或者 `EOB` 作为结束标识符使用。`EOF` 是“End of File”的简写,`EOB` 是“End of Block”的简写。
Here Document 的结束标识符一定要在行首。因此,在程序缩进比较深的地方使用 Here Document 的话,有时就会像下面的例子那样,出现整个缩进乱掉的情况。
~~~
10.times do |i|
10.times do |j|
print(<<"EOB")
i: #{i}
j: #{j}
i*j = #{i*j}
EOB
end
end
~~~
若希望缩进整齐,可以像下面那样用 `<<-` 代替 `<<`。这样程序就会忽略结束标识符前的空格和制表符,结束标识符也就没有必要一定要写在行首了。
~~~
10.times do |i|
10.times do |j|
print(<<-"EOB")
i: #{i}
j: #{j}
i*j = #{i*j}
EOB
end
end
~~~
这样操作以后,`print` 和 `EOB` 的缩进就会变得整齐,便于阅读。下面是将 Here Document 赋值给变量时的做法。
~~~
str = <<-EOB
Hello!
Hello!
Hello!
EOB
~~~
### **14.1.3 使用 sprintf 方法**
就像用 8 进制或 16 进制的字符串来表示数值那样,我们也可以用 `sprintf` 方法输出某种格式的字符串。关于 `sprintf` 的具体使用方法,请参考专栏《printf 方法与 sprintf 方法》。
### **14.1.4 使用 ``**
通过用 ` 命令 ` 的形式,我们可以得到命令的标准输出并将其转换为字符串对象。下面是获取 Linux 的 ls 命令与 cat 命令的输出内容的例子:
> **执行示例**
~~~
> irb --simple-prompt
>> `ls -l /etc/hosts`
=> "-rw-r--r-- 1 root root 158 Jan 12 2010 /etc/hosts\n"
>> puts `cat /etc/hosts`
# Host Database
#
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
fe80::1%lo0 localhost
=> nil
~~~
> **专栏**
> **printf 方法与 sprintf 方法**
> 虽然 `printf` 方法与 `sprintf` 方法都不是 `String` 类的方法,但在处理字符串时会经常用到它们。下面我们就来看看它们的用法。
> - > **关于 printf 方法**
> `printf` 方法可以按照某种格式输出字符串。例如在输出数值时,有时我们会需要在数值前补零,或者限定小数点显示的位数等,在这些情况下,用 `printf` 方法都能非常轻松地实现。
~~~
1: n = 123
2: printf("%d\n", n)
3: printf("%4d\n", n)
4: printf("%04d\n", n)
5: printf("%+d\n", n)
~~~
> 执行结果如下。
~~~
123
123
0123
+123
~~~
> `printf` 方法的第 1 个参数表示字符串的输出格式。而从第 2 个参数开始,往后的参数都会被依次嵌入到格式中 `%` 所对应的位置。
> 在本例中,第 2 行的 `printf` 方法被指定为了 `%d`,这表示输出的字符是整数。
> `%` 与 `d` 之间还能插入字符。第 3 行的 `printf` 方法里插入了 `4`,这表示按照 4 位整数的格式输出。我们发现,执行结果中出现了 `123` 这种开头有 1 个空格的情况,这是因为要把 3 位整数以 4 位的格式输出,因此就多输出了 1 个空格。
> 在第 4 行中,`%` 与 `d` 中间插入了 `04`,这表示若输出的整数位数不足,整数的开头就会做补零处理。
> 第 5 行中指定了 `+`,这表示输出的结果一定会包含 `+` 或者 `-`。
> 上面是关于数值格式的指定方法,同样,我们也可以指定字符串格式。
~~~
1: n = "Ruby"
2: printf("Hello,%s!\n", n)
3: printf("Hello,%8s!\n", n)
4: printf("Hello,%-8s!\n", n)
~~~
> 执行结果如下:
~~~
Hello,Ruby!
Hello, Ruby!
Hello,Ruby !
~~~
> 本例的第 2 行程序中指定了 `%s`,这表示将参数解析为字符串。参数 `n` 的值为 `Ruby`,因此输出的字符串为 `Hello,Ruby!`。
> 在第 3 行中,`%` 与 `s` 之间插入了数字 `8`,这表示将 `Ruby` 输出为 8 位字符串。
> 在第 4 行中,插入的内容为 `-8`,这表示按靠左对齐的方式输出 8 位字符串。
> - > **关于 sprintf 方法**
> `printf` 方法会把内容输出到控制台,而 `sprintf` 方法则是把同样的输出内容转换为字符串对象。开头的 `s` 指的就是 `String`。
~~~
p sprintf("%d", 123) #=> "123"
p sprintf("%04d", 123) #=> "0123"
p sprintf("%+d", 123) #=> "+123"
p sprintf("Hello,%s!\n", n) #=> "Hello,Ruby!"
p sprintf("Hello,%8s!\n", n) #=> "Hello, Ruby!"
p sprintf("Hello,%-8s!\n", n) #=> "Hello,Ruby !"
~~~
### **14.2 获取字符串的长度**
我们用 `length` 方法和 `size` 方法获取字符串的长度。两者都返回相同的结果,大家根据自己的习惯选用即可。
~~~
p "just another ruby hacker,".length #=> 25
p "just another ruby hacker,".size #=> 25
~~~
若是中文字符串,则返回字符数。
~~~
p ' 面向对象编程语言'.length #=> 8
~~~
如果想获取的不是字符数,而是字节数,可以用 `bytesize` 方法。
~~~
p ' 面向对象编程语言'.bytesize #=> 24
~~~
想知道字符串的长度是否为 0 时,可以使用 `empty?` 方法,该方法常被用于在循环等处理中判断字符串是否为空。
~~~
p "".empty? #=> true
p "foo".empty? #=> false
~~~
### **14.3 字符串的索引**
获取字符串中指定位置的字符,例如获取“开头第 3 位的字符”时,与数组一样,我们也需要用到索引。
~~~
str = "全新的String 类对象"
p str[0] #=> "全"
p str[3] #=> "S"
p str[9] #=> "类"
p str[2, 8] #=> "的String 类"
p str[4] #=> "t"
~~~
### **14.4 字符串的连接**
连接字符串有以下两种方法(图 14.1):
-
**将两个字符串合并为新的字符串**
-
**扩展原有的字符串**
![{%}](http://www.ituring.com.cn/figures/2015/Ruby4/21.d14z.001.png)
**图 14.1 连接字符串**
用 `+` 创建新的字符串。
~~~
hello = "Hello, "
world = "World!"
str = hello + world
p str #=> "Hello, World!"
~~~
为原有字符串连接其他字符串时,可以使用 `<<` 或者 `concat` 方法。
~~~
hello = "Hello, "
world = "World!"
hello << world
p hello #=> "Hello, World!"
hello.concat(world)
p hello #=> "Hello, World!World!"
~~~
使用 `+` 也能连接原有字符串。
~~~
hello = hello + world
~~~
用 `+` 连接原有字符串的结果会被再次赋值给变量 `hello`,这与使用 `<<` 的结果是一样的。但用 `+` 连接后的字符串对象是新创建的,并没有改变原有对象,因此即使有其他变量与 `hello` 同时指向原来的对象,那些变量的值也不会改变。而另一方面,由于使用 `<<` 与 `concat` 方法时会改变原有的对象,因此就会对指向同一对象的其他变量产生影响。虽然一般情况下使用 `<<` 与 `concat` 方法会比较有效率,但是我们也应该根据实际情况来选择适当的字符串连接方法。
### **14.5 字符串的比较**
我们可以使用 `==` 或者 `!=` 来判断字符串是否相同。例如在表达式 `str1 == str2` 中,`str1` 与 `str2` 为相同字符串时则返回 `true`,为不同字符串时则返回 `false`。`!=` 与 `==` 表示正相反的意思。
~~~
p "aaa" == "baa" #=> false
p "aaa" == "aa" #=> false
p "aaa" == "aaa" #=> true
p "aaa" != "baa" #=> true
p "aaa" != "aaa" #=> false
~~~
虽然判断字符串是否相同时使用 `==` 或者 `!=` 会很方便,但判断是否为相似的字符串时,使用正则表达式则会简单得多。
### **14.5.1 字符串的大小比较**
字符串也有大小关系,但字符串的大小关系并不是由字符串的长度决定的。
~~~
p ("aaaaa" < "b") #=> true
~~~
字符串的大小由字符编码的顺序决定。英文字母按照“ABC”的顺序排列,日语的平假名与片假名按照“あいうえお”的顺序排列 2,在排列英语或日语的字符串时,就可以使用该顺序。不过,Ruby 中日语的排序规则与字典中的顺序是不同的。例如对“かけ”、“かこ”、“がけ”这 3 个单词排序时,字典中的顺序是“かけ”、“がけ”、“かこ”,而在 Ruby 中,从小到大依次是“かけ”、“かこ”、“がけ”。
2即按日语 50 音的顺序排列。——译者注
另外,由于不能从汉字字符串得到汉字读音 3,因此,如果想根据读音排列汉字,就需要事先准备好读音数据。
3这里特指日语中使用的汉字及其日语读音,与中文中使用的汉字以及拼音是不同的。——译者注
中文字符也同样是由字符编码的顺序决定的。例如,在 UTF-8 字符编码表中,“一”的编码为 U+4E00,“丁”的编码为 U+4E01,两个字符的比较结果如下。
~~~
p (" 一" < " 丁") #=> true
~~~
> **专栏**
> **字符编码**
> 计算机中的字符都是用数值来管理的,这样的数值也称为编码。
> 字符与数值的对应关系如下表所示。
<table border="1" data-line-num="323 324 325 326 327 328" width="90%"><thead><tr><th> <p class="表头单元格">字符</p> </th> <th> <p class="表头单元格">数值</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格">A</p> </td> <td> <p class="表格单元格">65</p> </td> </tr><tr><td> <p class="表格单元格">B</p> </td> <td> <p class="表格单元格">66</p> </td> </tr><tr><td> <p class="表格单元格">C</p> </td> <td> <p class="表格单元格">67</p> </td> </tr></tbody></table>
> 我们把上表那样字符与数值一一对应的关系称为字符编码。但字符编码并不是一个正确的专业术语,因此我们需要小心使用。
> ASCII 编码是计算机的基础。ASCII 编码是指把英文字母、数值、其他符号、以及换行、制表符这样的特殊字符集合起来,为它们分配 1 到 127 之间的数值,并使之占用 1 个字节空间(1 个字节可以表示 0 到 255)。另外,在欧美,一种名为 ISO-8859-1 的编码曾经被广泛使用,该编码包含了欧洲常用的基本字符(拼写符号、原音变音等),并为它们分配 128 到 255之间的数值。也就是说,大部分的字符都是占用 1 个字节的空间。
> 使用日语时不可能不使用平假名、片假名或者汉字,但只用一个字节来表示这些字符是不可能的。因此,为了表示这些字符,用两个字节表示一个字符的技术就诞生了。
> 不过,非常可惜的是日语的字符编码并不只有 1 种,而是大概可以分为以下 4 种编码方式,并且相同编码方式得到的字符也不一定相等。
<table border="1" data-line-num="337 338 339 340 341 342 343" width="90%"><thead><tr><th> <p class="表头单元格">编码方式</p> </th> <th> <p class="表头单元格">主要使用的地方</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格">Shift_JIS</p> </td> <td> <p class="表格单元格">Windows 文本</p> </td> </tr><tr><td> <p class="表格单元格">UTF-8</p> </td> <td> <p class="表格单元格">Unix 文本、HTML 等</p> </td> </tr><tr><td> <p class="表格单元格">EUC-JP</p> </td> <td> <p class="表格单元格">Unix 文本</p> </td> </tr><tr><td> <p class="表格单元格">ISO-2022-JP</p> </td> <td> <p class="表格单元格">电子邮件等</p> </td> </tr></tbody></table>
> 为字符分配与之对应的数值,这样的分配方式就称为字符编码方式(character encoding scheme)。在日本,常用的编码方式有 Shift_JIS、EUC-JP、ISO-2022-JP 这 3 种。它们是日语标准字符编码 JIS X0208 的基础。字符编码的名称一般都直接使用编码方式的名称。例如我们会说“这个文本的字符编码是 EUC,在 Windows 中打开的时候要小心”。
> 编码方式不同的情况下,即使是相同的字符,对其分配的数值也会不一样。编码方式的不同,就是导致俗称为“乱码”的问题的原因之一4。
<table border="1" data-line-num="348 349 350 351" width="90%"><thead><tr><th> <p class="表头单元格">字符</p> </th> <th> <p class="表头单元格">Shift_JIS</p> </th> <th> <p class="表头单元格">EUC-JP</p> </th> <th> <p class="表头单元格">ISO-2022-JP</p> </th> <th> <p class="表头单元格">UTF-8</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格">あ</p> </td> <td> <p class="表格单元格">82A0</p> </td> <td> <p class="表格单元格">A4A2</p> </td> <td> <p class="表格单元格">2422</p> </td> <td> <p class="表格单元格">E38182</p> </td> </tr></tbody></table>
> 在上表中,あ字符被分配的数值是用 16 进制表示的。据此可以看出,不同的编码方式所对应的数值是不一样的。像这样表示字符的值,我们称之为码位(code point)。在 Ruby 中可以用 `String#ord` 方法获取字符的码位。
~~~
#encoding: EUC-JP
p "あ".ord #=> 42146(16 进制为A4A2)
~~~
> 另外,虽然 ISO-2022-JP 中使用了与 ASCII 相同的编码,但实际上也使用了一些小技巧以与 ASCII 有所区别(这些小技巧比较复杂,本专栏就不深入说明了)。
> 最近,一种名为 Unicode 的新的国际化字符编码慢慢地普及了起来。其中,UTF-8 就是 Unicode 的一种编码方式。
> 但 Unicode 并不能解决所有编码问题,它的产生也只是增加了一个不能互相转换的字符编码而已。而且,Unicode 中还有多种编码方式,很有可能导致在意想不到的地方出现问题。现在市面上有很多详细介绍字符编码的专业著作,也比较容易找到,有兴趣的读者可以自行查找阅读。不过,关于字符编码的理论一来比较深奥,二来存在各种各样的立场,其中也不乏偏见(笔者愚见)。因此,要想深入了解字符编码,建议大家不要只看某一本著作,而应该集各家之言,互相比较着阅读。
4在计算机中用 GB 系列编码显示中文字符,最早的 GB 编码是 GB2312,接着有 GBK,现在最新的是 GB18030,每次扩展都完全保留之前版本的编码,因此每个新版本都可向下兼容。Windows 平台的中文字符使用是 GBK 编码,但在非 Windows 平台下的中文字符一般都使用 UTF-8 编码,因此在跨平台使用时要小心字符编码问题。——译者注
### **14.6 字符串的分割**
用特定字符分割字符串时可以使用 `split` 方法。例如,用冒号(`:`)分割字符串的程序就可以像下面那样写:
~~~
column = str.split(/:/)
~~~
这样一来,分割后的各项字符串就会以数组的形式赋值给 `column`。
~~~
str = "高桥:gaoqiao:1234567:000-123-4567"
column = str.split(/:/)
p column
#=> ["高桥", "gaoqiao", "1234567", "000-123-4567"]
~~~
### **14.7 换行符的使用方法**
用 `each_line` 等方法从标准输入读取字符串时,末尾肯定有换行符。然而,在实际处理字符串时,换行符有时候会很碍事。这种情况下,我们就需要删除多余的换行符(表 14.2)。
**表 14.2 删除换行符的方法**
<table border="1" data-line-num="384 385 386 387 388" width="90%"><thead><tr><th> <p class="表头单元格"> </p> </th> <th> <p class="表头单元格">删除最后一个字符</p> </th> <th> <p class="表头单元格">删除换行符</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格">非破坏性的</p> </td> <td> <p class="表格单元格"><code>chop</code></p> </td> <td> <p class="表格单元格"><code>chomp</code></p> </td> </tr><tr><td> <p class="表格单元格">破坏性的</p> </td> <td> <p class="表格单元格"><code>chop!</code></p> </td> <td> <p class="表格单元格"><code>chomp!</code></p> </td> </tr></tbody></table>
`chop` 方法与 `chop!` 方法会删除字符串行末的任何字符,`chomp` 方法与 `chomp!` 方法则只在行末为换行符时才将其删除。
~~~
str = "abcde" # 没有换行符的情况
newstr = str.chop
p newstr #=> "abcd"
newstr = str.chomp
p newstr #=> "abcde"
str2 = "abcd\n" # 有换行符的情况
newstr = str2.chop
p newstr #=> "abcd"
newstr = str2.chomp
p newstr #=> "abcd"
~~~
用 `each_line` 方法循环读取新的行时,一般会使用具有破坏性的 `chomp!` 方法直接删除换行符。
**`f.each_line do |line|`
`line.chomp!`
处理`line`
`end`**
上面是 `chom p!` 的典型用法。另外,不同的运行环境下,换行符也不同,关于这方面的内容,请参考专栏《关于换行》。
### **14.8 字符串的检索与置换**
字符串处理一般都离不开检索与置换。Ruby 可以很轻松地处理字符串。
### **14.8.1 字符串的检索**
我们可以用 `index` 方法或者 `rindex` 方法,来检查指定的字符串是否存在在某字符串中。
`index` 方法会从左到右检查字符串中是否存在参数指定的字符串,而 `rindex` 方法则是按照从右到左的顺序来检查(`rindex` 的“r”表示的就是 right(右)的意思)。
~~~
str = "ABBBBBB"
p str.index("BB") #=> 1
p str.rindex("BB") #=> 5
~~~
找到字符串时,`index` 方法和 `rindex` 方法会返回字符串首个字符的索引值,没找到时则返回 `nil`。
另外,如果只是想知道字符串中是否有参数指定的字符串,用 `include?` 方法会更好。
~~~
str = "ABBBBBB"
p str.include?("BB") #=> true
~~~
除了直接检索字符串外,Ruby 还可以使用正则表达式来检索。关于正则表达式,我们将在第 16 章中详细介绍。
> **专栏**
> **关于换行符**
> 所谓换行符就是指进行换行的符号。就像在专栏《字符编码》中介绍的那样,计算机里使用的字符都被分配了 1 个与之对应的数值,同理,换行符也有 1 个与之对应的数值。不过麻烦的是,不同的 OS 对换行符的处理也不同。
> 下面是常用的 OS 的换行符。这里,LF(LineFeed)的字符为 `\n`,CR(Carriage Return)的字符为 `\r`。
<table border="1" data-line-num="440 441 442 443 444 445" width="90%"><thead><tr><th> <p class="表头单元格">OS 种类</p> </th> <th> <p class="表头单元格">换行符</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格">Unix 系列</p> </td> <td> <p class="表格单元格">LF</p> </td> </tr><tr><td> <p class="表格单元格">Windows 系列</p> </td> <td> <p class="表格单元格">CR + LF</p> </td> </tr><tr><td> <p class="表格单元格">Mac OS 9 以前</p> </td> <td> <p class="表格单元格">CR</p> </td> </tr></tbody></table>
> Ruby 中的标准换行符为 LF,一般在 `IO#each_line` 等方法中使用。也就是说,Ruby 在处理 Max OS 9 以前版本的文本时,可能会出现不能正确换行的情况。
> 我们可以通过参数指定 `each_line` 方法所使用的换行符,默认为 `each_line("\n")`。
### **14.8.2 字符串的置换**
有时我们可能会需要用其他字符串来替换目标字符串中的某一部分。我们把这样的替换过程称为置换。用 `sub` 方法与 `gsub` 方法即可实现字符串的置换。
关于 `sub` 方法与 `gsub` 方法,我们将会在 16.6.1 节中详细介绍。
### **14.9 字符串与数组的共同方法**
字符串中有很多与数组共同的方法。
当然,继承了 `Object` 类的实例的方法,在字符串(`String` 类的实例)以及数组(`Array` 类的实例)中也都能使用。除此以外,下面的方法也都能使用。
**(a)与索引操作相关的方法**
**(b)与 `Enumerable` 模块相关的方法**
**(c)与连接、反转(`reverse`)相关的方法**
### **14.9.1 与索引操作相关的方法**
在 14.3 节中我们提到过字符串也能像数组那样操作索引。数组适用的索引操作方法,也同样适用于字符串。
-
***s*[*n*] = *str*
*s*[*n..m*] = *str*
*s*[*n, len*] = *str***
用 *str* 置换字符串 *s* 的一部分。这里 *n*、*m*、*len* 都是以字符为单位的。
~~~
str = "abcde"
str[2, 1] = "C"
p str #=> "abCde"
~~~
-
***s*.`slice!`(*n*)
*s*.`slice!`(*n..m*)
*s*.`slice!`(*n, len*)**
删除字符串 *s* 的一部分,并返回删除的部分。
~~~
str = "Hello, Ruby."
p str.slice!(-1) #=> "."
p str.slice!(5..6) #=> ", "
p str.slice!(0, 5) #=> "Hello"
p str #=> "Ruby"
~~~
### **14.9.2 返回 Enumerator 对象的方法**
在处理字符串的方法中,有以行为单位进行循环处理的 `each_line` 方法、以字节为单位进行循环处理的 `each_byte` 方法、以及以字符为单位进行循环处理的 `each_char` 方法。调用这些方法时若不带块,则会直接返回 `Enumerator` 对象,因此,通过使用这些方法,我们就可以像下面的例子那样使用 `Enumerable` 模块的方法了。
~~~
# 用 collect 方法处理用 each_line 方法获取的行
str = "壹\n 贰\n 叁\n"
tmp = str.each_line.collect do |line|
line.chomp 3
end
p tmp #=> ["壹壹壹", "贰贰贰", "叁叁叁"]
# 用 collect 方法处理用 each_byte 方法获取的数值
str = "abcde"
tmp = str.each_byte.collect do |byte|
-byte
end
p tmp #=> [-97, -98, -99, -100, -101]
~~~
> **专栏**
> **Enumerator 类**
> 虽然 `Enumerable` 模块定义了很多方便的方法,但是作为模块中其他方法的基础,将遍历元素的方法限定为 `each` 方法,这一点有些不太灵活。
> `String` 对象有 `each_byte`、`each_line`、`each_char` 等用于循环的方法,如果这些方法都能使用 `each_with_index`、`collect` 等 `Enumerable` 模块的方法的话,那就方便多了。而 `Enumerator` 类就是为了解决这个问题而诞生的。
> `Enumerator` 类能以 `each` 方法以外的方法为基础,执行 `Enumerable` 模块定义的方法。使用 `Enumerator` 类后,我们就可以用 `String#each_line` 方法替代 `each` 方法,从而来执行 `Enumerable` 模块的方法了。
> 另外,不带块的情况下,大部分 Ruby 原生的迭代器在调用时都会返回 `Enumerator` 对象。因此,我们就可以对 `each_line`、`each_byte` 等方法的返回结果继续使用 `map` 等方法。
~~~
str = "AA\nBB\nCC\n"
p str.each_line.class #=> Enumerator
p str.each_line.map{|line| line.chop }
#=> ["AA", "BB", "CC"]
p str.each_byte.reject{|c| c == 0x0a }
#=> [65, 65, 66, 66, 67, 67]
~~~
### **14.9.3 与连接、反转(reverse)相关的方法**
除了与 `Enumerable` 模块、索引等相关的方法外,字符串中还有一些与数组共同的方法。
-
***s*.`concat`(*s2*)
*s*+*s2***
与数组一样,字符串也能使用 `concat` 方法和 `+` 连接字符串。
~~~
s = "欢迎"
s.concat("光临")
p s #=> "欢迎光临"
~~~
-
***s*.`delete`(*str*)
*s*.`delete!`(*str*)**
从字符串 *s* 中删除字符串 *str*。
~~~
s = "防/ 止/ 检/ 索"
p s.delete("/") #=> "防止检索"
~~~
-
***s*.`reverse`
*s*.`reverse!`**
反转字符串 *s*。
~~~
s = "晚上好"
p s.reverse #=> "好上晚"
~~~
### **14.10 其他方法**
-
***s*.`strip`
*s*.`strip!`**
这是删除字符串 s 开头和末尾的空白字符的方法。在不需要字符串开头和末尾的空白时,用这个方法非常方便。
~~~
p " Thank you. ".strip #=> "Thank you."
~~~
-
***s*.`upcase`
*s*.`upcase!`
*s*.`downcase`
*s*.`downcase!`
*s*.`swapcase`
*s*.`swapcase!`
*s*.`capitalize`
*s*.`capitalize!`**
所谓 case 在这里就是指英文字母的大、小写字母的意思。`~case` 方法就是转换字母大小写的方法。
`upcase` 方法会将小写字母转换为大写,大写字母保持不变。
~~~
p "Object-Oriented Language".upcase
#=> "OBJECT-ORIENTED LANGUAGE"
~~~
`downcase` 方法则刚好相反,将大写字母转换小写。
~~~
p "Object-Oriented Language".downcase
#=> "object-oriented language"
~~~
`swapcase` 方法会将大写字母转换为小写,将小写字母转换为大写。
~~~
p "Object-Oriented Language".swapcase
#=> "oBJECT-oRIENTED lANGUAGE"
~~~
`capitalize` 方法会将首字母转换为大写,将其余的字母转换为小写。
~~~
p "Object-Oriented Language".capitalize
#=> "Object-oriented language"
~~~
-
***s*.`tr`
*s*.`tr!`**
源自于 Unix 的 tr 命令的方法,用于置换字符。
该方法与 `gsub` 方法有点相似,不同点在于 `tr` 方法可以像 `s.tr("a-z", "A-Z")` 这样一次置换多个字符。
~~~
p "ABCDE".tr("B", "b") #=> "AbCDE"
p "ABCDE".tr("BD", "bd") #=> "AbCdE"
p "ABCDE".tr("A-E", "a-e") #=> "abcde"
~~~
相反,`tr` 方法不能使用正则表达式,也不能指定两个字符以上的字符串。
### **14.11 日语字符编码的转换**
字符编码转换有两种方法,分别是使用 `encode` 方法和使用 `nkf` 库的方法。
### **14.11.1 encode 方法**
`encode` 方法是 Ruby 中基本的字符编码转换的方法。将字符编码由 EUC-JP 转换为 UTF-8,程序可以像下面这样写 5 :
5简体中文的 GBK、GB2312 等编码也可以用同样的方法来转换。——译者注
~~~
# encoding: EUC-JP
euc_str = "日语EUC 编码的字符串"
utf8_str = euc_str.encode("utf-8")
~~~
另外,Ruby 中还定义了具有破坏性的 `encode!` 方法。
~~~
# encodng: EUC-JP
str = "日语EUC 编码的字符串"
str.encode!("utf-8") # 将str 转换为UTF-8
~~~
`encode` 方法支持的字符编码,可通过 `Encoding.name_list` 方法获得。
### **14.11.2 nkf 库**
使用 `encode` 方法可以进行字符编码的转换,但却不能进行半角假名与全角假名之间的转换。全半角假名的转换我们需要使用 `nkf` 库。
`nkf` 库由 `NKF` 模块提供。`NKF` 模块是 Unix 的 nkf(Network Kanji code conversion Filter)过滤命令在 Ruby 中的实现。
NKF 模块用类似于命令行选项的字符串指定字符编码等。
**`NKF.nkf`( 选项字符串, 转换的字符串)**
nkf 方法的主要参数如表 14.3 所示。
**表 14.3 nkf 的主要参数**
<table border="1" data-line-num="630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664" width="90%"><thead><tr><th> <p class="表头单元格">选项</p> </th> <th> <p class="表头单元格">意义</p> </th> </tr></thead><tbody><tr><td> <p class="表格单元格"><code>-d</code></p> </td> <td> <p class="表格单元格">从换行符中删除 CR</p> </td> </tr><tr><td> <p class="表格单元格"><code>-c</code></p> </td> <td> <p class="表格单元格">往换行符中添加 CR</p> </td> </tr><tr><td> <p class="表格单元格"><code>-x</code></p> </td> <td> <p class="表格单元格">不把半角假名转换为全角假名</p> </td> </tr><tr><td> <p class="表格单元格"><code>-m0</code></p> </td> <td> <p class="表格单元格">抑制 MIME 处理</p> </td> </tr><tr><td> <p class="表格单元格"><code>-h1</code></p> </td> <td> <p class="表格单元格">把片假名转换为平假名</p> </td> </tr><tr><td> <p class="表格单元格"><code>-h2</code></p> </td> <td> <p class="表格单元格">把平假名转换为片假名</p> </td> </tr><tr><td> <p class="表格单元格"><code>-h3</code></p> </td> <td> <p class="表格单元格">互换平假名与片假名</p> </td> </tr><tr><td> <p class="表格单元格"><code>-Z0</code></p> </td> <td> <p class="表格单元格">把 JIS X 0208 的数字转换为 ASCII</p> </td> </tr><tr><td> <p class="表格单元格"><code>-Z1</code></p> </td> <td> <p class="表格单元格">加上 -Z0,把全角空格转换为半角空格</p> </td> </tr><tr><td> <p class="表格单元格"><code>-Z2</code></p> </td> <td> <p class="表格单元格">加上 -Z0,把全角空格转换为两个半角空格</p> </td> </tr><tr><td> <p class="表格单元格"><code>-e</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 EUC-JP</p> </td> </tr><tr><td> <p class="表格单元格"><code>-s</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 Shift-JIS</p> </td> </tr><tr><td> <p class="表格单元格"><code>-j</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 ISO-2022-JP</p> </td> </tr><tr><td> <p class="表格单元格"><code>-w</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 UTF-8(无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-w8</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 UTF-8(有 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-w80</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 UTF-8(无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-w16</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 UTF-16(Big Endian/ 无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-w16B</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 UTF-16(Big Endian/ 有 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-w16B0</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 UTF-16(Big Endian/ 无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-w16L</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 UTF-16(Little Endian/ 有 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-w16L0</code></p> </td> <td> <p class="表格单元格">输出的字符编码为 UTF-16(Little Endian/ 无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-E</code></p> </td> <td> <p class="表格单元格">输入字符编码为 EUC-JP</p> </td> </tr><tr><td> <p class="表格单元格"><code>-S</code></p> </td> <td> <p class="表格单元格">输入字符编码为 Shift-JIS</p> </td> </tr><tr><td> <p class="表格单元格"><code>-J</code></p> </td> <td> <p class="表格单元格">输入字符编码为 ISO-2022-JP</p> </td> </tr><tr><td> <p class="表格单元格"><code>-W</code></p> </td> <td> <p class="表格单元格">输入的字符编码为 UTF-8(无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-W8</code></p> </td> <td> <p class="表格单元格">输入的字符编码为 UTF-8(有 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-W80</code></p> </td> <td> <p class="表格单元格">输入的字符编码为 UTF-8(无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-W16</code></p> </td> <td> <p class="表格单元格">输入的字符编码为 UTF-16(Big Endian/ 无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-W16B</code></p> </td> <td> <p class="表格单元格">输入的字符编码为 UTF-16(Big Endian/ 有 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-W16B0</code></p> </td> <td> <p class="表格单元格">输入的字符编码为 UTF-16(Big Endian/ 无 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-W16L</code></p> </td> <td> <p class="表格单元格">输入的字符编码为 UTF-16(Little Endian/ 有 BOM)</p> </td> </tr><tr><td> <p class="表格单元格"><code>-W16L0</code></p> </td> <td> <p class="表格单元格">输入的字符编码为 UTF-16(Little Endian/ 无 BOM)</p> </td> </tr></tbody></table>
为了避免半角假名转换为全角假名,或者因电子邮件的特殊字符处理而产生问题,如果只是单纯对字符进行编码转换的话,一般使用选项 `-x` 和 `-m0`(可以合并书写 `-xm0`)就足够了。
下面是把 EUC-JP 字符串转换为 UTF-8 的例子:
~~~
# encoding: EUC-JP
require "nkf"
euc_str = "日语EUC 编码的字符串"
utf8_str = NKF.nkf("-E -w -xm0", euc_str)
~~~
不指定输入字符编码时,`nkf` 库会自动判断其编码,基本上都可以按如下方式书写:
~~~
# encoding: EUC-JP
require "nkf"
euc_str = "日语EUC 编码的字符串"
utf8_str = NKF.nkf("-w -xm0", euc_str)
~~~
`NKF` 模块在 Ruby 字符串还未支持 encoding 功能以前就已经开始被使用了。大家可能会感觉选项的指定方法等与现在 Ruby 的风格有点格格不入,这是因为 nkf 库把其他命令的功能硬搬了过来的缘故。如果不涉及一些太特殊的处理,一般使用 `encode` 就足够了。
### **练习题**
1. 有字符串 `"Ruby is an object oriented programming language"`,请创建一个数组,数组元素为这个字符串的各个单词。
2. 有将 1 的数组按照英文字母顺序进行排序。
3. 有不区分大小写,将 2 的数组按照英文字母顺序进行排序。
4. 有把 1 的字符串的全部单词的首字母转换为大写,转换后的结果为 `"Ruby Is A n Object Oriented Programming Language"`。
5. 有统计 1 的字符串中的字符及其数量,并按下面的形式输出结果(空格字符 6 个,`'R'`1 个,`'a'`4 个等等)。
~~~
' ': ******
'R': *
'a': ****
'b': **
'c': *
┊
~~~
6. 定义方法 `han2num`, 将汉字数字转换为 1 ~ 9999 的阿拉伯数字形式,例如将“七千一百二十三”转换为“`7123`”。
> 参考答案:请到图灵社区本书的“随书下载”处下载([http://www.ituring.com.cn/book/1237](http://www.ituring.com.cn/book/1237))。
- 推荐序
- 译者序
- 前言
- 本书的读者对象
- 第 1 部分 Ruby 初体验
- 第 1 章 Ruby 初探
- 第 2 章 便利的对象
- 第 3 章 创建命令
- 第 2 部分 Ruby 的基础
- 第 4 章 对象、变量和常量
- 第 5 章 条件判断
- 第 6 章 循环
- 第 7 章 方法
- 第 8 章 类和模块
- 第 9 章 运算符
- 第 10 章 错误处理与异常
- 第 11 章 块
- 第 3 部分 Ruby 的类
- 第 12 章 数值类
- 第 13 章 数组类
- 第 14 章 字符串类
- 第 15 章 散列类
- 第 16 章 正则表达式类
- 第 17 章 IO 类
- 第 18 章 File 类与 Dir 类
- 第 19 章 Encoding 类
- 第 20 章 Time 类与 Date 类
- 第 21 章 Proc 类
- 第 4 部分 动手制作工具
- 第 22 章 文本处理
- 第 23 章 检索邮政编码
- 附录
- 附录 A Ruby 运行环境的构建
- 附录 B Ruby 参考集
- 后记
- 谢辞