使用Go读取带BOM的文件

14

我需要读取可能包含字节顺序标记的Unicode文件。当然,我可以自己检查文件的前几个字节,并在发现BOM时丢弃它。但在这样做之前,是否有任何标准方法来处理这个问题,无论是核心库还是第三方库?

5个回答

11

如果我没记错的话,没有标准的方法可以实现这个检查(在标准库中实现这样的检查确实是错误的层次),所以这里有两个例子可以让你自己处理。

一个方法是在数据流之上使用缓冲读取器:

import (
    "bufio"
    "os"
    "log"
)

func main() {
    fd, err := os.Open("filename")
    if err != nil {
        log.Fatal(err)
    }
    defer closeOrDie(fd)
    br := bufio.NewReader(fd)
    r, _, err := br.ReadRune()
    if err != nil {
        log.Fatal(err)
    }
    if r != '\uFEFF' {
        br.UnreadRune() // Not a BOM -- put the rune back
    }
    // Now work with br as you would do with fd
    // ...
}

另一种方法是使用实现了io.Seeker接口的对象,读取前三个字节,如果不是BOM,则像下面示例中这样io.Seek()返回到开头:

import (
    "os"
    "log"
)

func main() {
    fd, err := os.Open("filename")
    if err != nil {
        log.Fatal(err)
    }
    defer closeOrDie(fd)
    bom := [3]byte
    _, err = io.ReadFull(fd, bom[:])
    if err != nil {
        log.Fatal(err)
    }
    if bom[0] != 0xef || bom[1] != 0xbb || bom[2] != 0xbf {
        _, err = fd.Seek(0, 0) // Not a BOM -- seek back to the beginning
        if err != nil {
            log.Fatal(err)
        }
    }
    // The next read operation on fd will read real data
    // ...
}

这是可能的,因为*os.File实例(os.Open()返回的类型)支持寻址并且实现了io.Seeker接口。需要注意的是,对于HTTP响应中的Body读取器之类的类型来说,情况并非如此,因为无法“倒带”它们。 bufio.Buffer通过执行一些缓冲(显然)绕过了这种不能寻址流的特性,这就允许你在其上使用UnreadRune()

请注意,这两个示例都假定我们处理的文件使用UTF-8编码。如果需要处理其他(或未知)编码方式,则情况会更加复杂。


bufio的方法可行,我喜欢它将BOM视为单个rune而不是一组字节。 - Marcus Downing
@kostix 云,您能解释一下如何推导出第二种方法中的条件吗?谢谢。 - Anuruddha
@kostix 你使用的条件是 bom[0] != 0xef || bom[1] != 0xbb || bom[1] != 0xbf。 - Anuruddha
@kostix 谢谢。我之前因为你纠正的错误而感到困惑,它使用了 OR 运算符。难道不应该是 AND 吗? - Anuruddha
1
@Anuruddha,不,UTF-8编码的BOM是具有特定顺序和特定值的三个字节,因此逻辑是“如果序列中的任何字节具有其不应在该位置具有的值,则不是BOM”。 - kostix
显示剩余3条评论

5
您可以使用 utfbom 包。它包装了 io.Reader,可以检测并丢弃必要的 BOM。它还可以返回由 BOM 检测到的编码。

4

我想在这里补充一下从字符串中删除字节顺序标记序列的方法-而不是直接处理字节(如上所示)。

package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "\uFEFF is a string that starts with a Byte Order Mark"
    fmt.Printf("before: '%v' (len=%v)\n", s, len(s))

    ByteOrderMarkAsString := string('\uFEFF')

    if strings.HasPrefix(s, ByteOrderMarkAsString) {

        fmt.Printf("Found leading Byte Order Mark sequence!\n")
        
        s = strings.TrimPrefix(s, ByteOrderMarkAsString)
    }
    fmt.Printf("after: '%v' (len=%v)\n", s, len(s)) 
}

其他“字符串”函数也应该能够工作。 这是打印输出的内容:
before: ' is a string that starts with a Byte Order Mark (len=50)'
Found leading Byte Order Mark sequence!
after: ' is a string that starts with a Byte Order Mark (len=47)'

干杯!


3

2
如果我自己生成所有的文件和流,我当然会严格遵循Unicode标准。但像世界上许多人一样,我被迫消费由他人产生的数据。 - Marcus Downing

1

我们使用transform包来读取CSV文件(这些文件可能已经保存为UTF8、UTF8-with-BOM或UTF16格式),具体操作如下:

import (
    "encoding/csv"
    "golang.org/x/text/encoding"
    "golang.org/x/text/encoding/unicode"
    "golang.org/x/text/transform"
    "io"
}

// BOMAwareCSVReader will detect a UTF BOM (Byte Order Mark) at the
// start of the data and transform to UTF8 accordingly.
// If there is no BOM, it will read the data without any transformation.
func BOMAwareCSVReader(reader io.Reader) *csv.Reader {
    var transformer = unicode.BOMOverride(encoding.Nop.NewDecoder())
    return csv.NewReader(transform.NewReader(reader, transformer))
}

我们正在使用 Go 1.18。


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