【数据读取】go 读取 csv 文件

【数据读取】go 读取 csv 文件,第1张

目录 场景你能从本文中学到什么实际案例 开发读取 csv 文件预处理表头按照数据填充结构体技巧使用场景技巧写法麻烦一点的面向对象写法 总结

场景 你能从本文中学到什么 如何用 go 语言读取 csv 文件(第一行为列名,第二行开始为数据)如何将读取到的数据,填充入任意结构体数据中,类似于
type AnyStruct struct {
	m map[int32]string
	s []bool
	...
}
    
	stru:= AnyStruct{}
	struList, err:=csv.Unmarshal(str,&stru) // struList map[string]AnyStruct{}
...
一些使用 reflect 库的实际经验对于单处理函数接口的简化技巧 实际案例

我们现有的游戏项目采用 UE4 引擎,策划配置的数据以 .uasset 文件格式存储。后端各服务需要读取到指定的游戏数据(例如交易服务需要获取物品列表和价格)。我们采用如下的开发计划:

使用 UE4 自带的 Python 环境,编写python脚本脚本自动把指定的 .uasset 转成 .csv (待更新成博客)

注:.uasset 转 .csv 是 UE4 自带功能,支持打开引擎界面后,选中指定 .uasset(可多个)右击输出为 .csv。但是使用必须打开引擎界面,必须每次手动选择,没有命令行可执行、对特定的项目一键输出的脚本

后端使用 Go 语言,服务启动时读取需要的 .csv 文件数据,将数据按照自定义结构体的方式存储入内存,方便程序运行时(runtime)取用 (本文内容)后续开发定制化配表工具(C#),直接读取 .uasset 内容,策划也可直接使用定制化配表工具直接修改游戏数据,也能直接一键输出成任意后端可读取格式 (开发中,待更新) 开发

下述所有代码均节选核心逻辑展示,完整项目请查看 csvLoader.

读取 csv 文件
import (
    "encoding/csv"
    "os"
)

	f, err := os.Open(filepath)
	check(err) // 此处为伪代码,需要对错误进行处理,下同
	reader := csv.NewReader(f)
	preData, err := reader.ReadAll() // preData 数据格式为 [][]string
	check(err)

至此,我们将 csv 文件保存到一个二维数组 preData 中,第一维是行,第二维是列,例:
源文件 example.csv:

a,b,c
1,2,3
4,5,6

excel 等表格工具打开后:

abc
123
456

内存中:

fmt.Println(preData) // [[a b c] [1 2 3] [4 5 6]]
预处理表头
type fieldInfo struct {
	name    string 		// struct field name 结构体字段名
	kind    FieldKind 	// 结构体字段类型
	index   int       	// 结构体字段在表头的列号
	context reflect.Type// 附加类型信息,map、slice等复合类型需要用到
}

    // 检查处理表头
	length := len(preData) - 1
	if length < 0 {
		return nil, nil
	}
	allToLower(&preData[0]) // 将表头转化为小写,需求是大小写不敏感

	// 提取结构体参数名、类型、对应的位置
	vValue := reflect.ValueOf(v).Elem()
	fieldNum := vValue.NumField() // 获取结构体字段个数
	vType := reflect.TypeOf(v).Elem()
	fieldInfoList := make([]*fieldInfo, fieldNum) 
	for i := 0; i < fieldNum; i++ { //遍历所有结构体字段
		field := vType.Field(i)
		fieldInfoList[i] = &fieldInfo{
			name:  field.Name,
			kind:  getFieldKind(field.Type.Kind()), 		// 获取字段类型
			index: getFieldIndex(field.Name, &preData[0]),	// 获取字段在 csv 中的列号
		}
		if fieldInfoList[i].kind == Map || fieldInfoList[i].kind == Slice {
			fieldInfoList[i].context = field.Type
		}
	}

至此,我们将 csv 文件中的列名,按名称对应到了结构体的字段名称上,并提取了类型等信息(注:结构体中的字段如果未在csv中找到,则跳过不生成,csv中的字段结构体中未找到同理)

按照数据填充结构体
	data := make(map[string]interface{}, length)
	for i := 0; i < length; i++ {
		data[preData[i+1][0]] = reflect.New(vValue.Type()).Interface() // 新建一个给定结构体
		setFieldValue(data[preData[i+1][0]], &fieldInfoList, &preData[i+1]) // 填充数据
	}
	return data
	
func setFieldValue(v interface{}, infoList *[]*fieldInfo, data *[]string) {
	vValue := reflect.ValueOf(v).Elem()
	for i, info := range *infoList {
		if info.index > -1 {
		    // string 转化为字段指定类型
			value, err := fieldParser[info.kind]((*data)[info.index], info.context) 
			if err == nil {
				vValue.Field(i).Set(value) // 把动态生成的值写入结构体
			}
		}
	}
}

上述代码中,需要把csv单个单元格中的数据(string 格式),转化为结构体字段的给定格式(int、bool、map、slice 等),这里我使用的是 map[kind]handler 数组。

技巧使用场景

当出现如下情形时,使用这种方案高效且易于扩展:

大段类似的switch case 代码块
swicth kind {
	case String:
	// 处理
	case Int8:
	// 处理
	case Int16:
	// 处理
	case Map:
	// 处理 key
		swicth key {
			case String:
			...
		}
	// 处理 value
	...
} // 显而意见,这种大段switch很难维护,且在有嵌套(不管是递归、引用还是其他)时,很容易在增加删除一个case后出问题
使用事件系统(event handler),且事件和对应的处理函数不在运行时(runtime)发生变化

可以使用 map[kind]handler 数组,把函数作为值进行直接调用,如下:

技巧写法
type fieldHandler func(str string, context reflect.Type) (reflect.Value, error)
var (
	fieldParser = map[FieldKind]fieldHandler {
		Invalid: stringToInvalid,
		String: stringToString,
		Bool: stringToBool,
		Int: stringToInt,
		Int8: stringToInt8,
		Int16: stringToInt16,
		Int32: stringToInt32,
		Int64: stringToInt64,
		Uint: stringToUint,
		Uint8: stringToUint8,
		Uint16: stringToUint16,
		Uint32: stringToUint32,
		Uint64: stringToUint64,
		Float32: stringToFloat32,
		Float64: stringToFloat64,
		Map: stringToMap,
		Slice: stringToSlice,
	}
	fieldParserHelper map[FieldKind]fieldHandler
)
func init() {
	fieldParserHelper = fieldParser // fieldParserHelper 是在运行时赋值的指针,和 fieldParser 指向同一地址
}

func stringToInvalid(_ string, _ reflect.Type) (reflect.Value, error) {
	return reflect.Value{}, ErrUnsupportedDataType
}
func stringToString(str string, _ reflect.Type) (reflect.Value, error) {
	str = strings.Trim(str, "\"")
	return reflect.ValueOf(str), nil
}
.......
// stringToMap 格式 (A:"aaa",B:"ccc"),支持去除任何前后置空格
func stringToMap(str string, context reflect.Type) (reflect.Value, error) {
	if fieldParserHelper == nil {
		return reflect.Value{}, ErrInvalidDataSource
	}
	str = strings.Trim(str, " ")
	str = str[1:len(str)-1]
	strArr := strings.Split(str, ",")
	result := reflect.MakeMap(context) // reflect 动态生成指定格式的map
	for _, s := range strArr {
		s = strings.Trim(s, " ")
		kv := strings.Split(s, ":")
		if len(kv) != 2 {
			continue
		}

		// 避免循环引用,顺便检查是否是支持的数据类型,这里可以有嵌套,也能正常执行
		keyHandler, ok := fieldParserHelper[getFieldKind(context.Key().Kind())]
		if !ok {
			continue
		}
		valueKind, ok := fieldParserHelper[getFieldKind(context.Elem().Kind())]
		if !ok {
			continue
		}

		// 赋值
		key, err := keyHandler(kv[0], context.Key())
		if err != nil {
			continue
		}
		value, err := valueKind(kv[1], context.Elem())
		if err != nil {
			continue
		}
		result.SetMapIndex(key, value)	// 动态设置 map 的 key - value
	}
	return result, nil
}

// stringToSlice 格式 ("aaa","ccc"),支持去除任何前后置空格
func stringToSlice(str string, context reflect.Type) (reflect.Value, error) {
	if fieldParserHelper == nil {
		return reflect.Value{}, ErrInvalidDataSource
	}
	str = strings.Trim(str, " ")
	str = str[1:len(str)-1]
	strArr := strings.Split(str, ",")
	result := reflect.MakeSlice(context, 0, len(strArr)) // reflect 动态生成指定格式的 slice
	for _, s := range strArr {
		s = strings.Trim(s, " ")
		if handler, ok := fieldParserHelper[getFieldKind(context.Elem().Kind())]; ok {
			value, err := handler(s, context.Elem())
			if err != nil {
				continue
			}
			result = reflect.Append(result, value) // 动态增加 slice 的元素
		}
	}
	return result, nil
}

引入 fieldParserHelper 目的是避免对于 fieldParser 的循环引用,因为 fieldParserHelper 和 fieldParser 都是指向同一个 map 真正地址的指针,而指针可以在运行时再赋值,从而避免了 fieldParser 内函数引用 fieldParser 会导致的循环引用问题。
循环引用链路为 fieldParser — 存储了–> stringToMap 函数 — 函数里会使用–> fieldParser

麻烦一点的面向对象写法
type fieldParser interface {
	handler(string, reflect.Type) (reflect.Value, error)
}

type boolParser struct{}
func (bp *boolParser) handler(str string, context reflect.Type) (reflect.Value, error) {
	// 逻辑代码
}
type stringParser struct{}
func (sp *stringParser) handler(str string, context reflect.Type) (reflect.Value, error) {
	// 逻辑代码
}
...

这种标准面向对象写法也能达到需求,且执行效率并不差,但需要额外写不少结构体,这些结构体全局只会生成一次,且只做一个函数的活。

总结

本文是在我实现一个具体业务需求时,实现的一个 csv 解析库,他能用在 UE4 数据读取等多种涉及 csv 格式的场景。整个库的代码量约 300 行,且支持所有常用的数据格式解析。完整项目请查看 csvLoader。
如果你有更好的想法或建议,请留言指出,谢谢!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/989828.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存