# **第 23 章 檢索郵政編碼**
正式進行數據檢索時一般都需要用到一些專門的框架或者應用程序,但如果只是對手頭的數據加以整理以方便處理的話,根據實際情況做個簡易的小工具也未嘗不可。在本章中,作為 Ruby 的應用示例,我們來看看如何檢索郵政編碼。
### **23.1 獲取郵政編碼**
日本的郵政編碼可在郵局的官方網站上下載。
-
**郵政編碼檢索**:[http://www.post.japanpost.jp/zipcode](http://www.post.japanpost.jp/zipcode)
-
**郵政編碼下載**:[http://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html](http://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html)
-
**郵政編碼說明**:[http://www.post.japanpost.jp/zipcode/dl/readme.html](http://www.post.japanpost.jp/zipcode/dl/readme.html)
下載后的文件格式是 zip,用 zip 工具解壓后就可以得到格式為 CSV、編碼為 Shift_JIS 的全日本的郵政編碼文件 `KEN_ALL.CSV`。
> **備注** 所謂 CSV 格式是指,`"aaa"`,`”bb”`,`”ccccc”` 這樣的值之間用逗號(`,`)做間隔的數據格式。
關于 CSV 中各個字段的含義,可在郵政編碼的說明頁面查詢。前 9 個字段的含義如下所示。
① **日本地方公共團體編碼(JIS X0401、X0402):半角數字**
② **(舊)郵政編碼(5 位):半角數字**
③ **郵政編碼(7 位):半角數字**
④ **都道府縣名:半角片假名(按編碼順序排序)**
⑤ **市區町村名:半角片假名(按編碼順序排序)**
⑥ **町域名:半角片假名(按五十音順序排序)**
⑦ **都道府縣名:漢字(按編碼順序排序)**
⑧ **市區町村名:漢字(按編碼順序排序)**
⑨ **町域名:漢字(按五十音順序排序)**
另外,第 13 個字段表示的是“1 個郵政編碼表示多個町域”的情況,當這個值為 1 時,表示同一個郵政編碼會出現在多條數據中。
下面是實際文件中的 1 條數據,可以看出,各項目之間用逗號(`,`)做間隔,除了開頭和末尾的項目外,其他都用””括了起來。
~~~
01101,”060 “,”0600042”,”???????","????????????","????????(1-19 ????)","北海道","札幌市中央區","大通西(1?19丁目)",1,0,1,0,0,0
~~~
### **23.2 檢索郵政編碼**
首先我們先寫一個簡單的檢索程序,顯示郵政編碼所對應的數據(代碼清單 23.1)。為了獲知處理耗時,我們會獲取程序的開始和結束時間,并輸出兩者的差。在筆者的計算機上,整個處理過程大概用了 2 秒。
**代碼清單 23.1 split_zip.rb**
~~~
code = ARGV[0]
start_time = Time.now # 獲取處理開始的時間
File.open("KEN_ALL.CSV","r:shift_jis").each_line do |line|
line.chomp!
rows = line.split(/,/)
zipcode = rows[2].gsub(/"/,'')
if zipcode == code
puts line.encode('utf-8')
end
end
p Time.now - start_time # 輸出處理結束時間與開始時間的差
~~~
通過代碼 23.1 可以看出,按照從文件開頭逐行讀取、檢索一致的郵政編碼這種形式,遍歷所有的行需要花幾秒時間。這種做法很實用,接下來我們再來看看有沒有更快的處理辦法。
### **23.3 sqlite3 庫**
為了加快數據處理的速度,我們可以使用數據庫。這次我們使用開源的關系型數據庫 SQLite 以及操作數據庫用的語言 SQL,來對數據進行檢索、更新等操作。由于 SQLite 的第 3 版是最新版本,因此有時我們會直接稱之為 SQLite3。
- **SQLite 官方網站**:[http://www.sqlite.org/](http://www.sqlite.org/)
用 Ruby 操作 SQLite3,需要使用 `SQLite3` 庫。這里我們利用 gem 進行安裝。關于 gem 的內容,請參考 B.1 節。
> **執行示例**
~~~
> gem install sqlite3
~~~
> **注** sqlite3 的 gem,在 Windows 中是經過編譯并已發布的二進制包,而在 Linux、Mac OS X 中,除了安裝對應發行版的包以外,還需要執行 gem 命令。有關安裝 sqlite3 的 gem 方面內容,我們已經在本書的官方網站([http://www.notwork.org/tanoshiiruby4/](http://www.notwork.org/tanoshiiruby4/))中做了補充,讀者可以參考。
數據庫中的數據是以“表”為單位管理的。1 張數據庫表就像 1 個 CSV 文件,表中有多行數據,每行數據中有多個項目。CSV 文件的格式恰好與數據庫表存放數據時的結構是一致的。在數據庫中創建這樣的數據庫表,我們就可以將數據保存到表中,使用 SQL 對數據進行插入、更新、刪除等操作。
讓我們先看看用 SQLite3 處理數據的例子。在保存數據前,首先需要準備保存數據用的表。這里,我們對名為 mydb.db 的數據庫文件創建 `ADDRBOOK` 表,用于保存名字與住址。以下是創建表的程序。
~~~
SQLite3::Database.open “mydb.db” do |db|
db.execute “CREATE TABLE ADDRBOOK (name TEXT, address TEXT)”
end
~~~
本書只用了 SQLite3 中的個別功能,實際只使用了兩個方法:一個是 SQLite3::Database 類的類方法 open 方法,另外一個也是 SQLite3::Database 類的實例方法 `execute` 方法。
`SQLite3::Database.open` 方法的第 1 個參數為數據庫的文件名。第 2 行的 `Database#execute` 方法,用于執行在 mydb.db 中創建新表 `ADDRBOOK` 的 `CREATE TABLE` 語句。接著在表中定義 了 `name`、`email`、`address` 和 tel 四個字段。1 為了可以保存任何長度的字符串,各字段的類型都定義為沒有長度限制的 TEXT 類型。創建表后,像下面那樣對表插入數據。
1在上面的例子中實際只定義了 name、address 兩個字段。——譯者注
~~~
SQLite3::Database.open “mydb.db” do |db|
db.execute “INSERT INTO ADDRBOOK VALUES (?, ?), [col1, col2]”
end
~~~
第 1 行的 `SQLite3::Database.open` 方法和之前的一樣。第 2 行的 `Database#execute` 方法執行的是 `INSERT` 語句,這是向數據庫插入數據的 SQL。(?, ?) 表示表中的字段,分別用 `col1` 及 `col2` 對這兩個字段進行賦值。
最后,用以下方法讀取數據。
~~~
SQLite3::Database.open “mydb.db” do |db|
db.execute(“SELECT * FROM ADDRBOOK”) do |rows|
p rows
end
end
~~~
同樣是執行 `execute` 方法,不過參數的字符串要變為 SQL 的 `SELECT` 語句。另外,`execute` 方法可以使用塊,塊變量就是數組形式的 SQL 的執行結果。
關于 SQLite 的更詳細的資料,請參考 SQLite 手冊([http://www.sqlite.org](http://www.sqlite.org))。
### **23.4 插入數據**
接下來介紹的功能,都通過封裝為 `JZipCode` 類的方法來實現。
首先需要設計郵政編碼表的構成。這里,我們簡單地設計為下面那樣的表。
**表 23.1 郵政編碼檢索表**
<table border="1" data-line-num="116 117 118 119 120" width="90%"><thead><tr><th> <p class="表頭單元格">?</p> </th> <th> <p class="表頭單元格">郵政編碼</p> </th> <th> <p class="表頭單元格">都道府縣名</p> </th> <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</p> </td> <td> <p class="表格單元格">pref</p> </td> <td> <p class="表格單元格">city</p> </td> <td> <p class="表格單元格">address</p> </td> <td> <p class="表格單元格">alladdress</p> </td> </tr><tr><td> <p class="表格單元格">數據類型</p> </td> <td> <p class="表格單元格">TEXT</p> </td> <td> <p class="表格單元格">TEXT</p> </td> <td> <p class="表格單元格">TEXT</p> </td> <td> <p class="表格單元格">TEXT</p> </td> <td> <p class="表格單元格">TEXT</p> </td> </tr></tbody></table>
為了簡化程序,數據類型都定義為 TEXT 類型,可保存任意長度的字符串數據。
開頭 4 個字段的數據原封不動來自 CSV 文件。最后的“用于檢索的住址”是連接都道府縣名、市區町村名、町域名后的字符串。用住址進行檢索時,可用“東京都港區”2。
2這樣的都道府縣名 + 市區町村名的字符串進行檢索東京都為都道府縣名,港區為市區町村名。——譯者注
我們使用 SQL 的 `CREATE TABLE` 語句創建表。表 23.1 的 SQL 如下所示。
~~~
CREATE TABLE IF NOT EXISTS zips (code TEXT, pref TEXT, city TEXT, addr TEXT, alladdr TEXT);
~~~
`IF NOT EXISTS` 表示沒有同名表時才創建表。執行上面的 SQL 后就創建了有 5 個 TEXT 類型字段的 zips 表。
代碼清單 23.2 中的 `JZipCode` 類實現了把郵政編碼數據插入到 zips 表的功能(`JZipcode#make_db` 方法)。
**代碼清單 23.2 jzipcode.rb(插入處理)**
~~~
require 'sqlite3'
class JZipCode
COL_ZIP = 2
COL_PREF = 6
COL_CITY = 7
COL_ADDR = 8
def initialize(dbfile)
@dbfile = dbfile
end
def make_db(zipfile)
return if File.exists?(@dbfile)
SQLite3::Database.open(@dbfile) do |db|
db.execute <<-SQL
CREATE TABLE IF NOT EXISTS zips
(code TEXT, pref TEXT, city TEXT, addr TEXT, alladdr TEXT)
SQL
File.open(zipfile, 'r:shift_jis') do |zip|
db.execute "BEGIN TRANSACTION"
zip.each_line do |line|
columns = line.split(/,/).map{|col| col.delete('"')}
code = columns[COL_ZIP]
pref = columns[COL_PREF]
city = columns[COL_CITY]
addr = columns[COL_ADDR]
all_addr = pref+city+addr
db.execute "INSERT INTO zips VALUES (?,?,?,?,?)",
[code, pref, city, addr, all_addr]
end
db.execute "COMMIT TRANSACTION"
end
end
end
end
~~~
`COL_ZIP`、`COL_PREF` 等是表示數據是在 CSV 文件的第幾個字段的常量。
`JZipCode.new` 方法的參數為數據庫文件名。`initialize` 方法只是單純將文件名保存到實例變量。`make_db` 方法、`find_by_code` 方法等在打開數據庫時將會用到這個變量。
如果數據庫文件已經存在,程序的初始化已經完成,make_db 方法將不會進行任何操作。
文件不存在時,`SQLite3::Database#open` 方法將會打開新增的數據庫文件,執行 SQL 語句 `CREATE TABLE`。然后打開編碼為 Shift_JIS 的 CSV 文件,使用 `split` 方法以 , 分割。對分割后的各個元素的字符串使用 `delete` 方法刪除”,然后再通過 `map` 方法的塊,將結果保存到變量 `columns` 中。最后執行 `INSERT` 語句,把變量中的都道府縣名、市區町村名、町域名等數據插入到數據庫。
執行 `INSERT` 語句的前后分別有 `BEGIN TRANSACTION` 語句,以及 `COMMIT TRANSACTION` 語句,這是為加快 `INSERT` 語句執行速度常用的方法,在 SQLite3 中也經常使用。
### **23.5 檢索數據**
接下來,我們來檢索已經保存好的郵政編碼。代碼清單 23.2 的 JZipCode 類定義了 `find_by_code` 方法以及 `find_by_address` 方法。
~~~
class JZipCode
┊
def find_by_code(code)
sql = "SELECT * FROM zips WHERE code = ?"
str = ""
SQLite3::Database.open(@dbfile) do |db|
db.execute(sql, code) do |row|
str << sprintf("%s %s", row[0], row[4]) << "\n"
end
end
str
end
def find_by_address(addr)
sql = "SELECT * FROM zips WHERE alladdr LIKE ?"
str = ""
SQLite3::Database.open(@dbfile) do |db|
db.execute(sql, "%#{addr}%") do |row|
str << sprintf("%s %s", row[0], row[4]) << "\n"
end
end
str
end
end
~~~
`find_by_code` 方法以郵政編碼為參數,返回該郵政編碼對應的住址。`find_by_address` 方法恰好相反,以住址為參數,返回包含該住址的郵政編碼。
執行檢索時,與 `INSERT` 語句一樣,使用 `Database#execute` 方法,執行 `SELECT` 語句。通過參數傳遞的值,會置換掉 `WHERE code = ?`、`WHERE alladdr LIKE` 中的條件部分 ? 的值。檢索結果以數組的形式賦值給變量 row,然后再將連接后的字符串保存在變量 `str`。
下面,我們通過 irb 命令,測試一下 `find_by_code` 方法以及 `find_by_address` 方法。
> **執行示例**
~~~
> irb --simple-prompt
>> require "./jzipcode"
=> true
>> jzipcode = JZipCode.new("zip.db")
=> #<JZipCode:0x2c2ab08 @dbfile="zip.db">
>> jzipcode.make_db("KEN_ALL.CSV")
=> #<SQLite3::Database:0x2c2eb40>
>> puts jzipcode.find_by_code('1060031')
1060031 東京都港區西麻布
=> nil
>> puts jzipcode.find_by_address("東京都渋谷區神")
1500047 東京都渋谷區神山町
1500001 東京都渋谷區神宮前
1500045 東京都渋谷區神泉町
1500041 東京都渋谷區神南
=> nil
~~~
首先,通過 `require "./jzipcode"` 引用 `jzipcode.rb` 的內容。接著,用 `JZipCode.new` 方法創建實例,通過 `make_db` 方法讀取數據。之后,對 `find_by_code` 方法指定郵政編碼,則會輸出對應該郵政編碼的住址。同樣地,對 `find_by_address` 方法指定住址的某部分,則會輸出包含該住址的郵政編碼。
### **23.6 總結**
本章我們介紹了如何使用 SQLite3 庫提高檢索大量數據時的速度。處理大量數據時,根據實際需要,使用數據庫以及 RubyGems 等現成的類庫,會大大提高程序編寫效率。
數據庫產品中,除了 SQLite3 外,MySQL、PostgreSQL 等也都被廣泛使用。雖然不同的數據庫產品的 SQL 語法等在細節上存在差異,但基本結構是大致相同的。有興趣的讀者可以繼續學習其他數據庫產品。
- 推薦序
- 譯者序
- 前言
- 本書的讀者對象
- 第 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 參考集
- 后記
- 謝辭