21

我可以将文件读入字节数组

但是当我将它转换为字符串时

它将 utf16 字节视为 ascii

如何正确转换?

package main

import ("fmt"
"os"
"bufio"
)

func main(){
    // read whole the file
    f, err := os.Open("test.txt")
    if err != nil {
        fmt.Printf("error opening file: %v\n",err)
        os.Exit(1)
    }
    r := bufio.NewReader(f)
    var s,b,e = r.ReadLine()
    if e==nil{
        fmt.Println(b)
        fmt.Println(s)
        fmt.Println(string(s))
    }
}

输出:

错误的

[255 254 91 0 83 0 99 0 114 0 105 0 112 0 116 0 32 0 73 0 110 0 102 0 111 0 93 0 13 0]

脚本信息]


更新:

在我测试了这两个例子之后,我现在明白了确切的问题是什么。

在 Windows 中,如果我在行尾添加换行符 (CR+LF),则会在行中读取 CR。因为 readline 函数无法正确处理 unicode([OD OA]=ok, [OD 00 OA 00]=not ok)。

如果 readline 函数可以识别 unicode,它应该理解 [OD 00 OA 00] 并返回 []uint16 而不是 []bytes。

所以我认为我不应该使用 bufio.NewReader,因为它无法读取 utf16,我看不到 bufio.NewReader.ReadLine 可以接受参数作为标志来指示读取文本是 utf8、utf16le/be 或 utf32。go 库中是否有任何用于 unicode 文本的 readline 函数?

4

5 回答 5

15

最新版本golang.org/x/text/encoding/unicode更容易做到这一点,因为它包含unicode.BOMOverride,它将智能地解释 BOM。

这是 ReadFileUTF16(),它类似于 os.ReadFile(),但解码 UTF-16。

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "log"
    "strings"

    "golang.org/x/text/encoding/unicode"
    "golang.org/x/text/transform"
)

// Similar to ioutil.ReadFile() but decodes UTF-16.  Useful when
// reading data from MS-Windows systems that generate UTF-16BE files,
// but will do the right thing if other BOMs are found.
func ReadFileUTF16(filename string) ([]byte, error) {

    // Read the file into a []byte:
    raw, err := ioutil.ReadFile(filename)
    if err != nil {
        return nil, err
    }

    // Make an tranformer that converts MS-Win default to UTF8:
    win16be := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM)
    // Make a transformer that is like win16be, but abides by BOM:
    utf16bom := unicode.BOMOverride(win16be.NewDecoder())

    // Make a Reader that uses utf16bom:
    unicodeReader := transform.NewReader(bytes.NewReader(raw), utf16bom)

    // decode and print:
    decoded, err := ioutil.ReadAll(unicodeReader)
    return decoded, err
}

func main() {
    data, err := ReadFileUTF16("inputfile.txt")
    if err != nil {
        log.Fatal(err)
    }
    final := strings.Replace(string(data), "\r\n", "\n", -1)
    fmt.Println(final)

}

这是 NewScannerUTF16 ,它类似于 os.Open() 但返回一个扫描仪。

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"

    "golang.org/x/text/encoding/unicode"
    "golang.org/x/text/transform"
)

type utfScanner interface {
    Read(p []byte) (n int, err error)
}

// Creates a scanner similar to os.Open() but decodes the file as UTF-16.
// Useful when reading data from MS-Windows systems that generate UTF-16BE
// files, but will do the right thing if other BOMs are found.
func NewScannerUTF16(filename string) (utfScanner, error) {

    // Read the file into a []byte:
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }

    // Make an tranformer that converts MS-Win default to UTF8:
    win16be := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM)
    // Make a transformer that is like win16be, but abides by BOM:
    utf16bom := unicode.BOMOverride(win16be.NewDecoder())

    // Make a Reader that uses utf16bom:
    unicodeReader := transform.NewReader(file, utf16bom)
    return unicodeReader, nil
}

func main() {

    s, err := NewScannerUTF16("inputfile.txt")
    if err != nil {
        log.Fatal(err)
    }

    scanner := bufio.NewScanner(s)
    for scanner.Scan() {
        fmt.Println(scanner.Text()) // Println will add back the final '\n'
    }
    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "reading inputfile:", err)
    }

}

仅供参考:我已将这些功能放入一个开源模块中,并进行了进一步的改进。见https://github.com/TomOnTime/utfutil/

于 2016-01-21T17:44:20.767 回答
14

UTF16、UTF8 和字节顺序标记由Unicode 联盟定义:UTF-16 FAQUTF-8 FAQ字节顺序标记 (BOM) FAQ


问题 4802:bufio:读行太麻烦

在 Go 中从文件中读取行太麻烦了。

人们经常被 bufio.Reader.ReadLine 所吸引,因为它的名字,但是它有一个奇怪的签名,返回 (line []byte, isPrefix bool, err error),并且需要大量的工作。

ReadSlice 和 ReadString 需要一个分隔字节,它几乎总是明显且难看的 '\n',也可以返回一行和一个 EOF


修订: f685026a2d38

bufio:新的扫描仪界面

添加一个新的、简单的界面,用于扫描(可能是文本)数据,基于一种名为 Scanner 的新类型。它有自己的内部缓冲,因此即使没有注入 bufio.Reader 也应该是合理有效的。输入的格式由“拆分函数”定义,默认情况下拆分为行。


go1.1beta1 发布

您可以从通常的位置下载二进制和源代码分发: https ://code.google.com/p/go/downloads/list?q=go1.1beta1


这是一个使用 Unicode 规则将 UTF16 文本文件行转换为 Go UTF8 编码字符串的程序。代码已被修改以利用bufio.ScannerGo 1.1 中的新接口。

package main

import (
    "bufio"
    "bytes"
    "encoding/binary"
    "fmt"
    "os"
    "runtime"
    "unicode/utf16"
    "unicode/utf8"
)

// UTF16BytesToString converts UTF-16 encoded bytes, in big or little endian byte order,
// to a UTF-8 encoded string.
func UTF16BytesToString(b []byte, o binary.ByteOrder) string {
    utf := make([]uint16, (len(b)+(2-1))/2)
    for i := 0; i+(2-1) < len(b); i += 2 {
        utf[i/2] = o.Uint16(b[i:])
    }
    if len(b)/2 < len(utf) {
        utf[len(utf)-1] = utf8.RuneError
    }
    return string(utf16.Decode(utf))
}

// UTF-16 endian byte order
const (
    unknownEndian = iota
    bigEndian
    littleEndian
)

// dropCREndian drops a terminal \r from the endian data.
func dropCREndian(data []byte, t1, t2 byte) []byte {
    if len(data) > 1 {
        if data[len(data)-2] == t1 && data[len(data)-1] == t2 {
            return data[0 : len(data)-2]
        }
    }
    return data
}

// dropCRBE drops a terminal \r from the big endian data.
func dropCRBE(data []byte) []byte {
    return dropCREndian(data, '\x00', '\r')
}

// dropCRLE drops a terminal \r from the little endian data.
func dropCRLE(data []byte) []byte {
    return dropCREndian(data, '\r', '\x00')
}

// dropCR drops a terminal \r from the data.
func dropCR(data []byte) ([]byte, int) {
    var endian = unknownEndian
    switch ld := len(data); {
    case ld != len(dropCRLE(data)):
        endian = littleEndian
    case ld != len(dropCRBE(data)):
        endian = bigEndian
    }
    return data, endian
}

// SplitFunc is a split function for a Scanner that returns each line of
// text, stripped of any trailing end-of-line marker. The returned line may
// be empty. The end-of-line marker is one optional carriage return followed
// by one mandatory newline. In regular expression notation, it is `\r?\n`.
// The last non-empty line of input will be returned even if it has no
// newline.
func ScanUTF16LinesFunc(byteOrder binary.ByteOrder) (bufio.SplitFunc, func() binary.ByteOrder) {

    // Function closure variables
    var endian = unknownEndian
    switch byteOrder {
    case binary.BigEndian:
        endian = bigEndian
    case binary.LittleEndian:
        endian = littleEndian
    }
    const bom = 0xFEFF
    var checkBOM bool = endian == unknownEndian

    // Scanner split function
    splitFunc := func(data []byte, atEOF bool) (advance int, token []byte, err error) {

        if atEOF && len(data) == 0 {
            return 0, nil, nil
        }

        if checkBOM {
            checkBOM = false
            if len(data) > 1 {
                switch uint16(bom) {
                case uint16(data[0])<<8 | uint16(data[1]):
                    endian = bigEndian
                    return 2, nil, nil
                case uint16(data[1])<<8 | uint16(data[0]):
                    endian = littleEndian
                    return 2, nil, nil
                }
            }
        }

        // Scan for newline-terminated lines.
        i := 0
        for {
            j := bytes.IndexByte(data[i:], '\n')
            if j < 0 {
                break
            }
            i += j
            switch e := i % 2; e {
            case 1: // UTF-16BE
                if endian != littleEndian {
                    if i > 1 {
                        if data[i-1] == '\x00' {
                            endian = bigEndian
                            // We have a full newline-terminated line.
                            return i + 1, dropCRBE(data[0 : i-1]), nil
                        }
                    }
                }
            case 0: // UTF-16LE
                if endian != bigEndian {
                    if i+1 < len(data) {
                        i++
                        if data[i] == '\x00' {
                            endian = littleEndian
                            // We have a full newline-terminated line.
                            return i + 1, dropCRLE(data[0 : i-1]), nil
                        }
                    }
                }
            }
            i++
        }

        // If we're at EOF, we have a final, non-terminated line. Return it.
        if atEOF {
            // drop CR.
            advance = len(data)
            switch endian {
            case bigEndian:
                data = dropCRBE(data)
            case littleEndian:
                data = dropCRLE(data)
            default:
                data, endian = dropCR(data)
            }
            if endian == unknownEndian {
                if runtime.GOOS == "windows" {
                    endian = littleEndian
                } else {
                    endian = bigEndian
                }
            }
            return advance, data, nil
        }

        // Request more data.
        return 0, nil, nil
    }

    // Endian byte order function
    orderFunc := func() (byteOrder binary.ByteOrder) {
        switch endian {
        case bigEndian:
            byteOrder = binary.BigEndian
        case littleEndian:
            byteOrder = binary.LittleEndian
        }
        return byteOrder
    }

    return splitFunc, orderFunc
}

func main() {
    file, err := os.Open("utf16.le.txt")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    defer file.Close()
    fmt.Println(file.Name())

    rdr := bufio.NewReader(file)
    scanner := bufio.NewScanner(rdr)
    var bo binary.ByteOrder // unknown, infer from data
    // bo = binary.LittleEndian // windows
    splitFunc, orderFunc := ScanUTF16LinesFunc(bo)
    scanner.Split(splitFunc)

    for scanner.Scan() {
        b := scanner.Bytes()
        s := UTF16BytesToString(b, orderFunc())
        fmt.Println(len(s), s)
        fmt.Println(len(b), b)
    }
    fmt.Println(orderFunc())

    if err := scanner.Err(); err != nil {
        fmt.Println(err)
    }
}

输出:

utf16.le.txt
15 "Hello, 世界"
22 [34 0 72 0 101 0 108 0 108 0 111 0 44 0 32 0 22 78 76 117 34 0]
0 
0 []
15 "Hello, 世界"
22 [34 0 72 0 101 0 108 0 108 0 111 0 44 0 32 0 22 78 76 117 34 0]
LittleEndian

utf16.be.txt
15 "Hello, 世界"
22 [0 34 0 72 0 101 0 108 0 108 0 111 0 44 0 32 78 22 117 76 0 34]
0 
0 []
15 "Hello, 世界"
22 [0 34 0 72 0 101 0 108 0 108 0 111 0 44 0 32 78 22 117 76 0 34]
BigEndian
于 2013-04-03T17:31:13.190 回答
7

这是最简单的阅读方法:

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"

    "golang.org/x/text/encoding/unicode"
    "golang.org/x/text/transform"
)

func main() {
    file, err := os.Open("./text.txt")
    if err != nil {
        log.Fatal(err)
    }

    scanner := bufio.NewScanner(transform.NewReader(file, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
    for scanner.Scan() {
        fmt.Printf(scanner.Text())
    }
}

由于 Windows 默认使用 little-endian 顺序,因此我们使用unicode.UseBOM策略从文本中检索 BOM,并使用 unicode.LittleEndian 作为后备

于 2019-04-11T12:28:58.920 回答
4

例如:

package main

import (
        "errors"
        "fmt"
        "log"
        "unicode/utf16"
)

func utf16toString(b []uint8) (string, error) {
        if len(b)&1 != 0 {
                return "", errors.New("len(b) must be even")
        }

        // Check BOM
        var bom int
        if len(b) >= 2 {
                switch n := int(b[0])<<8 | int(b[1]); n {
                case 0xfffe:
                        bom = 1
                        fallthrough
                case 0xfeff:
                        b = b[2:]
                }
        }

        w := make([]uint16, len(b)/2)
        for i := range w {
                w[i] = uint16(b[2*i+bom&1])<<8 | uint16(b[2*i+(bom+1)&1])
        }
        return string(utf16.Decode(w)), nil
}

func main() {
        // Simulated data from e.g. a file
        b := []byte{255, 254, 91, 0, 83, 0, 99, 0, 114, 0, 105, 0, 112, 0, 116, 0, 32, 0, 73, 0, 110, 0, 102, 0, 111, 0, 93, 0, 13, 0}
        s, err := utf16toString(b)
        if err != nil {
                log.Fatal(err)
        }

        fmt.Printf("%q", s)
}

(也在这里

输出:


"[Script Info]\r"
于 2013-04-03T10:05:33.547 回答
0

如果您想将任何内容打印为字符串,您可以使用fmt.Sprint

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    // read whole the file
    f, err := os.Open("test.txt")
    if err != nil {
        fmt.Printf("error opening file: %v\n", err)
        return
    }
    r := bufio.NewReader(f)
    var s, _, e = r.ReadLine()
    if e != nil {
        fmt.Println(e)
        return
    }
    fmt.Println(fmt.Sprint(string(s)))
}
于 2019-07-16T02:57:37.120 回答