tsv文件操作

参考文档: https://blog.csdn.net/neweastsun/article/details/129654924

本文介绍TSV文件类型及其应用,同时介绍Golang语句读取TSV文件并转为struct的实现过程。

tsv 文件介绍

TSV(tab-separated values)文件表示以tab分割值的文件格式,也就是说,TSV文件包括一系列数据信息,其中数据使用tab符(也称制表符,\t)进行分割。与CSV文件格式类似,CSV使用半角逗号(,)分割。

TSV文件和CSV文件一样,非常通用,被大多数平台或处理软件支持,但TSV文件采用不可见制表符作为分隔符,被用户误用的概率较低,相对CSV容错性更好。

go 读取 tsv文件

golang 包encoding/csv提供了csv文件的读写功能,我们值得tsv和csv的差异仅为分隔符,因此下面代码可以很容易读取tsv:

package main

import (
	"encoding/csv"
	"fmt"
	"log"
	"os"
)

func main() {

	f, err := os.Open("users.csv")

	if err != nil {

		log.Fatal(err)
	}

	r := csv.NewReader(f)
	r.Comma = '\t'
	r.Comment = '#'

	records, err := r.ReadAll()

	if err != nil {
		log.Fatal(err)
	}

	fmt.Print(records)
}

解析为结构体

一般我们希望读取tsv文件并解析为struct,下面一起看一些开源代码实现。tsv文件可能包括标题行,同时字段增加tsv标签,示例如下:

type TestTaggedRow struct {
Age    int    `tsv:"age"`
Active bool   `tsv:"active"`
Gender string `tsv:"gender"`
Name   string `tsv:"name"`
}

因此定义Parse类型:

// Parser has information for parser
type Parser struct {
Headers    []string      // 标题数组
Reader     *csv.Reader   // 读取器
Data       interface{}   // 希望解析为结构体的类型
ref        reflect.Value // 反射值
indices    []int // indices is field index list of header array
structMode bool  // 结构模式,结构体有tsv标签
normalize  norm.Form     // 解析UTF8方式
}

定义无标题行的机构函数:

// NewParserWithoutHeader creates new TSV parser with given io.Reader
func NewParserWithoutHeader(reader io.Reader, data interface{}) *Parser {
r := csv.NewReader(reader)
r.Comma = '\t'

p := &Parser{
Reader:    r,
Data:      data,
ref:       reflect.ValueOf(data).Elem(),
normalize: -1,
}

return p
}

带标题行的解析构造函数:

// NewStructModeParser creates new TSV parser with given io.Reader as struct mode
func NewParser(reader io.Reader, data interface{}) (*Parser, error) {
r := csv.NewReader(reader)
r.Comma = '\t'

// 读取一行,即标题行;函数字符串数组
headers, err := r.Read()

if err != nil {
return nil, err
}

// 循环给标题数组赋值
for i, header := range headers {
headers[i] = header
}

p := &Parser{
Reader:     r,
Headers:    headers,
Data:       data,
ref:        reflect.ValueOf(data).Elem(),
indices:    make([]int, len(headers)),
structMode: false,
normalize:  -1,
}

// get type information
t := p.ref.Type()

for i := 0; i < t.NumField(); i++ {
// get TSV tag
tsvtag := t.Field(i).Tag.Get("tsv")
if tsvtag != "" {
// find tsv position by header
for j := 0; j < len(headers); j++ {
  if headers[j] == tsvtag {
    // indices are 1 start
    p.indices[j] = i + 1
    p.structMode = true
  }
}
}
}

if !p.structMode {
for i := 0; i < len(headers); i++ {
p.indices[i] = i + 1
}
}

return p, nil
}

与上面无标题行相比,多了解析tsv标签的逻辑。

下面开始解析每行数据,我们看Next()方法:

// Next puts reader forward by a line
func (p *Parser) Next() (eof bool, err error) {

// Get next record
var records []string

for {
// read until valid record
records, err = p.Reader.Read()
if err != nil {
if err.Error() == "EOF" {
  return true, nil
}
return false, err
}
if len(records) > 0 {
break
}
}

if len(p.indices) == 0 {
p.indices = make([]int, len(records))
// mapping simple index
for i := 0; i < len(records); i++ {
p.indices[i] = i + 1
}
}

// record should be a pointer
for i, record := range records {
idx := p.indices[i]
if idx == 0 {
// skip empty index
continue
}
// get target field
field := p.ref.Field(idx - 1)
switch field.Kind() {
case reflect.String:
// Normalize text
if p.normalize >= 0 {
  record = p.normalize.String(record)
}
field.SetString(record)
case reflect.Bool:
if record == "" {
  field.SetBool(false)
} else {
  col, err := strconv.ParseBool(record)
  if err != nil {
    return false, err
  }
  field.SetBool(col)
}
case reflect.Int:
if record == "" {
  field.SetInt(0)
} else {
  col, err := strconv.ParseInt(record, 10, 0)
  if err != nil {
    return false, err
  }
  field.SetInt(col)
}
default:
return false, errors.New("Unsupported field type")
}
}

return false, nil
}

上面主要逻辑就是通过反射解析并存储每行数据,并填充结构体的过程。这里仅考虑了string、bool、Int三种类型,当然我们可以扩展支持更多类型。

下面通过main函数进行测试:

package main

import (
	"fmt"
	"os"
)

type TestRow struct {
	Name   string // 0
	Age    int    // 1
	Gender string // 2
	Active bool   // 3
}

func main() {

	file, _ := os.Open("example.tsv")
	defer file.Close()

	data := TestRow{}
	parser, _ := NewParser(file, &data)

	for {
		eof, err := parser.Next()
		if eof {
			return
		}
		if err != nil {
			panic(err)
		}
		fmt.Println(data)
	}

}

打开文件,定义结构体对象,然后定义解析器,传入文件和结构体对象作为参数。解析结果存储在结构体对象中。上面代码参考tsv开源项目:https://github.com/dogenzaka/tsv。还有咱们更强大的开源库:https://github.com/shenwei356/csvtk,不仅解析CSV/TSV文件,还能实现不同格式的转换。