在缓冲读取器中查找

7
我需要创建一个缓存读取器,基于现有的子io.Reader,但是该读取器必须支持在已读取和缓冲的数据中进行查找。
因此,当已经读取了n个字节时,我想能够将读取器重置为偏移量0并重新读取该块。
不幸的是,bufio.Reader不支持查找。
是否有标准的读取器支持这一点,或者我必须自己实现?
2个回答

9
bufio 的目的是提供缓冲 I/O。缓冲 I/O 是为了提高性能,而不是时间旅行。
您可以将数据读入字节切片中,然后使用 bytes.Reader 进一步处理。

3
很遗憾,我需要读取的文件是相对较大的多部分文件,需要分块读取。这就解释了为什么我不能使用 bytes.Reader(需要将所有内容加载到内存中)。看起来我需要使用自定义实现。 - user187676
@ErikAigner:自定义实现怎么样了?我也遇到了类似的情况,需要查看CSV数据的io.Reader字节流以检测分隔符,然后再将读取器传递给csv.NewReader() - DanielSmedegaardBuus
1
这真是令人失望。Rust的BufReader支持寻址,并且我不认为Go的不应该支持。通常我们希望通过缓冲来提高性能,同时也希望能够向前查找。这有什么问题吗? - wchargin
@wchargin Rust的BufReader<R>只有在底层的R也实现了Seek时才会实现Seek。Go的类型系统太弱,无法表达这种约束。Peeking不同于seeking;Go的bufio.Reader确实有Peek,它允许您向前查看缓冲区大小(我想)。要在Go中寻找,如果您拥有底层的io.Reader并且您以某种方式知道它也是一个io.Seeker,则可以始终丢弃bufio.Reader,寻找底层的io.ReadSeeker并创建一个新的bufio.Reader来包装它。 - Thomas
@Thomas:好观点,谢谢;我没有意识到Go的类型系统太弱以至于无法表达约束条件。虽然似乎寻求底层读取器可以让您从io.SeekStart/io.SeekEnd实现Seek,但不能实现io.SeekCurrent,特别是不允许您在不需要引用两个读取器的情况下实现ftell,例如underlying.Seek(0, io.SeekCurrent) - bufReader.Buffered() - wchargin
如果您寻找底层的读取器,缓冲读取器将不知道它,因此从缓冲读取器进行的后续读取将返回错误位置的数据,直到其缓冲区为空。 - Thomas

2

我曾想过在已打开的os.File中查找,然后使用bufio.Reset(),这似乎是一个答案,但并不理想。首先,bufio.Reset的文档说它“丢弃任何缓冲数据”,但操作系统难道不会缓存最近读取的文件内容吗?

其次,从指定的文件位置开始读取和缓冲是正确的,但它没有考虑扇区对齐:它一次缓冲整个缓冲区,而不管起始点在哪里。因此,假设缓冲区大小(默认为4096)等于文件系统的簇大小,并且除非起始点对齐到簇,否则bufio将每次需要读取时从2个簇中读取。我并不是说性能受到明显影响。实际上,通过尽可能地预读,性能可能比我的对齐迷恋所产生的更好。

我认为这段代码演示了这一点,从可执行文件本身读取几个2000字节的块:

package main

import (
    "bufio"
    "crypto/md5"
    "fmt"
    "os"
)

func readBytes(r *bufio.Reader, block []byte) {
    for i := 1; i < len(block); i++ {
        var err error
        block[i], err = r.ReadByte()
        if err != nil {
            panic(err)
        }
    }
}

func status(f *os.File, r *bufio.Reader, block []byte, what string) {
    fpos, err := f.Seek(0, os.SEEK_CUR)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%s: fpos=%5d, buffered=%4d, md5=%X\n", what, fpos, r.Buffered(), md5.Sum(block))
}

func main() {
    f, err := os.Open(os.Args[0])
    if err != nil {
        panic(err)
    }
    defer func() { f.Close() }()
    r := bufio.NewReader(f)
    var block = make([]byte, 2000)
    status(f, r, block, "initial")
    readBytes(r, block)
    status(f, r, block, "block 1")
    readBytes(r, block)
    status(f, r, block, "block 2")
    readBytes(r, block)
    status(f, r, block, "block 3")
    f.Seek(2000, os.SEEK_SET) // return to start of buf1a
    r.Reset(f)
    readBytes(r, block)
    status(f, r, block, "block 2")
    readBytes(r, block)
    status(f, r, block, "block 3")
    readBytes(r, block)
    status(f, r, block, "block 4")
}

典型输出(显示在查找后,文件位置不返回到4096的倍数):

initial: fpos=    0, buffered=   0, md5=CF40A1DE3F93B4A025409B5EFA5AA210
block 1: fpos= 4096, buffered=2096, md5=C7015DD984AB85CCCBD206BA8243647D
block 2: fpos= 4096, buffered=  96, md5=E0D75F4A6DE681316515F5CD53F0D95C
block 3: fpos= 8192, buffered=2192, md5=7961B1A889E9793344374B3022314CD0
block 2: fpos= 6096, buffered=2096, md5=E0D75F4A6DE681316515F5CD53F0D95C
block 3: fpos= 6096, buffered=  96, md5=7961B1A889E9793344374B3022314CD0
block 4: fpos=10192, buffered=2192, md5=2A2F77C23EF4651E630855D9C3AA29DE

这是一个部分回答,但 Seek 的一个目的是确定文件的当前位置(在 Go 中,ftell(3) 被实现为 Seek(0, io.SeekCurrent))。一个适当的缓冲读取器可以实现这个,但如果你只能访问底层文件,就无法完成这个操作——正如你从示例中看到的那样,fpos 只看到缓冲区的值。 - wchargin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接