# **第 22 章 文本處理**
在本章中,我們會以在第 3 章中創建的 simple_grep.rb 為基礎,來學習文本的一般處理方法。
這里,我們將會創建實現以下功能的腳本。
-
**獲取 HTML 文件并進行簡單的加工**
-
**查找單詞并顯示其出現的次數**
-
**強調查找結果并對輸出結果進行加工**
### **22.1 準備文本**
首先是準備作為處理對象的文本。
### **22.1.1 下載文件**
這里,我們以山形浩生翻譯的 *The Cathedral and the Bazaar*1 為例進行說明,該翻譯版本已在網上公開并可自由使用。這是一篇非常著名的有關開源項目(Open Source Project)開發模式的論文,有興趣的讀者不妨一讀。
1中文簡體版名為《大教堂與集市》,由機械工業出版社于 2014 年出版,衛劍釩譯。——譯者注
*The Cathedral and the Bazaar* 翻譯版的 URL 如下所示。
- [http://cruel.org/freeware/cathedral.html](http://cruel.org/freeware/cathedral.html)
雖然也可以使用瀏覽器訪問、下載上面的 URL,不過既然已經學習了 Ruby,下面就讓我們來試試用 Ruby 下載。
**代碼清單 22.1 get_cathedral.rb**
~~~
require "open-uri"
require "nkf"
url = "http://cruel.org/freeware/cathedral.html"
filename = "cathedral.html"
File.open(filename, "w") do |f|
text = open(url).read
f.write text # UTF-8 環境下使用此段代碼
#f.write NKF.nkf("-s", text) # Shift_JIS 環境下(日語Windows)使用此段代碼
end
~~~
程序在處理日語字符的時候需要注意編碼問題 2。特別是從外部的輸入,全部都用一樣的編碼也無可非議。在日語 Windows 環境中,由于是用 Shift_JIS 編碼傳遞字符給命令行,因此文件也采用了相同的編碼。本例中的 HTML 文件的編碼為 UTF-8,因此如果是日語 Windows 環境的話,就需要用 NFK 轉換為 Shift_JIS,然后再用 `write` 方法輸出(請修改并執行代碼清單 22.1 中注釋了的代碼部分)。
2中文也同樣需要注意。——譯者注
### **22.1.2 獲取正文**
從代碼清單 22.1 得到的是用于在瀏覽器中顯示的 HTML 文件。這個文件中有很多像頭部、底部這樣的我們不需要的部分,因此這里我們只把正文部分抽取出來。
首先必須定義好正文的起始位置與結束位置。而為此就必須先看看 HTML 里的內容。下面,我們來好好研究一下剛才下載的 cathedral.html。
~~~
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/
strict.dtd">
<html lang="ja"><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="Author" content="Eric Raymond, YAMAGATA Hiroo">
┊
<hr />
<h2><a name="1">1 伽藍方式とバザール方式</a></h2>
<p>
Linux
は破壊的存在なり。インターネットのかぼそい糸だけで結ばれた、地球全體に散らばった數千人の開発者たちが片手間にハッキングするだけで、超一流の OS が魔法みたいに編み出されてしまうなんて、ほんの 5 年前でさえだれも想像すらできなかったんだから。</p>
┊
</p><hr>
<h2><a name="version">バージョンと変更履歴</a></h2>
<p>
$Id: cathedral-bazaar.sgml,v 1.40 1998/08/11 20:27:29 esr Exp $</p>
┊
~~~
通過上面的內容可以看出,以“`<h2><a name="1">1 伽藍方式とバザール方式</a></h2>`”開始的行就是正文的開始。
同樣,從“`<h2><a name="version"> バージョンと変更履歴 </a></h2>`”這一行開始則為底部,之后的內容與正文無關。
然后,對這兩行做上標記,把正文部分摘取出來。
**代碼清單 22.2 cut_cathedral.rb**
~~~
1: htmlfile = "cathedral.html"
2: textfile = "cathedral.txt"
3:
4: html = File.read(htmlfile)
5:
6: File.open(textfile, "w") do |f|
7: in_header = true
8: html.each_line do |line|
9: if in_header && /<a name="1">/ !~ line
10: next
11: else
12: in_header = false
13: end
14: break if /<a name="version">/ =~ line
15: f.write line
16: end
17: end
~~~
在這個腳本中,以包含字符串 `<a name="1">` 的行作為開始,包含字符串 `<a name = "version">` 的行作為結束,把中間的內容保存到 catedral.txt 文件中。
首先,使用 `File.read` 方法讀取 HTML 文件的全部內容。
接下來,對 HTML 文件的字符串使用 `each_line` 方法,逐行讀取內容并賦值給變量 `line`,然后再將其保存到文件中。不過,在保存之前,需要先把 `in_header` 變量設為 `true`。這個變量可被用于檢查正在處理的行是否為頭部。第 9 行的 `if` 語句會利用這個變量值進行判斷,如果是在頭部內,并且正在讀取的行不包含 `<a name="1">` 則跳出本次循環。而除此以外的情況下,則表示已經離開了頭部部分,因此就要將 `in_header` 設置為 `false`。這樣一來,從下個循環開始就不會再執行 next 了。
第 14 行中使用了 `if` 修飾符。break if…這樣的形式常被用于跳出循環。這種寫法的優點在于可以緊湊地書寫不太長的 `if` 條件。在這里,程序會判斷是否為表示正文結束的行,成功匹配則跳出循環。
接下來是第 15 行,程序能走到這里就證明 `line` 是正文部分,因此,使用 `write` 方法將 `line` 的內容輸出到文件。
### **22.1.3 刪除標簽**
但是輸出到文件的正文部分中還殘留著 HTML 標簽(tag)。雖然即使有 HTML 標簽也可以進行文本處理,但在本章中并不需要標簽。因此接下來我們就來考慮一下如何把標簽刪除,以獲取純文本(plain text)格式的文件。
一般情況下,刪除 HTML 標簽,可以考慮使用解析 HTML 用的類庫,不過在本例中,我們只是單純地通過正則表達式來置換。
**代碼清單 22.3 cut_cathedral2.rb**
~~~
1: require 'cgi/util'
2: htmlfile = "cathedral.html"
3: textfile = "cathedral.txt"
4:
5: html = File.read(htmlfile)
6:
7: File.open(textfile, "w") do |f|
8: in_header = true
9: html.each_line do |line|
10: if in_header && /<a name="1">/ !~ line
11: next
12: else
13: in_header = false
14: end
15: break if /<a name="version">/ =~ line
16: line.gsub!(/<[^>]+>/, '')
17: esc_line = CGI.unescapeHTML(line)
18: f.write esc_line
19: end
20: end
~~~
代碼清單 22.3 在代碼 22.2 的基礎上添加了刪除標簽的功能,而實際兩者只有第 16 行和第 17 行代碼是不一樣的。
在第 16 行中,用正則 表達式 `/<[^>]+>/` 表示標簽。由于 HTML 標簽是以 `<` 開始,以 `>` 結束的,因此這樣就可以匹配標簽部分。在第 17 行中,使用 `CGI.unescapeHTML` 方法,將 HTML 標簽的 `&`、`<` 等轉義字符,轉換為普通字符 `&`、`<` 等。通過在第 1 行追加 `require 'cgi/util'`,我們就可以使用這個方法了。
做出這樣的修改后,我們就可以得到以下文本:
~~~
1 伽藍方式とバザール方式
Linux
は破壊的存在なり。インターネットのかぼそい糸だけで結ばれた、地球全體に散らばった數千人の開発者たちが片手間にハッキングするだけで、超一流の OS が魔法みたいに編み出されてしまうなんて、ほんの 5 年前でさえだれも想像すらできなかったんだから。
┊
~~~
### **22.2 擴展 simple_grep.rb :顯示次數**
接下來,我們來看看 simple_grep.rb。這里對第 3 章中的例子稍微做了一點修改(代碼清單 22.4)。
**代碼清單 22.4 simple_grep.rb**
~~~
pattern = Regexp.new(ARGV[0])
filename = ARGV[1]
File.open(filename) do |file|
file.each_line do |line|
if pattern =~ line
print line
end
end
end
~~~
由于我們在 `File.open` 方法中使用了塊,因此 `File#close` 就不再需要了,這樣一來,程序也清爽了好多。
利用這個程序,我們來調查一下正文中的“伽藍”3 以及“バザール”4 這兩個單詞總共出現了多少次。
3中文是“大教堂”的意思。——譯者注
4中文是“集市”的意思。——譯者注
### **計算匹配行**
由于通過 simple_grep.rb 匹配的行會原封不動地顯示出來,因此 Mac OS X 或 Linux 的情況下,就可以配合 wc 命令 5 查看文本的行數。
5wc 即 word count 的縮寫。——譯者注
> **執行示例**
~~~
> ruby simple_grep.rb ' 伽藍' cathedral.txt | wc
20 79 4352
> ruby simple_grep.rb ' バザール' cathedral.txt | wc
38 122 8376
~~~
但是,我們不能確切地說正文中“伽藍”出現了 20 次,這是因為如果 1 行中該單詞出現了多次的話,那么只通過計算行數是不能得出正確的結果的。
下面我們來改造這個程序,用 `String#scan` 方法,計算匹配的次數。
**代碼清單 22.5 simple_scan.rb**
~~~
pattern = Regexp.new(ARGV[0])
filename = ARGV[1]
count = 0
File.open(filename) do |file|
file.each_line do |line|
if pattern =~ line
line.scan(pattern) do |s|
count += 1
end
print line
end
end
end
puts "count: #{count}"
~~~
> **執行示例**
~~~
> ruby simple_scan.rb ' 伽藍' cathedral.txt
1 伽藍方式とバザール方式
みたいな本當に大規模なツール)は伽藍のように組み立てられなきゃダメで、一人のウィザードか魔術師の小集団が、まったく孤立して慎重に組み立てあげるべ
┊
のほうは、閉じた開発者グループとめったにないリリースとでもっと伽藍的な開発方式を続けたということだった。
count: 21
> ruby simple_scan.rb ' バザール' cathedral.txt
1 伽藍方式とバザール方式
コミュニティはむしろ、いろんな作業やアプローチが渦を巻く、でかい騒がしいバザールに似ているみたいだった(これをまさに象徴しているのが
┊
が意識的に、ぼくがこれまでに記述してきたバザール戦術を用い、それに対して GCC
count: 43
~~~
結果顯示,“伽藍”出現了 21 次,“バザール”出現了 43 次。
如果不需要逐行處理,而只是單純計算出現次數的話,則有更簡單的實現方法(代碼清單 22.6)。
**代碼清單 22.6 simple_count.rb**
~~~
pattern = Regexp.new(ARGV[0])
filename = ARGV[1]
count = 0
File.read(filename).scan(pattern) do |s|
count += 1
end
puts "count: #{count}"
~~~
由于 `String#scan` 方法是對字符串使用的方法,因此,本例中沒有使用 `File.open` 方法,而是通過 `File.read` 方法一次性讀取了所有內容并將其保存為了字符串。
### **22.3 擴展 simple_grep.rb :顯示匹配的部分**
下面,讓我們再次回到原來的 simple_scan.rb 來繼續進行改造。
### **22.3.1 突出匹配到的位置**
雖然顯示出了匹配的行,但具體匹配到的位置卻很難看清楚。因此下面我們就來試試在顯示的時候強調匹配的部分(代碼清單 22.7)。
**代碼清單 22.7 simple_match.rb**
~~~
1: pattern = Regexp.new(ARGV[0])
2: filename = ARGV[1]
3:
4: count = 0
5: File.open(filename) do |file|
6: file.each_line do |line|
7: if pattern =~ line
8: line.scan(pattern) do |s|
9: count += 1
10: end
11: print line.gsub(pattern){|str| "<<#{str}>>"}
12: end
14: end
15: end
16: puts "count: #{count}"
~~~
在第 11 行,使用 `gsub` 方法將原來直接輸出變量 `line` 的地方進行轉換后再輸出。由于對 `gsub` 方法使用塊后,匹配部分就可以通過塊變量取得,因此這里在匹配部分的前后加上 `<<>>` 并返回。
執行結果如下所示:
> **執行示例**
~~~
> ruby simple_match.rb ' 伽藍' cathedral.txt
1 << 伽藍>> 方式とバザール方式
みたいな本當に大規模なツール)は<< 伽藍>> のように組み立てられなきゃダメで、一人のウィザードか魔術師の小集団が、まったく孤立して慎重に組み立てあげるべ
┊
のほうは、閉じた開発者グループとめったにないリリースとでもっと<< 伽藍>> 的な開発方式を続けたということだった。
count: 21
~~~
可以看出,與之前相比,匹配部分被突出顯示了。
### **22.3.2 顯示前后各 10 個字符**
然而,在上述 simple_match.rb 的執行結果中,匹配部分在行中的位置比較分散,這時我們就希望顯示效果能更加緊湊些。例如顯示匹配部分及其前后各 10 個字符(代碼清單 22.8)。
**代碼清單 22.8 simple_match2.rb**
~~~
pattern = Regexp.new("(.{10})("+ARGV[0]+")(.{10})")
filename = ARGV[1]
count = 0
File.open(filename) do |file|
file.each_line do |line|
line.scan(pattern) do |s|
puts "#{s[0]}<<#{s[1]}>>#{s[2]}"
count += 1
end
end
end
puts "count: #{count}"
~~~
正則表達式中的 {n} 表示重復之前的模式 n 次。因此,本例中的 `.{10}` 就表示匹配 10 個任意字符。除此以外,還可以用 {n,m} 匹配 n 次以上 m 次以下,用 {n,} 匹配 n 次以上,用 {,m} 匹配 m 次以下。
接下來,我們來看看效果如何。
> **執行示例**
~~~
> ruby simple_match2.rb ' 伽藍' cathedral.txt
に大規模なツール)は<< 伽藍>> のように組み立てられ
された。靜かで荘厳な<< 伽藍>> づくりなんかない――
、それどころかなぜ、<< 伽藍>> 建設者たちの想像を絶
F ツールみたいな、<< 伽藍>> 建築方式にくらべると
┊
count: 12
~~~
通過 `count: 12` 可以看出,出現的次數減少了。這是因為匹配部分前后不夠 10 個字符時就不會匹配。另外,即使是 10 個字符,英文數字(ASCII)的字符長度與日語字符也不一樣,這就導致了輸出結果排列不整齊。因此我們再來修改一下(代碼清單 22.9)。
**代碼清單 22.9 simple_match3.rb**
~~~
1: pattern = Regexp.new("(.{0,10})("+ARGV[0]+")(.{0,10})")
2: filename = ARGV[1]
3:
4: count = 0
5: File.open(filename) do |file|
6: file.each_line do |line|
7: line.scan(pattern) do |s|
8: prefix_len = 0
9: s[0].each_char do |ch|
10: if ch.ord < 128
11: prefix_len += 1
12: else
13: prefix_len += 2
14: end
15: end
16: space_len = 20 - prefix_len
17: puts "#{" "*space_len}#{s[0]}<<#{s[1]}>>#{s[2]}"
18: count += 1
19: end
20: end
21: end
22: puts "count: #{count}"
~~~
修改程序第 1 行的正則表達式,將原來的匹配 10 個字符的 `{10}` 的地方,修改為匹配 0 個以上 10 個以下字符的 `{0,10}`。另外,在程序第 9 行以后,使用 `each_char` 方法逐個讀取字符,并通過 `ord` 方法獲取字符編碼的碼位。由于碼位小于 128 時即為 ASCII 碼,這時將長度加 1,除此以外的情況下則加 2,這些都是為了確定空白個數 `space_len` 以確保 20 個字符。然后再在 `s[0]` 之前留出與字符數相應的空白,這樣輸出結果就整齊多了。
> **執行示例**
~~~
> ruby simple_match3.rb ' 伽藍' cathedral.txt
1 << 伽藍>> 方式とバザール方式
に大規模なツール)は<< 伽藍>> のように組み立てられ
された。靜かで荘厳な<< 伽藍>> づくりなんかない――
、それどころかなぜ、<< 伽藍>> 建設者たちの想像を絶
F ツールみたいな、<< 伽藍>> 建築方式にくらべると
この信念のおかげで、<< 伽藍>> 建設式の開発への関與
自體が、FSF 式の<< 伽藍>> 建設型開発モデルの問
ープンな開発方針は、<< 伽藍>> 建設の正反対のものだ
ここに、<< 伽藍>> 建築方式とバザール式
┊
count: 21
~~~
可以看出,這次的結果整齊了許多。
### **22.3.3 讓前后的字符數可變更**
現在我們默認前后的字符只能是 10 個。不過這個數量如果能修改,那就靈活多了。于是,我們再次改造了程序(代碼清單 22.10)。
**代碼清單 22.10 simple_match4.rb**
~~~
1: len = ARGV[2].to_i
2: pattern = Regexp.new("(.{0,#{len}})("+ARGV[0]+")(.{0,#{len}})")
3: filename = ARGV[1]
4:
5: count = 0
6: File.open(filename) do |file|
7: file.each_line do |line|
8: line.scan(pattern) do |s|
9: prefix_len = 0
10: s[0].each_char do |ch|
11: if ch.ord < 128
12: prefix_len += 1
13: else
14: prefix_len += 2
15: end
16: end
17: space_len = len * 2 - prefix_len
18: puts "#{" " * space_len}#{s[0]}<<#{s[1]}>>#{s[2]}"
19: count += 1
20: end
21: end
22: end
23: puts "count: #{count}"
~~~
將指定長度的位置換為變量 `len`,可以通過 `ARGV[2]` 將其作為第 3 個參數來指定。
這里,我們指定為 5 個字符,來看看執行結果如何。
> **執行示例**
~~~
> ruby simple_match4.rb ' 伽藍' cathedral.txt 5
1 << 伽藍>> 方式とバザ
ツール)は<< 伽藍>> のように組
かで荘厳な<< 伽藍>> づくりなん
ろかなぜ、<< 伽藍>> 建設者たち
みたいな、<< 伽藍>> 建築方式に
おかげで、<< 伽藍>> 建設式の開
SF 式の<< 伽藍>> 建設型開発
┊
とでもっと<< 伽藍>> 的な開発方
count: 21
~~~
從結果可以看出,前后的字符串都變為了 5 個字符。
正如本章所演示的那樣,制作工具的時候,有效的做法是,首先由簡單的開始,然后再慢慢地接近目標功能。即使是很難馬上解決的問題,我們也可以將其細化,來逐個擊破。
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 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 參考集
- 后記
- 謝辭