Go语言如何从Multipart.File格式CSV提取数据并写入模板CSV?
解决Go中处理多表头CSV并映射结构体的问题
你的CSV文件属于非标准格式:包含前置无关行、多个独立的表头-数据块,且数值带千分位逗号。下面是分步解决方案,包含提取指定列、映射结构体、写入模板CSV的完整实现。
一、核心思路
- 过滤无效行:跳过前置的公司信息行(列数不足4的行);
- 识别表头:通过检测是否包含
Msrp/Dealer(支持大小写变体)来定位表头行,记录列名到索引的映射; - 解析数据行:针对每个表头块,提取对应列的数值,处理千分位逗号后转换为整数;
- 映射结构体:将解析后的数据赋值给结构体字段;
- 写入模板:将结构体数据按模板格式写入新CSV。
二、完整代码实现
1. 定义结构体
调整结构体字段名更清晰,明确对应CSV列:
type Part struct { PartName string // 对应CSV第一列(如VGA-1、CASE-1) Price int // 对应CSV的Dealer列 Msrp int // 对应CSV的Msrp列 }
2. 处理上传的CSV文件
import ( "encoding/csv" "fmt" "io" "strconv" "strings" ) // ProcessUploadedCSV 从Multipart.File中解析CSV并映射到Part切片 func ProcessUploadedCSV(f io.Reader) ([]Part, error) { reader := csv.NewReader(f) records, err := reader.ReadAll() if err != nil { return nil, fmt.Errorf("读取CSV失败: %w", err) } var parts []Part var currentColIndex map[string]int // 存储当前表头的列名与索引映射 // 定义目标列的大小写变体,兼容不同写法 targetCols := map[string]map[string]bool{ "Dealer": {"Dealer": true, "DEALER": true, "dealer": true}, "Msrp": {"Msrp": true, "MSRP": true, "msrp": true}, } for _, row := range records { // 跳过空行或列数不足的无效行 if len(row) < 4 { continue } // 检查当前行是否是表头 isHeader := false colIndex := make(map[string]int) for idx, col := range row { trimmedCol := strings.TrimSpace(col) // 只要包含Dealer或Msrp的任意变体,就判定为表头 if targetCols["Dealer"][trimmedCol] || targetCols["Msrp"][trimmedCol] { isHeader = true } colIndex[trimmedCol] = idx } if isHeader { // 更新当前表头的列索引映射 currentColIndex = colIndex continue } // 未找到有效表头前,跳过数据行 if currentColIndex == nil { continue } // 提取并处理各列数据 partName := strings.TrimSpace(row[0]) // 处理Dealer列(转换为int,先去掉千分位逗号) dealerVal := strings.TrimSpace(row[currentColIndex["Dealer"]]) dealerVal = strings.ReplaceAll(dealerVal, ",", "") price, err := strconv.Atoi(dealerVal) if err != nil { return nil, fmt.Errorf("解析Dealer价格%s失败: %w", dealerVal, err) } // 处理Msrp列 msrpVal := strings.TrimSpace(row[currentColIndex["Msrp"]]) msrpVal = strings.ReplaceAll(msrpVal, ",", "") msrp, err := strconv.Atoi(msrpVal) if err != nil { return nil, fmt.Errorf("解析Msrp价格%s失败: %w", msrpVal, err) } // 添加到结构体切片 parts = append(parts, Part{ PartName: partName, Price: price, Msrp: msrp, }) } return parts, nil }
3. 将数据写入CSV模板
// WriteToTemplateCSV 将Part切片写入指定的CSV模板(输出流) func WriteToTemplateCSV(parts []Part, w io.Writer) error { writer := csv.NewWriter(w) defer writer.Flush() // 写入模板表头(可根据你的模板需求修改) if err := writer.Write([]string{"部件名称", "经销商价格", "建议零售价"}); err != nil { return fmt.Errorf("写入表头失败: %w", err) } // 写入数据行,还原千分位格式 for _, part := range parts { priceStr := formatWithCommas(part.Price) msrpStr := formatWithCommas(part.Msrp) if err := writer.Write([]string{part.PartName, priceStr, msrpStr}); err != nil { return fmt.Errorf("写入行%s失败: %w", part.PartName, err) } } return nil } // formatWithCommas 给整数添加千分位分隔符 func formatWithCommas(n int) string { s := strconv.Itoa(n) if len(s) <= 3 { return s } var sb strings.Builder remainder := len(s) % 3 sb.WriteString(s[:remainder]) for i := remainder; i < len(s); i += 3 { if remainder != 0 { sb.WriteString(",") } sb.WriteString(s[i:i+3]) remainder = 1 } return sb.String() }
三、使用示例
在处理上传文件的Handler中调用:
func UploadHandler(w http.ResponseWriter, r *http.Request) { // 解析上传的文件 r.ParseMultipartForm(10 << 20) // 10MB file, _, err := r.FormFile("csv-file") if err != nil { http.Error(w, "获取上传文件失败", http.StatusBadRequest) return } defer file.Close() // 处理CSV parts, err := ProcessUploadedCSV(file) if err != nil { http.Error(w, fmt.Sprintf("处理CSV失败: %v", err), http.StatusInternalServerError) return } // 写入模板CSV(这里直接输出到响应,也可以写入文件) w.Header().Set("Content-Type", "text/csv") w.Header().Set("Content-Disposition", "attachment; filename=output.csv") if err := WriteToTemplateCSV(parts, w); err != nil { http.Error(w, fmt.Sprintf("生成CSV失败: %v", err), http.StatusInternalServerError) return } }
四、关键优化点
- 兼容大小写:通过map存储列名的大小写变体,避免因列名大小写不一致导致的解析失败;
- 灵活列索引:用map记录列名到索引的映射,无需硬编码列位置,适配表头顺序变化;
- 错误处理:每个步骤都添加明确的错误信息,便于调试;
- 千分位处理:解析时去除逗号转整数,写入时还原千分位格式,保证数据格式一致性。
内容的提问来源于stack exchange,提问作者carlcaroline
相关产品推荐
相关产品推荐

