
ããã«ã¡ã¯ãã«ããã·ã§ãã«ããã· å¾æ¥å¡ãã®éçºãè¡ã£ã¦ãã nilpoona ã§ãã
æ¥åã¢ããªã±ã¼ã·ã§ã³ãä½ã£ã¦ããã¨ãé¿ãã¦éããªãã®ã CSV ã¤ã³ãã¼ãæ©è½ ã§ãã
æåã¯ãencoding/csv ã§èªãã§ã«ã¼ãåãã°å®è£
ã§ãããã¨èãã¦ä½ãå§ããã®ã§ããã仿§ãè¤éã«ãªãã«ã¤ãã¦ã以ä¸ã®ãããªèª²é¡ã«ç´é¢ãããã¨ãããã¾ãã
- ããªãã¼ã·ã§ã³ã¨ãã¼ã¹å¦çãæ··å¨ããã¨ã©ã¼ã®çºçç®æã追ãã¥ããã
- ãæåã³ã¼ãã Shift_JIS ã ã£ãããªã©å¤æ§ãªã¨ã³ã³ã¼ãã£ã³ã°ã¸ã®å¯¾å¿ã§ããã¸ãã¹ãã¸ãã¯ãè¤éã«ãªãã
- ãã¼ã¹ãããªãã¼ã·ã§ã³ã¨ã©ã¼ãå³åº§ã«ãªã¿ã¼ã³ãã¦ãã¾ãã¨ãã¦ã¼ã¶ã¼ã¯ä¸ã¤ã®ã¨ã©ã¼ãç´ãã¦ãã¾ã次ã®ã¨ã©ã¼ãåºããã¢ã°ã©å©ããã®ãããªä¿®æ£ãµã¤ã¯ã«ãç¹°ãè¿ããã¨ã«ãªãã
- ãã¼ã¿ãæ£ããç¶æ ãä¿è¨¼ãããªãã¾ã¾ãå¾ç¶ã®å¦çï¼DB ä¿åãªã©ï¼ã«æ¸¡ããã¦ãã¾ãã
ä»åã¯ããããã£ã CSV å¦çã«ããã課é¡ã解決ããããã«ãã Parse, don't validate ã ã¨ããèãæ¹ãåèã« CSV å¦çã®ã©ã¤ãã©ãªãå®è£ ãã¾ããã
ããã¯ãå ¥åãåã«æ¤è¨¼ï¼Validateï¼ãã¦çµããã®ã§ã¯ãªããåã®ãããã¼ã¿æ§é ã«å¤æï¼Parseï¼ãããã¨ã§ããã®å¾ã®å®å ¨æ§ãåã·ã¹ãã ã§ä¿è¨¼ãããã¨ããèãæ¹ã§ãã
ä»åã¯ãã®èãæ¹ãå ã«å®è£ ãã CSV ã©ã¤ãã©ãªã®è¨è¨ã«ã¤ãã¦ãç´¹ä»ãã¾ãã
ä¸è¨ã®èãæ¹ãåèã«ãå¦çã以ä¸ã®3ã¤ã®ãã§ã¼ãºã«åå²ãã¾ããã
- Reader: ã¨ã³ã³ã¼ãã£ã³ã°ã®è©³ç´°ãé è½ï¼Shift_JIS / BOM 対å¿ãªã©ï¼
- Parser: CSVè¡ â æ§é ä½ï¼ä¸éåï¼ã¸ã®å¤æ
- Validator: ä¸éå â æ¤è¨¼æ¸ã¿ã®åã¸ã®ææ ¼
ããããã®å®è£ 詳細ã¨ãè¨è¨æå³ãç´¹ä»ãã¾ãã
1. Reader 層ï¼ã¨ã³ã³ã¼ãã£ã³ã°ã®è©³ç´°ãé è½ãã
æ¥æ¬ã®æ¥åã·ã¹ãã ã§ã¯ãExcel ããåºåãã CSVããé »ç¹ã«ä½¿ããã¾ããããã㯠Shift_JIS ã ã£ãã BOM ãä»ãã¦ããããã¾ããããã¸ãã¹ãã¸ãã¯ï¼ããªãã¼ã·ã§ã³ã夿ï¼ã®æ®µéã§ã¯ãããããã¨ã³ã³ã¼ãã£ã³ã°ã®éããæèããã«æ¸ãããã«ãããã¨ããã§ãã
èªã¿è¾¼ã¿æã«ã¨ã³ã³ã¼ãã£ã³ã°ãèªåå¤å®ã»å¤æãã常ã«ãUTF-8 ã® Readerããè¿ãããã«å®è£ ãã¾ããã
// csv.go package csv import ( "bytes" "encoding/csv" "io" "unicode/utf8" // ... imports ) // newCsvReader 㯠Shift_JIS ã BOM ãééçã«å¦çãã func newCsvReader(reader io.Reader) (*csv.Reader, error) { data, err := io.ReadAll(reader) if err != nil { return nil, err } // 1. BOMä»ãUTF-8 ã® CSV ãã¡ã¤ã«ããµãã¼ãï¼BOMé¤å»ï¼ if hasBOM(data) { cleanData := removeBOM(data) return csv.NewReader(bytes.NewReader(cleanData)), nil } // 2. BOMãªã UTF-8 ã®ãµãã¼ã if utf8.Valid(data) { return csv.NewReader(bytes.NewReader(data)), nil } // 3. Shift_JIS ããµãã¼ãï¼UTF-8ã¸å¤æï¼ if convertData, ok := tryConvertShiftJISToUTF8(data); ok { return csv.NewReader(bytes.NewReader(convertData)), nil } return nil, fmt.Errorf("Unsupported Encoding Type") }
ãã®å±¤ããããããè æé²æ¢å±¤ï¼Anti-Corruption Layerï¼ãã¨ãã¦æ©è½ãããã¨ã§ãå¾ç¶ã®å¦çã¯ãã¡ã¤ã«ãã©ã®ãããªå½¢å¼ã§ä¿åããã¦ããããç¥ãå¿ è¦ããªããªãã¾ãã
2. Parser 層ï¼Generics ã¨ãªãã¬ã¯ã·ã§ã³ã«ããæ§é å
次ã«ãèªã¿è¾¼ãã è¡ã Go ã®æ§é ä½ã«ãããã³ã°ãã¾ãã
ããã§ã¯ãå夿ï¼String -> Int ãªã©ï¼ãã¨ãæ§é ä½ã¸ã®ãããã³ã°ãã®ã¿ãè¡ãããã¸ãã¹çãªããªãã¼ã·ã§ã³ã¯è¡ãã¾ããã
ä¸éåã®å®ç¾© (Parsed[T])
// parse.go // ColumnCountStatus: ã«ã©ã æ°ã®éä¸è¶³ç¶æ type ColumnCountStatus string const ( ColumnCountFew ColumnCountStatus = "few" ColumnCountMatch ColumnCountStatus = "match" ColumnCountTooMany ColumnCountStatus = "tooMany" ) // ParsedRecord[T]: 1è¡ãã¨ã®ãã¼ã¹çµæï¼æªæ¤è¨¼ï¼ type ParsedRecord[T any] struct { Record T ColumnCountStatus ColumnCountStatus } // Parsed[T]: ãã¼ã¹å¦çå ¨ä½ã®çµæ type Parsed[T any] struct { Headers []string Records []ParsedRecord[T] }
ãã¼ã¹å¦çã®å®è£
Go ã®ãªãã¬ã¯ã·ã§ã³ã使ã£ã¦ãæ§é ä½ã®ã¿ã°ï¼csv:"name"ï¼ãè¦ã¦å¤ãã»ãããã¾ãã
ããã§éè¦ãªã®ã¯ããã«ã©ã æ°ãåããªããã¨ãã£ãæ§é çãªåé¡ããå³åº§ã«ã¨ã©ã¼(return err)ã«ããã®ã§ã¯ãªããã¹ãã¼ã¿ã¹ã¨ãã¦è¨é²ãã¦ããç¹ã§ãã
ããã§å¦çã䏿ããã«ã¹ãã¼ã¿ã¹ã¨ãã¦è¨é²ãã¦ããã®ã¯ããã¨ã©ã¼ã®éç´ï¼Error Aggregationï¼ã ãå®ç¾ããããã§ãã
ãã¼ã¹æ®µéã§è¦ã¤ãã£ãä¸åã§å³åº§ã«ã¨ã©ã¼ãè¿ãã¦ãã¾ãã¨ãã¦ã¼ã¶ã¼ã¯ä¿®æ£ã¨ã¢ãããã¼ããä½åº¦ãç¹°ãè¿ããã¨ã«ãªã£ã¦ãã¾ãã¾ãã
Parser ã¯ããã¾ã§ãèµ·ãããã¨ï¼ã«ã©ã ä¸è¶³ãªã©ï¼ã®è¨é²ãã«å¾¹ããå¾ç¶ã® Validator ã§ä»ã®å ¥åãã¹ã¨åãã㦠ããã¡ã¤ã«å ã®å ¨ã¨ã©ã¼ãã¾ã¨ãã¦å ±åã ã§ããããã«ãããã¨ã§ãã¦ã¼ã¶ã¼ä½é¨ï¼UXï¼ãæãªããªãè¨è¨ã¨ãã¦ãã¾ãã
// parse.go (ç¶ã) func ParseTypedCSV[T any](ctx context.Context, r io.Reader) (*Parsed[T], error) { // ... reader使å¦ç ... records := make([]ParsedRecord[T], 0) for { record, err := reader.Read() if err == io.EOF { break } // ã«ã©ã æ°ä¸ä¸è´ãªã©ã®ã¨ã©ã¼ãã³ããªã³ã° var countStatus ColumnCountStatus = ColumnCountMatch if err != nil && errors.Is(err, csv.ErrFieldCount) { // ... ã«ã©ã æ°ã®éä¸è¶³ãå¤å®ã㦠countStatus ã«ã»ãã ... } else if err != nil { return nil, err } // ãªãã¬ã¯ã·ã§ã³ã§ãããã³ã°ï¼String -> Int夿ãªã©ï¼ var item T if err := mapRecordToStruct(record, rawHeaders, &item); err != nil { return nil, err } records = append(records, ParsedRecord[T]{ Record: item, ColumnCountStatus: countStatus, // â ã¨ã©ã¼ã§ã¯ãªãç¶æ ã¨ãã¦ä¿æ }) } return &Parsed[T]{Headers: rawHeaders, Records: records}, nil }
3. Validator 層ï¼ãã¸ãã¯ã®æ³¨å ¥ã¨åå®å ¨æ§ã®ä¿è¨¼
ãããè¨è¨ã®è¦ã§ãã
ãã©ã¤ãã©ãªã«ã¯æ±ç¨çãªæ¤è¨¼ããã¼ã ããæãããå ·ä½çãªãã¸ãã¹ã«ã¼ã«ã¯å¤é¨ããæ³¨å ¥ãããå½¢ãã¨ãã¾ããã
åå®ç¾©ã¨ Validate 颿°
// validate.go // ããªãã¼ã·ã§ã³å¾ã®ãå®å ¨ãªããã¼ã¿ type Valid[T any] struct { Headers []string Records []T } // å¤é¨ããæ³¨å ¥ããããªãã¼ã·ã§ã³é¢æ°ã®åå®ç¾© type ValidateRecordFunc[T any] func(ctx context.Context, record ParsedRecord[T]) ErrorCode funcValidate[T any]( ctx context.Context, parsed *Parsed[T], expectedHeader [][]string, validateFuncs ...ValidateRecordFunc[T], // â ããã§ã«ã¼ã«ãæ³¨å ¥ ) (*Valid[T], []ValidateErrorDetail) { details := []ValidateErrorDetail{} // 1. ãããã¼æ§é ã®æ¤è¨¼ if errCode := validateHeader(parsed.Headers, expectedHeader); errCode != "" { return nil, []ValidateErrorDetail{{LineNumber: 1, Code: errCode}} } // 2. ã¬ã³ã¼ããã¨ã®æ¤è¨¼ï¼æ³¨å ¥ããã颿°ãå®è¡ï¼ validRecords := make([]T, 0, len(parsed.Records)) for idx, record := range parsed.Records { lineNumber := idx + 2 hasError := false for _, validateFunc := range validateFuncs { if errCode := validateFunc(ctx, record); errCode != "" { details = append(details, ValidateErrorDetail{ LineNumber: lineNumber, Code: errCode, }) hasError = true } } // ã¨ã©ã¼ããªããã°ãæå¹ãªã¬ã³ã¼ããã¨ãã¦ãªã¹ãã«è¿½å if !hasError { validRecords = append(validRecords, record.Record) } } // ã¨ã©ã¼ãããã°ãValid[T] ã¯è¿ããªãï¼nil ãè¿ãï¼ if len(details) > 0 { return nil, details } // 3. å ¨ã¦åæ ¼ãã¦åã㦠Valid[T] ãè¿ã return &Valid[T]{ Headers: parsed.Headers, Records: validRecords, }, nil }
ãã®è¨è¨ã«ãããParsed[T] ãã Valid[T] ã¸ã®å¤æãæåããã°ããã¼ã¿ã¯æ´åæ§ãåãã¦ãããã¨ãä¿è¨¼ããã¾ãã
å®éã«ä½¿ã£ã¦ã¿ã
ãã®ã©ã¤ãã©ãªã使ãå´ã®ã³ã¼ãã¯ãé常ã«å®£è¨çã«ãªãã¾ãã
ãå¹´é½¢ãã§ãã¯ããªã©ã®ãã¸ãã¯ã¯ãå©ç¨å´ããããå®ç¾©ãã¦ã©ã¤ãã©ãªã«æ³¨å ¥ãã¾ãã
// main.go (å©ç¨ä¾) // 1. åãè¾¼ã¿ããCSVã®æ§é ãå®ç¾© type UserCSV struct { Name string `csv:"æ°å"` Age int `csv:"å¹´é½¢"` } func ImportUsers(ctx context.Context, file io.Reader) error { // 2. ãã¼ã¹ï¼èªã¿è¾¼ã¿ + æ§é åï¼ // ããã§ã¯ã¾ã ããªãã¼ã·ã§ã³ã¨ã©ã¼ã«ã¯ãªããªã parsed, err := csv.ParseTypedCSV[UserCSV](ctx, file) if err != nil { return err // ã·ã¹ãã ã¨ã©ã¼ç } // 3. ããªãã¼ã·ã§ã³ã«ã¼ã«ã®å®ç¾©ï¼ãã®ãã¡ã¤ã³åºæã®ãã¸ãã¯ï¼ // ParsedRecord[T] ãåãåã£ã¦ã¨ã©ã¼ã³ã¼ããè¿ã validateAge := func(ctx context.Context, r csv.ParsedRecord[UserCSV]) csv.ErrorCode { // ã«ã©ã æ°ãè¶³ããªãå ´åã®ãã§ãã¯ï¼Parserãæ®ããè¨é²ã確èªï¼ if r.ColumnCountStatus != csv.ColumnCountMatch { return csv.ErrTooManyColumns } // ãã¸ãã¹ãã¸ã㯠if r.Record.Age < 20 { return "AgeUnder20" } return "" } // 4. ããªãã¼ã·ã§ã³å®è¡ï¼ã«ã¼ã«ã®æ³¨å ¥ï¼ expectedHeader := [][]string{{"æ°å"}, {"å¹´é½¢"}} // â ããã§ããªãã¼ã·ã§ã³ãå®è¡ validData, errDetails := csv.Validate(ctx, parsed, expectedHeader, validateAge) if len(errDetails) > 0 { // ã¨ã©ã¼ãããã°ãä½è¡ç®ã®ä½ããããããããã¾ã¨ãã¦è¿å´ã§ãã return NewValidationError(errDetails) } // 5. å¾ç¶å¦ç // ããã«æ¥ãæç¹ã§ validData 㯠*Valid[UserCSV] åã§ããã // å ¨ãã¼ã¿ãã«ã¼ã«ã«é©åãã¦ãããã¨ãä¿è¨¼ããã¦ãã return saveToDB(validData.Records) }
ã¾ã¨ã
CSV å¦çã¯å¤é¨ããã®å ¥åãæ±ãããè¤éã«ãªããã¡ã§ããã以ä¸ã®ããã«è²¬åãåãããã¨ã§ãã¡ã³ããããªãã£ãåä¸ããããã¨ãã§ãã¾ããã
- Reader: ã¨ã³ã³ã¼ãã£ã³ã°ã®è©³ç´°ï¼Shift_JIS / BOM 対å¿ãªã©ï¼ãé è½ããè æé²æ¢å±¤ã¨ãã¦æ©è½ã
- Parser: CSVè¡ãä¸éåã¸å¤æãã¨ã©ã¼ã¯å³æãªã¿ã¼ã³ãããç¶æ ãã¨ãã¦è¨é²ããUX åä¸ã«è²¢ç®ã
- Validator: ä¸éåãæ¤è¨¼æ¸ã¿ã®åã¸ææ ¼ãæ±ç¨çãªæ¤è¨¼ããã¼ãæä¾ããå ·ä½çãªã«ã¼ã«ã¯å¤é¨ããæ³¨å ¥ããã
ç¹ã«ãããªãã¼ã·ã§ã³ãã¸ãã¯ã颿°ã¨ãã¦æ³¨å ¥ãããè¨è¨ã«ãããã¨ã§ãã©ã¤ãã©ãªèªä½ã夿´ãããã¨ãªããæ§ã ãªCSVï¼ã¦ã¼ã¶ã¼ãååãå¨åº«ãªã©ï¼ã«æè»ã«å¯¾å¿ã§ããæ§æã¨ãªãã¾ããã
CSV å¦çã®è¨è¨ã«ããã¦ãå°ãã§ãåèã«ãªãã°å¹¸ãã§ãã