Golang实现Map[]到TSV(制表符分隔文件)的转换

Golang实现Map[]到TSV(制表符分隔文件)的转换 我获取的数据是一个映射:

[
map[user_id:3 user_name:John Doe] 
map[user_id:23 user_name:Donald Duck] 
map[user_id:24 user_name:Jane Doe]
]

我希望它成为一个简单的 .tsv 文件(制表符分隔),像这样:

user_id   -> user_name
3         -> John Doe 
23        -> Donald Duck 
24        -> Jane Doe
func main() {
	Connect()
	c := cron.New()
	c.AddFunc("@every 30s", SendMail)
	c.Start()
	time.Sleep(time.Duration(1<<63 - 1))
}

func SendMail() {
	data := getall("SELECT * FROM usr")
	t := time.Now()
	t.String()

	file := "test_" + t.Format("2006-01-02T15-04-05.000Z") + ".tsv"
	f, err := os.Create(file)

	fmt.Println(file)

	if err != nil {
		fmt.Println(err)
	}
	defer f.Close()
	_, err2 := f.WriteString(data)

}

这可以实现吗?


更多关于Golang实现Map[]到TSV(制表符分隔文件)的转换的实战教程也可以访问 https://www.itying.com/category-94-b0.html

18 回复

这是一个切片,其元素类型为映射。切片中的每个元素都是映射类型。

更多关于Golang实现Map[]到TSV(制表符分隔文件)的转换的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,CSV中的分隔符也可以是空格。

for k,v := range userMap {
  fmt.Fprintf(f,"%d\t%s\n",k,v)
}

你好 Sibert,

函数 getall("SELECT * FROM usr") 的返回类型是什么?你可以像这样检查:

fmt.Printf("%T\n", getall("SELECT * FROM usr"))

ljh: 函数 getall("SELECT * FROM usr") 的返回类型是什么?

[]map[string]interface{}

我假设您“将数据作为映射获取”,并以此为前提编写了示例,假设 userMap 是一个 map[int]string。请根据您的实际类型相应地调整示例。

func main() {
    fmt.Println("hello world")
}

谢谢,但我不是很理解:

for key, val := range data {
	fmt.Fprintf(f, "%d\t%s\n", key, val)
}

错误:

无法在 data 上进行遍历(类型为 interface {})

我做错了什么?如何从 data 中获取 userMap

假设你“获取到的数据是一个映射”

data := getall("SELECT * FROM usr") 返回

[ map[user_id:3 user_name:John Doe] map[user_id:23 user_name:Donald Duck] map[user_id:24 user_name:Jane Doe] ]

Dean_Davidson:

你可以使用 encoding/csv 来写入 TSV 文件。

当然,当值中包含分隔符时,这种方法在引号处理方面也会更加健壮,并且能正确处理被引号包裹的值内部的引号字符。

mje:

你可以清楚地看到,strings.Join 确实在这里的每个字段之间都放置了一个制表符:

是的,我发现这个方法有效。但我该如何处理“多行”字段呢?

它们显示为单独的行。

image

你可以直接操作 data。通过 for ... := range data { 循环遍历 data 的元素。每个元素都是一个 map[string]interface{}。从你的打印输出中可知,该映射中的两个键是 “user_id” 和 “user_name”。打印这些键对应的值,并用制表符 ‘\t’ 分隔。

for _, entryMap := range data {
  fmt.Fprintf(f, "%d\t%v\n", entryMap["user_id"], entryMap["user_name"])
}

Sibert:

tsk_desc 后面缺少一个制表符(是空格而不是制表符)tsk_id

你的映射中几乎可以肯定是将 "tsk_desc tsk_id" 作为键,而将 "Desc 176 176" 等作为值。请检查你的映射创建过程。

你可以清楚地看到,strings.Join 确实在每个字段之间放置了一个制表符:

Go Playground - The Go Programming Language

func main() {
    fmt.Println("hello world")
}

Sibert:

	keys := []string{}

{} 不正确

Sibert:

	var vals []interface{}

	for _, row := range data {
		for _, val := range row {
			vals = append(vals, val)
		}
	}

这看起来像是 data 中所有值的一个扁平切片。我认为这不是你想要的。

此外,你不能指望对 row 的迭代在行与行之间保持相同的顺序,也不能指望它与你的 cols 匹配。为了获得一致的顺序,最好遍历你的 keys 切片。这可能相关也可能不相关,但你可能需要考虑某一行中某一列的值缺失的情况。

但是,如果你想要一个通用的TSV函数,这里有一个简单的实现:

谢谢!这几乎正是我想要的。我发现的唯一问题是标题中的制表符添加不正确:

fmt.Fprintln(w, strings.Join(headers, "\t"))

tsk_desctsk_id 之间缺少一个制表符(显示为空格而不是制表符):

image

有没有办法用缺失的制表符替换这个空格?

我首先要提醒你,对于你想要表示的数据,[]map[string]interface{} 可能不是理想的选择。

有没有更好的通用方法来实现这个?

是的,这是一种方法。谢谢!

但我的本意是想让它更通用。这里有一个新手尝试的简陋版本: https://play.golang.com/p/wNDDSiMcKJk

远非完美,但至少展示了我想要实现的效果。

package main

import (
	"fmt"
)

func main() {
	data := getall("SELECT * FROM usr")
	tsv := map2csv(data)
	fmt.Println(tsv)

}

// Simulate db call
func getall(sql string) []map[string]interface{} {
	return []map[string]interface{}{
		{"user_id": 3, "user_name": "John Doe"},
		{"user_id": 23, "user_name": "Donald Duck"},
		{"user_id": 24, "user_name": "Jane Doe"},
	}
}

func map2csv(data []map[string]interface{}) (tsv []interface{}) {

	cols := make(map[string]struct{})
	for _, record := range data {
		for key, _ := range record {
			cols[key] = struct{}{}
		}
	}

	keys := []string{}
	for key := range cols {
		keys = append(keys, key)
	}

	fmt.Println(keys)

	var vals []interface{}

	for _, row := range data {
		for _, val := range row {
			vals = append(vals, val)
		}
	}

	return vals

}

这可行吗?使用 encoding/csv 可以实现吗?

[]map[string]interface{}

好的,根据你提供的数据结构,我使用 @mje 提供的示例代码可以正常运行:

package main

import (
	"fmt"
	"os"
	"time"
)

func main() {
	SendMail()
}

// 模拟数据库调用
func getall(sql string) []map[string]interface{} {
	return []map[string]interface{}{
		{"user_id": 3, "user_name": "John Doe"},
		{"user_id": 23, "user_name": "Donald Duck"},
		{"user_id": 24, "user_name": "Jane Doe"},
	}
}

// 发送邮件函数,已移除部分冗余代码
func SendMail() {
	data := getall("SELECT * FROM usr")
	t := time.Now()

	fileName := "test_" + t.Format("2006-01-02T15-04-05.000Z") + ".tsv"
	f, err := os.Create(fileName)
	if err != nil {
		fmt.Println(err)
	}
	defer f.Close()
	// 打印表头
	fmt.Fprint(f, "user_id\tuser_name\n")
	// 打印每条记录
	for _, entryMap := range data {
		fmt.Fprintf(f, "%d\t%v\n", entryMap["user_id"], entryMap["user_name"])
	}
}

我得到了一个包含以下内容的文件:

user_id	user_name
3	John Doe
23	Donald Duck
24	Jane Doe

你遇到了什么问题?另外值得注意的是,你也可以使用 encoding/csv 来写入 TSV 文件。读取器和写入器都支持更改逗号 符文。默认情况下它被设置为 ‘,’:

// Comma 是字段分隔符。
// 在 NewReader 中默认设置为逗号 (',')。
// Comma 必须是一个有效的符文,且不能是 \r、\n,
// 或 Unicode 替换字符 (0xFFFD)。
Comma rune

如果你想尝试这种方法,这里有一个示例

首先需要提醒你,对于你想要表示的数据,使用 []map[string]interface{} 可能并不理想。但是,如果你想要一个通用的 TSV 函数,这里有一个简单的实现:

// writeTSV writes tab-separated data to w. It doesn't support jagged maps, so
// all maps must contain the same keys. It also doesn't support escaped tabs so
// it is up to caller to sanitize data.
func writeTSV(w io.Writer, items []map[string]interface{}) {
	// Empty data
	if len(items) == 0 {
		fmt.Fprint(w, "Empty dataset")
		return
	}

	var headers = make([]string, len(items[0]))
	i := 0
	// Iterate over first item in our array to get headers (AKA keys)
	// These will not be in any specific order. See also:
	// https://stackoverflow.com/questions/9619479/go-what-determines-the-iteration-order-for-map-keys
	for key := range items[0] {
		headers[i] = key
		i++
	}
	// Since not in specific order, sort columns to make this predictable.
	sort.Strings(headers)
	// Print our header
	fmt.Fprintln(w, strings.Join(headers, "\t"))

	// Iterate over our maps and populate each row with data
	for _, row := range items {
		for i, column := range headers {
			// Print our row value. This is the part that would break
			// with jagged maps. TODO: support jagged maps?
			fmt.Fprint(w, row[column])
			// For the last column, we print newline
			if i+1 == len(headers) {
				fmt.Fprint(w, "\n")
			} else {
				// Otherwise, print our delimiter (tab in this case).
				fmt.Fprint(w, "\t")
			}
		}
	}
}

使用方法如下:

func SendMail() {
	data := getall("SELECT * FROM usr")

	t := time.Now()

	fileName := "test_" + t.Format("2006-01-02T15-04-05.000Z") + ".tsv"
	f, err := os.Create(fileName)
	if err != nil {
		fmt.Println(err)
	}
	defer f.Close()
	writeTSV(f, data)
}

整合起来,这里有一个写入 os.stdout 的 Go Playground 链接:

https://play.golang.com/p/-Qsx6igk8-E

总之,类似这样的代码应该能让你更接近你想要实现的目标。希望如此。

是的,可以直接将Map切片转换为TSV格式。以下是完整的实现方案:

package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    // 示例数据
    data := []map[string]interface{}{
        {"user_id": 3, "user_name": "John Doe"},
        {"user_id": 23, "user_name": "Donald Duck"},
        {"user_id": 24, "user_name": "Jane Doe"},
    }
    
    // 转换为TSV
    tsvData := convertToTSV(data)
    
    // 写入文件
    file := "users.tsv"
    f, err := os.Create(file)
    if err != nil {
        fmt.Println("创建文件失败:", err)
        return
    }
    defer f.Close()
    
    _, err = f.WriteString(tsvData)
    if err != nil {
        fmt.Println("写入文件失败:", err)
        return
    }
    
    fmt.Println("TSV文件已生成:", file)
}

func convertToTSV(data []map[string]interface{}) string {
    if len(data) == 0 {
        return ""
    }
    
    var builder strings.Builder
    
    // 写入表头
    headers := make([]string, 0, len(data[0]))
    for key := range data[0] {
        headers = append(headers, key)
    }
    builder.WriteString(strings.Join(headers, "\t") + "\n")
    
    // 写入数据行
    for _, row := range data {
        values := make([]string, 0, len(row))
        for _, key := range headers {
            values = append(values, fmt.Sprintf("%v", row[key]))
        }
        builder.WriteString(strings.Join(values, "\t") + "\n")
    }
    
    return builder.String()
}

针对你的具体场景,修改SendMail函数:

func SendMail() {
    data := getall("SELECT * FROM usr")
    
    // 转换为TSV格式
    tsvContent := convertToTSV(data)
    
    t := time.Now()
    file := "test_" + t.Format("2006-01-02T15-04-05") + ".tsv"
    f, err := os.Create(file)
    
    if err != nil {
        fmt.Println("创建文件失败:", err)
        return
    }
    defer f.Close()
    
    _, err = f.WriteString(tsvContent)
    if err != nil {
        fmt.Println("写入文件失败:", err)
        return
    }
    
    fmt.Println("TSV文件已生成:", file)
}

如果getall函数返回的是[]map[string]interface{}类型,这个方案可以直接使用。如果返回类型不同,需要相应调整convertToTSV函数的参数类型。

回到顶部