Go批量操作excel导入到mongodb的技巧

 更新时间:2022年03月24日 11:36:54   作者:1907  
这篇文章主要介绍了Go批量操作excel导入到mongo,包括选择命令行包,读取配置连接数据库的方法,本文示例代码相结合给大家介绍的非常详细,需要的朋友可以参考下

需求:完成一个命令工具,批量处理某个目录下面的一些excel,将这些excel数据导入到mongodb,同时可以同步到mysql

:: 花了一天时间写完代码,代码库位置:https://gitee.com/foz/lib/tree/master/ecc

代码目录:

├─cmd
|  └─ecc.go     # 命令
├─configs
├─data
├─internal
│  └─importing  # 主要逻辑处理
├─pkg           # 处理文件读取、连接数据库等
│  ├─files
│  ├─mongo
│  └─mysql
├─queue
└─tools

1. 选择命令行包

平常使用的的命令工具包有:

  • urfave/cli
  • spf13/cobra

这里使用的是urfave/cli包,比较简单

var DirPath = "../data"     // 默认位置
var dir = DirPath
app := &cli.App{
		Name:  "Ecc",
		Usage: "Ecc is a tools for batch processing of excel data",
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:        "model",
				Aliases:     []string{"m"},
				Usage:       "The model of searching",
				Value:       "model",
				Destination: &model,
			},
			&cli.StringFlag{    // 设置一个 -d 的参数,用来确定目标文件夹位置
				Name:        "dir",
				Aliases:     []string{"d"},
				Usage:       "Folder location of data files",
				Destination: &dir,
				Value:       DirPath,
		},
		Action: func(c *cli.Context) error {
			importing.Load("../configs/cfg.yaml")  // 引入配置文件,读取mongodb、mysql等配置
			importing.Handle(dir)  ## 具体逻辑处理
			return nil
	}

2. 读取配置,连接数据库

读取配置使用spf13/viper库,需要读取一下配置,连接mongodb

var C Config

type Config struct {
	Env   string `yaml:"env"`
	Mongo struct {
		DNS        string `yaml:"dns"`
		Db         string `yaml:"db"`
		Collection string `yaml:"collection"`
	} `yaml:"mongo"`
	Mysql struct {
		Alias string `yaml:"alias"`
		Dns   string `yaml:"dns"`
	} `yaml:"mysql"`
}
func Load(cf string) {
	var err error
	viper.SetConfigFile(cf)
	if err = viper.ReadInConfig(); err != nil {
		log.Fatal(fmt.Errorf("fatal error config file: %s \n", err))
	}
	if err = viper.Unmarshal(&configs.C); err != nil {
		log.Fatal(fmt.Errorf("unmarshal conf failed, err:%s \n", err))
	if err = mongo.Conn(configs.C.Mongo.DNS, configs.C.Mongo.Db); err != nil {
		log.Fatal(color.RedString("%s:\n%v", "mongo connect err", err))
	if mongo.CheckCollection(configs.C.Mongo.Collection) {
		if err = mongo.DelCollection(configs.C.Mongo.Collection); err != nil {
			log.Fatal(color.RedString("%s:\n%v", "mongo del collection err", err))
		}
	if err = mongo.CreateCollection(configs.C.Mongo.Collection); err != nil {
		log.Fatal(color.RedString("%s:\n%v", "mongo create collection err", err))

3. 读取文件

先确定文件权限以及文件是否存在

func ReadDir(dir string) ([]os.FileInfo, error) {
	perm := checkPermission(dir)
	if perm == true {
		return nil, fmt.Errorf("permission denied dir: %s", dir)
	}

	if isNotExistDir(dir) {
		return nil, fmt.Errorf("does not exist dir: %s", dir)
	files, err := ioutil.ReadDir(dir)
	if err == nil {
		return files, err
	return nil, fmt.Errorf("ReadDir: %s, err: %v", dir, err)
}

拿到文件后就要并发读取每个excel文件数据

这里需求是一次任务必须读完所有的文件,任何一个文件有错误就退出程序。

:: 所以需要定义异常退出信道和一个完成读取两个信道,总的数据使用sync.Map安全并发写入。

3.1. 并发读

rWait   = true
rDone   = make(chan struct{})
rCrash  = make(chan struct{})

read(f, dir, data)
for rWait {  		// 使用for循环来阻塞读文件
	select {
	case <-rCrash:
		abort("-> Failure")
		return
	case <-rDone:
		rWait = false
	}
}
func read(fs []os.FileInfo, dir string, data *sync.Map) {
	for _, file := range fs {
		fileName := file.Name()
		_ext := filepath.Ext(fileName)
		if Include(strings.Split(Exts, ","), _ext) {
			wg.Add(1)
			inCh := make(chan File)
			go func() {
				defer wg.Done()
				select {
				case <-rCrash:
					return // 退出goroutine
				case f := <-inCh:
					e, preData := ReadExcel(f.FilePath, f.FileName, pb)
					if e != nil {
						tools.Red("%v", e)
						// 使用sync.once防止多个goroutine关闭同一个信道
						once.Do(func() { 
							close(rCrash)
						})
						return
					}
					data.Store(f.FileName, preData)
				}
			}()
				inCh <- File{
					FileName: fileName,
					FilePath: dir + string(os.PathSeparator) + fileName,
		}
	go func() {
		wg.Wait()
		close(rDone)
	}()

3.2. 使用excelize处理excel

excelize是一个非常好用的excel处理库,这里使用这个库读取excel文件内容

type ExcelPre struct {
	FileName    string
	Data        [][]string
	Fields      []string
	Prefixes    string
	ProgressBar *mpb.Bar  // 进度条对象
}

func ReadExcel(filePath, fileName string, pb *mpb.Progress) (err error, pre *ExcelPre) {
	f, err := excelize.OpenFile(filePath)
	if err != nil {
		return err, nil
	}
	defer func() {
		if _e := f.Close(); _e != nil {
			fmt.Printf("%s: %v.\n\n", filePath, _e)
		}
	}()
	// 获取第一页数据
	firstSheet := f.WorkBook.Sheets.Sheet[0].Name
	rows, err := f.GetRows(firstSheet)
	lRows := len(rows)
	if lRows < 2 {
		lRows = 2
	rb := ReadBar(lRows, filePath, pb)
	wb := WriteBar(lRows-2, filePath, rb, pb)
	var fields []string
	var data [][]string
        // 进度条增加一格
	InCr := func(start time.Time) {
		rb.Increment()
		rb.DecoratorEwmaUpdate(time.Since(start))
	for i := 0; i < lRows; i++ {
		InCr(time.Now())
		// 这里对第一行处理,用来判断一些约定的条件
		if i == 0 {
			fields = rows[i]
			for index, field := range fields {
				if isChinese := regexp.MustCompile("[\u4e00-\u9fa5]"); isChinese.MatchString(field) || field == "" {
					err = errors.New(fmt.Sprintf("%s: line 【A%d】 field 【%s】 \n", filePath, index, field) + "The first line of the file is not a valid attribute name.")
					return err, nil
				}
			}
			continue
		// 过滤第二行,这一行通常是中文解释字段
		if i == 1 {
		data = append(data, rows[i])
	return nil, &ExcelPre{
		FileName:    fileName,
		Data:        data,
		Fields:      fields,
		Prefixes:    Prefix(fileName),
		ProgressBar: wb,

3.3. 使用mpb在命令行输出进度显示

mpb是一个很好用的命令行进度输出库,上面代码里里有两个进度条,一个是读进度条,第二个是写进度条,读进度条在文件读取的时候就显示了,返回的结构体里有写进度条对象,便于后面写操作时候显示。

下面是两个进度条显示的配置,具体参数可以看这个库的文档。

func ReadBar(total int, name string, pb *mpb.Progress) *mpb.Bar {
	return pb.AddBar(int64(total),
		mpb.PrependDecorators(
			decor.OnComplete(decor.Name(color.YellowString("reading"), decor.WCSyncSpaceR), color.YellowString("waiting")),
			decor.CountersNoUnit("%d / %d", decor.WCSyncWidth, decor.WCSyncSpaceR),
		),
		mpb.AppendDecorators(
			decor.NewPercentage("%.2f:", decor.WCSyncSpaceR),
			decor.EwmaETA(decor.ET_STYLE_MMSS, 0, decor.WCSyncWidth),
			decor.Name(": "+name),
	)
}

func WriteBar(total int, name string, beforeBar *mpb.Bar, pb *mpb.Progress) *mpb.Bar {
		mpb.BarQueueAfter(beforeBar, false),
		mpb.BarFillerClearOnComplete(),
			decor.OnComplete(decor.Name(color.YellowString("writing"), decor.WCSyncSpaceR), color.GreenString("done")),
			decor.OnComplete(decor.CountersNoUnit("%d / %d", decor.WCSyncSpaceR), ""),
			decor.OnComplete(decor.NewPercentage("%.2f:", decor.WCSyncSpaceR), ""),
			decor.OnComplete(decor.EwmaETA(decor.ET_STYLE_MMSS, 0, decor.WCSyncWidth), ""),
			decor.OnComplete(decor.Name(": "+name), name),

4. 写入mongodb

同写入操作,这里拿到所有数据,然后使用goroutine并发写入mongodb,在处理数据时候需要查重,还需要记录一下本次操作插入了哪些数据的_id值,在报错的时候进行删除(这里可以使用事务,直接删除简单些),所以定义了一个Shuttle结构体用来在记录并发时的数据。

wWait   = true
wDone   = make(chan struct{})
wCrash  = make(chan struct{})

type Shuttle struct {
	Hid []string  	// 用来判断是否是重复数据
	Mid []string  	// 用来记录本次插入的数据_id
	mu  sync.Mutex
}
func (s *Shuttle) Append(t string, str string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	switch t {
	case "h":
		s.Hid = append(s.Hid, str)
	case "m":
		s.Mid = append(s.Mid, str)
	}
write2mongo(data)
for wWait {
	select {
	case <-wCrash:
		abort("-> Failure")
		return
	case <-wDone:
		wWait = false
func write2mongo(data *sync.Map) {
	collection := mongo.GetCollection(configs.C.Mongo.Collection)
	data.Range(func(key, value interface{}) bool {
		if v, ok := value.(*ExcelPre); ok {
			wg.Add(1)
			inCh := make(chan []bson.M)
			go func() {
				defer wg.Done()
				select {
				case <-wCrash:
					return // exit
				case rows := <-inCh:
					e := Write2Mongo(rows, collection, v, &shuttle)
					if e != nil {
						tools.Red("%v", e)
						once.Do(func() {
							close(wCrash)
						})
						return
					}
				}
			}()
				inCh <- PreWrite(v)
		}
		return true
	})
	go func() {
		wg.Wait()
		close(wDone)
	}()
// 具体处理逻辑
func Write2Mongo(rows []bson.M, collection *mongoDb.Collection, v *ExcelPre, s *Shuttle) error {
	v.ProgressBar.SetCurrent(0)
	incr := func(t time.Time, b *mpb.Bar, n int64) {
		b.IncrInt64(n)
		b.DecoratorEwmaUpdate(time.Since(t))
	for _, row := range rows {
		start := time.Now()
		key := v.Prefixes + "@@" + row["_hid"].(string)
		s.mu.Lock()
		if Include(s.Hid, key) {
			s.mu.Unlock()
			incr(start, v.ProgressBar, 1)
			continue
		} else {
			s.Hid = append(s.Hid, key)
		var err error
		var id primitive.ObjectID
		if id, err = mongo.CreateDocs(collection, row); err != nil {
			return errors.New(fmt.Sprintf("%s:\n%v", "mongo create docs err", err))
		s.Append("m", id.Hex())
		incr(start, v.ProgressBar, 1)
	return nil

5. 同步mysql

因为同步mysql不是必要的,这里使用命令行输入进行判断:

tools.Yellow("-> Whether to sync data to mysql? (y/n)")
if !tools.Scan("aborted") {
	return
} else {
	tools.Yellow("-> Syncing data to mysql...")
	if err = write2mysql(); err != nil {
		tools.Red("-> Failure:" + err.Error())
	} else {
		tools.Green("-> Success.")
	}
}

连接mysql数据库,拿到当前monogodb的数据:

func write2mysql() error {
	if err := mysql.Conn(configs.C.Mysql.Dns); err != nil {
		return err
	}

	d, err := mongo.GetCollectionAllData(configs.C.Mongo.Collection)
	if err != nil {
	err = Write2Mysql(d)
	return err
}

创建表,直接拼sql就完事了:

func CreateTable(tableName string, fields []string) error {
	var err error
	delSql := fmt.Sprintf("DROP TABLE IF EXISTS `%s`", tableName)
	err = Db.Exec(delSql).Error
	if err != nil {
		return err
	}

	s := "id bigint(20) NOT NULL PRIMARY KEY"
	for _, field := range fields {
		s += fmt.Sprintf(",%s varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL", field)
	sql := fmt.Sprintf("CREATE TABLE `%s` (%s) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", tableName, s)
	err = Db.Exec(sql).Error
	return nil
}

插入数据,bson.M本身就是一个map,转一下使用gorm分批插入数据,速度快一点:

func InsertData(tableName string, fields []string, data []bson.M) error {
	var err error
	var maps []map[string]interface{}
	for _, d := range data {
		row := make(map[string]interface{})
		for _, field := range fields {
			row[field] = d[field]
		}
		if row != nil {
			row["id"] = d["id"].(string)
			maps = append(maps, row)
	}

	if len(maps) > 0 {
		err = Db.Table(tableName).CreateInBatches(maps, 100).Error
		if err != nil {
			return err
	return err
}

6. 总结

做为golang新手,看了很多文档、文章,好似懂了,其实啥都不懂。

到此这篇关于Go批量操作excel导入到mongo的文章就介绍到这了,更多相关Go excel导入mongo内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 用Go语言编写一个简单的分布式系统

    用Go语言编写一个简单的分布式系统

    这篇文章主要介绍了用Go语言编写一个简单的分布式系统,文中的代码示例讲解的非常详细,对我们的学习或工作有一定的帮助,感兴趣的小伙伴跟着小编一起来看看吧
    2023-08-08
  • Go 中 time.After 可能导致的内存泄露问题解析

    Go 中 time.After 可能导致的内存泄露问题解析

    这篇文章主要介绍了Go 中 time.After 可能导致的内存泄露,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • golang 中 recover()的使用方法

    golang 中 recover()的使用方法

    这篇文章主要介绍了Guam与golang  recover()的使用方法,Recover 是一个Go语言的内建函数,可以让进入宕机流程中的 goroutine 恢复过来,下文更多相关资料需要的小伙伴可以参考一下
    2022-04-04
  • golang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2018-03-03
  • Golang之reflect包的使用方法详解

    Golang之reflect包的使用方法详解

    Golang的Reflect包(reflect)是一个强大的内置包,它提供了在运行时进行程序反射的功能,帮助我们编写更加灵活、通用且动态的代码,为Golang开发者带来了更多的可能性,感兴趣的同学可以参考一下
    2023-06-06
  • Golang中switch语句和select语句的用法教程

    Golang中switch语句和select语句的用法教程

    这篇文章主要给大家介绍了关于Golang中switch和select的用法教程,文中通过示例代码将switch语句与select语句的使用方法介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面跟着小编一起来学习学习吧。
    2017-06-06
  • Go Gin 处理跨域问题解决

    Go Gin 处理跨域问题解决

    在前后端分离的项目中,经常会遇到跨域问题,本文主要介绍了Go Gin 处理跨域问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-05-05
  • Golang中urlencode与urldecode编码解码详解

    Golang中urlencode与urldecode编码解码详解

    这篇文章主要给大家介绍了关于Golang中urlencode与urldecode编码解码的相关资料,在Go语言中转码操作非常方便,可以使用内置的encoding包来快速完成转码操作,Go语言中的encoding包提供了许多常用的编码解码方式,需要的朋友可以参考下
    2023-09-09
  • Go语言中http.ResponseWriter接口

    Go语言中http.ResponseWriter接口

    http.ResponseWriter是Go语言中用来设置HTTP响应的接口,本文主要介绍了Go语言中http.ResponseWriter接口,具有一定的参考价值,感兴趣的可以了解一下
    2024-08-08
  • Go语言JSON解析器gjson使用方法详解

    Go语言JSON解析器gjson使用方法详解

    这篇文章主要介绍了Go语言json解析框架与gjson,JSON 解析是我们不可避免的常见问题,在Go语言中,我们可以借助gjson库来方便的进行json属性的提取与解析,需要的朋友可以参考一下
    2022-12-12

最新评论