👑

KEN_ALL.csv を Vim でシュッとする

2020/12/25に公開

この記事は Vim Advent Calendar 2020 25日目の記事です。

はじめに

年末ですね。年賀状ですね。インターネットが普及した現代、若い世代の人達においては年賀状を交わす事は少なくなってしまったかもしれません。しかし田舎育ちの僕やある程度年配の方々の世界線では年賀状は未だ変わらず有効なコミュニケーション手段として採用されており「アイツん家の子、大きくなったな」「アイツ随分老けたな」といった思い出回帰の方法としても使われています。

さて皆さんは年賀状をどの様に作っておられますか?手書きですか?それとも「筆○2020」等といった年賀状作成専用ソフトをお使いだったりしますか?僕は毎年 LibreOffice の宛名書き印刷を使っています。

LibreOfficeで宛名印刷(縦書き/連名): ひろろろぐ

LibreOffice で差し込み印刷する記事は沢山あるのですが、連名がちゃんと出せる物だとここの記事に従うのが良いでしょう。

年賀状を作る流れはおおよそ以下の通り。

  1. LibreOffice Calc で住所録を作る
  2. LibreOffice Base に取り込む
  3. LibreOffice Writer で作られた宛名面を使って差し込み印刷する

この手順をうまくやれば「筆○2020」といったソフトウェアを買う必要などありません。無料で出来るし、専用ソフトの流儀に悩まされる必要もないし、なんなら自分で拡張する事もできるのです。

事件が起きた

ところで「毎年」と書きましたが実は今年、毎年使っていた住所録 CSV ファイルを紛失してしまいまして、今年は昨年貰った年賀状から宛名書き用 CSV を起こし直すという、温かみのある手打ち入力作業が発生してしまいました。

僕は LibreOffice はほぼ年賀状の為だけに使ってるので、毎年年末になるとアップデートをしています。

最近の LibreOffice はずいぶんと軽くなってとても良いですね。

(まだ Vim 出て来ないの)

住所録を作るダルさ

紙媒体の年賀状から住所録 CSV を作るには

  1. 郵便番号を読み取り入力する
  2. 住所を読み取り入力する
  3. 地番を読み取り入力する
  4. 名前を読み取り入力する

といった全て人間による作業が必要となる訳です。そんな中、今年の年賀状作成で僕がやった Vim ハックをご紹介しようと思います。

手作業を省けるのはどこか

上記の一覧のうち、手作業が省けるのはどこでしょうか?始めはスキャナで取り込む事も考えましたが印刷面に住所が書いてある人もいるので難易度が高かったです。実は 2 の「住所の読み取り」が省略できるのです。読み方が分からない住所も郵便番号さえ分かれば部分的には再現できるはずです。

例えば

〒102-0072
東京都千代田区飯田橋 12-34-56 ほげほげ荘123号

この住所のうち、実際に必要なのは

〒102-0072
12-34-56 ほげほげ荘123号

これだけでいいはずです。全て人間がタイプするよりも、およそ半分くらいのタイプ数になるはずです。おまけに日本の住所には、ごくごくまれに読み方が分からない漢字が使われている物もある訳で、そんな住所を毎行毎行調べていたら幾ら時間があっても足りません。

手数を省いて楽をする

そこで僕は以下の様に、郵便番号と住所以降の地番のみを入力する事にしました。

1020075,ほげほげ荘123号,ほげ田 ほげ夫,ほげ美
1056412,1-77-8989,ほげ村 ほげ和,

幾分、手数が減ったので徹夜する事もなく入力できました。なお完成形として欲しいのは以下の形です。

1020075,東京都千代田区三番町,ほげほげ荘123号,ほげ田 ほげ夫
1056412,東京都港区虎ノ門虎ノ門ヒルズビジネスタワー(12階),1-77-8989,ほげ村 ほげ和

住所と地番が別のカラムになっているのは、年賀状の住所欄が長いと印刷で溢れてしまうので、2段で印刷するという理由です。

世紀末覇者 KEN_ALL

ここで登場するのが郵便局が提供する「郵便番号データ 全国一括版」、別名 KEN_ALL です。プログラマの方であれば名前くらいは知っていると思います。始めは無料の郵便番号検索 API を使おうと考えたのですが、リクエスト数が多い上に信頼してもないサーバ側に住所を特定されかねない情報である為、今回は KEN_ALL を使う事にしました。

CSV データは以下の URL からダウンロード可能です。

https://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html

またこの CSV のフォーマット説明は以下の URL から参照可能です。

https://www.post.japanpost.jp/zipcode/dl/readme.html

全国地方公共団体コード
(旧)郵便番号
郵便番号
都道府県名 ………… 半角カタカナ(コード順に掲載)
市区町村名 ………… 半角カタカナ(コード順に掲載)
町域名 ……………… 半角カタカナ(五十音順に掲載)
都道府県名 ………… 漢字(コード順に掲載)
市区町村名 ………… 漢字(コード順に掲載)
町域名 ……………… 漢字(五十音順に掲載)
一町域が二以上の郵便番号で表される場合の表示
小字毎に番地が起番されている町域の表示
丁目を有する町域の場合の表示
一つの郵便番号で二以上の町域を表す場合の表示
更新の表示
変更理由
表示の実施

住所を組み立てるのであれば以下を結合すれば良さそうです。

都道府県名 ………… 漢字(コード順に掲載)
市区町村名 ………… 漢字(コード順に掲載)
町域名 ……………… 漢字(五十音順に掲載)

ところで Vim でやる意味はあるのか

こういったテキストファイルの操作は本来、UNIX シェルやコマンドを使ってやるのが一般的です。もちろん Vim script も強力です。今回の様に、CSV カラムの2列目にマッチさせて 7~9 カラム目を結合するといった処理を書く際に、どの手段を使うべきかは、そのやる内容によって変えるべきでしょう。人によっては awk を使った方が早いという人もいるでしょうし perl でやる人もいるでしょう。

もちろん CSV ファイルを LibreOffice Calc に取り込んで DLOOKUP する人もいるでしょう。

しかし7桁固定の郵便番号で無かった場合や、複数の条件検索が必要な場合、さらに細かいテキスト編集が必要になる様な場面において、いずれのケースでも対応できるという点で「テキストエディタに搭載されているスクリプト言語 Vim script」はそんなに悪くない選択肢だと考えました。

またシェルで解決する方法については、コマンドラインにシングルクオートを使って文字列を渡せない某 OS も世の中にはある訳で、万人が使える方法を考えるとプラットフォーム非依存のスクリプト言語を使うのは安全、安心、安価と言って良いでしょう。

組み立てを考える

今回は割とすんなりこなした作業でしたが、これは僕が Vim script に慣れているからです。どういう思考でコマンドを組み立てたのかを順を追って説明したいと思います。慣れるとこれが数回の try&error で出来る様になるでしょう。

まずはファイルを読み込みましょう。Vim script でファイルを読み込むのは readfile() を使います。readfile() は与えられたファイル名のテキストファイルを改行で区切り、文字列配列として返します。(これがデフォルト動作ですが第二引数の指定でバイナリを読む事もできます)

echo readfile('KEN_ALL.csv')

ここで気を付けたいのは KEN_ALL.csv は Shift_JIS でエンコードされているという事です。readfile() が文字列配列を返すのでそれを Vim script のメソッド記法を使って変換しましょう。

echo readfile('KEN_ALL.csv')->map({_,a->iconv(a,'cp932',&encoding})

map() は配列や辞書に対して使う事ができます。クロージャの第一引数は配列インデックスもしくは辞書のキーになります。ですので例えば

echo [1,2,3]->map({_,a->a+1})

[2, 3, 4] になる訳です。上記の iconv は Shift_JIS (cp932) から Vim の現在の内部エンコーディングへ変換を行っているという意味になります。

さて、読み込んだ各行はダブルクオートで囲われた文字列をカンマで分割している CSV です。まさかダブルクオート文字やカンマを含む住所などありません。今回には必要の無いダブルクオートを雑に削除し、カンマをセパレータとして文字列分割します。

echo readfile('KEN_ALL.csv')->map({_,a->iconv(a,'cp932',&encoding)})->map({_,a->substitute(a,'"','','g')->split(',')})

readfile で返された配列を mapiconv で内部エンコーディングに変換された配列になっています。各行のダブルクオートを削除し、,split しています。これで CSV ファイルが文字列を要素に持つ2次元配列になりました。郵便番号が入っているのは3カラム目ですので指定の郵便番号のレコードだけ残す様にフィルタします。

echo readfile('KEN_ALL.csv')->map({_,a->iconv(a,'cp932',&encoding)})->map({_,a->substitute(a,'"','','g')->split(',')})->filter({_,a-> a[2]=='1020075'})[0]

出力結果は以下の通り。

['13101', '102  ', '1020075', 'トウキョウト', 'チヨダク', 'サンバンチョウ', '東京都', '千代田区', '三番町', '0', '0', '0', '0', '0', '0']

住所を作るのに必要なのは 7~9 カラム目ですから抜き出して結合しましょう。

echo readfile('KEN_ALL.csv')->map({_,a->iconv(a,'cp932',&encoding)})->map({_,a->substitute(a,'"','','g')->split(',')})->filter({_,a-> a[2]=='1020075'})[0][6:8]->join('')
東京都千代田区三番町

Vim の式置換を使う

Vim の置換には強力は武器が備わっています。

\%V

Vim には矩形選択内で文字列置換する方法があります。通常、ビジュアル選択した内容を置換するには

:'<,'>s/foo/bar/g

この様に実行しますが、これだと矩形選択の場合に期待通り動作しません。矩形で使うには \%V を使います。\%V は選択範囲にマッチします。1個使うと開始位置に、2個使うと選択範囲にマッチします。

この状態から以下を実行すると、選択範囲内だけが置換されます。

'<,'>s/\%Vほ//g

このケースの様に範囲内の1要素「ほ」だけを扱う場合は必要ないですが、例えば .* の様なアトムを使う場合には終了位置を示す為に \%V.*\%V の様に2つ使う必要があります。

submatch

今回の要件では矩形選択内の各行は全て異なる郵便番号になっているはずです。つまりマッチパターンは .* になります。このパターンを置換後文字列で利用したいのです。そこで便利なのが submatch です。

:help sub-replace-\=

例えば以下の HTML を見て下さい。

<h1>example</h1>
<h2>example</h2>
<h3>example</h3>

この h1, h2, h3 をそれぞれ h2, h3, h4 に置換するにはどうするのがスマートでしょうか?僕であれば以下の様に実行します。

%s/\<h\(\d\+\)>/\='h' .. (submatch(1)+1) .. '>'/g

もしくは \zs\ze を使い以下の様に実行します。

%s/\<h\zs\d\+\ze>/\=submatch(0)+1/g

submatch(0) はマッチした全体が、submatch(1) はグループが渡されます。

仕上げ

やり方が分かったので、矩形選択と submatch を使います。CSV ファイルの先頭から始まる郵便番号7桁を矩形選択し、以下を実行します。

'<,'>s!\%V.*\%V!\=submatch(0) .. ',' .. readfile('KEN_ALL.csv')->map({_,a->iconv(a,'cp932',&encoding)})->map({_,a->substitute(a,'"','','g')->split(',')})->filter({_,a-> a[2]==submatch(0)})[0][6:8]->join('')!

欲しい CSV の形式が得られました。

まとめ

この様に Vim はプログラミングができるテキストエディタでありながら、時には Excel の様な CSV 編集もできてしまいます。使い方次第では可能性がどんどん広がるとても興味深いテキストエディタです。面白いテクニックを見付けたら、ぜひ共有しましょう。

今年の Advent Calendar も無事完走する事ができました。みなさんお疲れさまでした。

Discussion