元ネタ:連結されたgzipを1行ずつ見る
そういえばgolangにもgzipあったよなぁと思って書き始めたらかなりハマってしまった話です。
単純に1行ずつ見るやつ
複数ファイルを区別せず1行1行見ていくコードは結構簡単に書けました。
compress/gzip
のReader
にはMultistreamというオプション関数があって、これにtrueを渡すと複数のgzipされたデータストリームを1つのデータストリームとして扱ってくれるそうです(デフォルトでtrueになっている)
今回のお題にうってつけですね( ´・‿・`)
package main
import (
"bufio"
"compress/gzip"
"fmt"
"os"
)
func gzipEachLine(filename string, proc func(string)) {
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()
r, err := gzip.NewReader(f)
if err != nil {
return
}
defer r.Close()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
proc(scanner.Text())
}
}
func main() {
gzipEachLine("foo.gz", func(line string) {
fmt.Println(line)
})
}
ただしbufio.Scanner
は1行あたりの文字数制限があるので1行が結構長くなりそうな場合は工夫が必要っぽいです。
参考:Go 言語で標準入力から読み込む競技プログラミングのアレ --- 改訂第二版
ファイルの名前とかも見るやつ
trueにすればひとつにまとまるってことはfalseにすれば1個1個見れるんじゃないかと思ってソースを覗いたらそれっぽいコードがありました。
https://golang.org/src/compress/gzip/gunzip_test.go#L372
これを参考に次のように書いてみました
package main
import (
"compress/gzip"
"fmt"
"os"
)
func main() {
f, err := os.Open("foo.gz")
if err != nil {
return
}
defer f.Close()
var r gzip.Reader
var needClose bool
for {
if err := r.Reset(f); err != nil {
break
}
needClose = true
fmt.Printf("filename : %s\n", r.Header.Name)
r.Multistream(false)
if data, err := ioutil.ReadAll(&r); err == nil {
fmt.Println(string(data))
}
}
if needClose {
r.Close()
}
}
がこれは動きませんでした。
最初のファイルの名前と中身は出力されましたが2番目以降のファイルが読み取れませんでした。
原因がわからないので元ソースをあたってみると元ソースではos.File
ではなくbytes.Reader
が使われていました。
(そもそもgzipされたデータがソースに埋め込まれている)
これかな?と思って一旦[]byte
に読み込んでbytes.Reader
に変換すると2番目以降も読み取れる!
これはどういうこっちゃと思ってソース読んだり、printfデバッグしたり、gdbしたりしてやっとこさわかりました。
原因はbufio.Reader
gzip.NewReader
もしくはgzip.Reader
のReset
を呼んだとき内部ではmakeReader
という関数を呼んでいます。
func makeReader(r io.Reader) flate.Reader { if rr, ok := r.(flate.Reader); ok { return rr } return bufio.NewReader(r) }
この関数は受け取った引数r
がflate.Reader
を実装していればそのまま返して、そうでないならbufio.Reader
に包んで返すというものです。
*bytes.Reader
はflate.Reader
を実装していましたが、*os.File
は実装していなかったのでbufio.Reader
に包まれていました。
bufio.Reader
はその名の通りバッファを持ったIOでRead
の際には元となるio.Reader
からバッファに読み込めるだけ読み込んでいます。https://golang.org/src/bufio/bufio.go?#L96
今回検証に使ったgzipファイルは91byteと小さかったため最初のファイルのヘッダ情報を読み取った時点でファイルの中身がすべてバッファに移ってしまっていました。
ちゃんと書いてあった
で原因がわかったあとにドキュメントを見てたらちゃんと注意書きがありました。。。(´・ω・`)
ドキュメントはちゃんと読もう!!!
If r does not also implement io.ByteReader, the decompressor may read more data than necessary from r.
https://golang.org/pkg/compress/gzip/
解決策
一旦[]byte
に読み込んでbytes.Reader
にすればできるんですが、これじゃ展開してるのと同じなのでio.Reader
をアドホックにflate.Reader
に変換するコードを書いてみました。
package main
import (
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
)
type adhocFlateReader struct {
io.Reader
}
func (r adhocFlateReader) ReadByte() (byte, error) {
var b [1]byte
if n, _ := r.Read(b[:]); n > 0 {
return b[0], nil
}
return 0, io.EOF
}
func main() {
f, err := os.Open("foo.gz")
if err != nil {
return
}
defer f.Close()
fr := adhocFlateReader{f}
var r gzip.Reader
var needClose bool
for {
if err := r.Reset(fr); err != nil {
break
}
needClose = true
fmt.Printf("filename : %s\n", r.Header.Name)
r.Multistream(false)
if data, err := ioutil.ReadAll(&r); err == nil {
fmt.Println(string(data))
}
}
if needClose {
r.Close()
}
}
追記
最初の1回はgzip.NewReader
を呼び出す方が自然っぽいです。
package main
import (
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
)
type adhocFlateReader struct {
io.Reader
}
func (r adhocFlateReader) ReadByte() (byte, error) {
var b [1]byte
if n, _ := r.Read(b[:]); n > 0 {
return b[0], nil
}
return 0, io.EOF
}
func main() {
f, err := os.Open("foo.gz")
if err != nil {
return
}
defer f.Close()
fr := adhocFlateReader{f}
r, err := gzip.NewReader(fr)
if err != nil {
return
}
defer r.Close()
for {
fmt.Printf("filename : %s\n", r.Header.Name)
r.Multistream(false)
if data, err := ioutil.ReadAll(r); err == nil {
fmt.Println(string(data))
}
if err := r.Reset(fr); err != nil {
break
}
}
}
追記
これもっと単純にこれで良かった
package main
import (
"compress/gzip"
"fmt"
"io/ioutil"
"os"
"bufio"
)
func main() {
f, err := os.Open("foo.gz")
if err != nil {
return
}
defer f.Close()
br := bufio.NewReader(f)
r, err := gzip.NewReader(br)
if err != nil {
return
}
defer r.Close()
for {
fmt.Printf("filename : %s\n", r.Header.Name)
r.Multistream(false)
if data, err := ioutil.ReadAll(r); err == nil {
fmt.Println(string(data))
}
if err := r.Reset(br); err != nil {
break
}
}
}