ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# **第 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 语法等在细节上存在差异,但基本结构是大致相同的。有兴趣的读者可以继续学习其他数据库产品。