はじめに
TIG真野です。Go 1.19リリース連載の2本目です。
encoding/csv のアップデート内容について紹介します。 Go 1.17のときも encoding/csv を取り上げたので2回目です。
ちなみにその時に説明したFieldPos()
ですが、類似のメソッドがGo 1.19の encoding/xml
にも追加されています。隣のパッケージに類似の展開がされているのは面白いですね。
encoding/xml
The new methodDecoder.InputPos
reports the reader’s current input position as a line and column, analogous to encoding/csv’sDecoder.FieldPos
.
readerの現在入力位置の行と列を示すを取得します。encoding/csv
のDecoder.FieldPos
と同様です。
アップデート内容について
さて、encoding/csv
のアップデートについてです。
Go 1.19 Release Notes に1行だけ書いてあります。
The new method Reader.InputOffset reports the reader’s current input position as a byte offset, analogous to encoding/json’s Decoder.InputOffset.
新しいReader.InputOffset
メソッドは、現在の入力位置をバイトオフセットとして取得します。encoding/jsonのDecoder.InputOffset
と類似しています。
追加された関数はGoDocsに次のように書かれています。
func (r *Reader) InputOffset() int64 |
これであーこれで便利になるねってピンと来る人は、そこそこディープな encoding/csv
のファンではないでしょうか? 説明していきます。
まずどのようなAPIか動かしましょう
以下のバージョンで動かしています。
>go version |
改行を含むCSVデータで動かしてみます。
package main |
結果は次のようになります。
>go run main.go |
最初は0で r.Read()
が呼び出されるとバイト位置が進み、呼び出されるたびにddd、zzz項目の直前まで先に進んでいることがわかります。バイト位置的にどこまで進んだかがわかります。
Issue上でのやり取り
encoding/csv: add Reader.InputOffset method #43401 が該当のIssueですが、encoding/json.Decoder
という前例があるからか割りとすんなり提案から受け入れられています。
議論があったポイントは以下程度。
encoding/json
のDecoder.InputOffset
はJSONバイナリをどこまで読み取ったか知るすべが無いので有効である- CSVの場合は行ベースなので、不要では?
- いやいや、ランダムアクセスが必要なケースもあるし必要。例えば CSVに対して転置インデックスを作成し、オフセットから特定の行を読み取りたいケースがある
- (筆者補足)あと、CSVは項目の改行が許容されているので必ずしも読み取った行でポジションが分かるわけでもない
- CSVの場合は行ベースなので、不要では?
ちなみに、前例とされている encoding/json.Decoder
は2020年2月25日の Go 1.14に追加されたメソッドです。
追加された関数の内容は分かるけど、どのような時に嬉しいのか
Issueのくだりで触れられていますが、主要なユースケースは2つ思いつきます。
- CSVをデコード時に不正な入力が合った場合、どのバイト位置で失敗したか知らせたい
- 通常はこちらの用途が多いのではと思いますが…さていかに。
- (巨大な)CSVファイルに対してランダムアクセスをさせるため、正確な位置を知りたい場合
- どのようなケースで必要になるかイメージがわかないですが(私が扱うような業務システム領域だとRDBとかElasticsearchに突っ込んじゃうので..)、 Read random lines off a text file in go - Stack Overflow にある通り、需要はあるようです
それぞれどんな感じになるか簡易実装で紹介します。
不正なCSV入力例
試しに不正なクウォートを混入させてみます。 ee"
e が該当の部分です。
s := `aaa,"b |
動かしてみると、次のメッセージが取得できます。
>go run main.go |
やってみて気がついたんですが、CSVパースエラーの場合は行番号も列位置も出ているため、 InputOffset()
をわざわざ出さなくても良いかなとは思いました。
信頼できない連携先とのやり取りで、不正値を混入することを考慮して、 InputOffset()
を表示しなくてもまぁ大丈夫かなということが分かりました。
巨大なCSVに対するランダムアクセス例
ということで、巨大なCSVのランダムアクセスです。データは国勢調査 - 年齢(5歳階級),男女別人口-都道府県(大正9年~平成27年) から取得したCSVを使っています(c03.csv
という名称もそのままです)。1.7万行ほどで1MBほどなのでメモリに載せれるほどですが、いったんこれが1000TBくらいあると仮定します。
最初に対象となる巨大なCSVファイルを読み取り、後で検索したいレコードが登場したらそのオフセット位置を覚えておきます(この情報を後でファイルに出力しても良いですね)。
その後、fetchFirstLine()
で覚えておいたオフセット位置をもとに巨大なファイルを f.Seek()
を用いてその位置から1行だけCSVを読み込みます。
package main |
実行するとこういう感じです。
>go run main.go |
こういった用途に関しては、 f.Seek()
を使ってファイルの特定位置から読み込むためには、従来の行番号などでは不適切です。なぜなら100万行目から読み込む必要があると分かっても、ファイルの先頭から改行コードをカウントする必要があり、アクセスしたい行番号が大きければ大きいほど時間がかかってしまいます。
このようにして、 InputOffset()
で取得したバイト位置をもとに、巨大なファイルに対してランダムアクセスのようなことができます。
まとめ
新しく追加された InputOffset()
について調べました。リリースノートの内容だけ見ると不正な入力値に対するトレース用途に用いるのかなと思っていましたが、どちらかといえばもう少しテクニカルな用途での利用を想定していそうです。