CSV/TSV/Excel/Parquet 파일의 정리, 검증 및 쿼리를 위한 미니멀한 Go 툴킷
Source: Dev.to
많은 소프트웨어 시스템에서 모든 데이터가 데이터베이스 안에만 존재하는 것은 아닙니다.
때로는 CSV, TSV, 스프레드시트와 같은 구조화된 파일에 저장되며, 실제로 이러한 파일은 깨끗하지 않은 경우가 많습니다. 데이터가 수동으로 입력되기도 하고, 값이 누락될 수도 있으며, 행에 일관성이 없어 다운스트림 처리에 문제를 일으키기도 합니다. 파일이 크면 “Excel에서 고치려”는 시도가 충돌을 일으키거나 잘못된 레코드를 찾기 어렵게 만들 수 있습니다.
실제 프로젝트에서 이러한 문제를 반복해서 겪으면서, 구조화된 파일 데이터를 전처리, 검증, 가벼운 분석에 초점을 맞춘 세 개의 작은 Go 라이브러리를 만들었습니다. 세 라이브러리 모두 표준 io.Reader 인터페이스와 함께 동작하므로, 별도의 데이터 구조 없이 익숙한 Go 프리미티브를 사용할 수 있습니다.
- fileprep – 전처리 + 구조체 태그를 이용한 필드‑단위 검증
- fileframe – 필터링 및 검사를 위한 작고 불변인 DataFrame
- filesql – 임베디드 SQLite 데이터베이스를 통해 CSV/TSV/LTSV/Excel/Parquet에 바로 SQL 실행
이 라이브러리들은 독립적이지만 동일한 io.Reader‑기반 API를 공유합니다.
fileprep
Features
- 전처리: 트림, 치환, 유니코드 정규화, 타입 강제 변환 등
- 명확한 오류 보고: 어느 행의 어느 열에서 실패했는지 식별
- 복합 검증 지원 (열 간 규칙)
- CSV, TSV, LTSV, Parquet, Excel 등
io.Reader로 들어오는 모든 형식 지원
Example
type User struct {
Name string `prep:"trim" validate:"required"`
Email string `prep:"trim,lowercase"`
Age string
}
func main() {
csvData := `name,email,age
John Doe ,JOHN@EXAMPLE.COM,30
Jane Smith,jane@example.com,25
`
processor := fileprep.NewProcessor(fileprep.FileTypeCSV)
var users []User
reader, result, err := processor.Process(strings.NewReader(csvData), &users)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Processed %d rows, %d valid\n", result.RowCount, result.ValidRowCount)
for _, user := range users {
fmt.Printf("Name: %q, Email: %q\n", user.Name, user.Email)
}
// `reader` can be passed directly to filesql
_ = reader
}
Output
Processed 2 rows, 2 valid
Name: "John Doe", Email: "john@example.com"
Name: "Jane Smith", Email: "jane@example.com"
fileframe
Features
- 불변 DataFrame
- 필터링, 매핑, 그룹화
- 소규모/중간 규모 CSV/TSV 데이터에 대한 “한 번만 쓰는 변환”에 이상적
Example
// Sample sales data
csvData := `product,amount,category
Apple,100,Fruit
Banana,150,Fruit
Carrot,80,Vegetable
Orange,120,Fruit
Broccoli,90,Vegetable`
df, err := fileframe.NewDataFrame(strings.NewReader(csvData), fileframe.CSV)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Total rows: %d\n", df.Len())
fmt.Printf("Columns: %v\n", df.Columns())
// Filter
filtered := df.Filter(func(row map[string]any) bool {
amount, ok := row["amount"].(int64)
return ok && amount > 100
})
fmt.Printf("Rows with amount > 100: %d\n", filtered.Len())
// GroupBy + Sum
groupedDf, err := df.GroupBy("category")
if err != nil {
fmt.Println("Error:", err)
return
}
grouped, err := groupedDf.Sum("amount")
if err != nil {
fmt.Println("Error:", err)
return
}
for _, row := range grouped.ToRecords() {
fmt.Printf(" %s: %.0f\n", row["category"], row["sum_amount"])
}
Output
Total rows: 5
Columns: [product amount category]
Rows with amount > 100: 2
Fruit: 370
Vegetable: 170
filesql
filesql은 문자 그대로 “파일에 대한 SQL”이 아닙니다. 내부적으로 데이터를 임시 SQLite 데이터베이스에 로드하여, 별도의 데이터베이스를 관리할 필요 없이 완전한 SQL 기능을 제공합니다.
Example
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
db, err := filesql.OpenContext(ctx, "data.csv")
if err != nil {
log.Fatal(err)
}
defer db.Close()
rows, err := db.QueryContext(ctx, "SELECT * FROM data WHERE age > 25")
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var name string
var age int
if err := rows.Scan(&name, &age); err != nil {
log.Fatal(err)
}
fmt.Printf("Name: %s, Age: %d\n", name, age)
}
}
Go 개발자이며 Apache나 Python ETL 생태계 전체가 필요하지 않은 경우, 이 세 가지 가벼운 라이브러리가 워크플로에 잘 맞을 수 있습니다. 사용자 기반이 아직 작아 발견되지 않은 버그가 있을 수 있으니, 피드백과 이슈 보고를 적극 환영합니다.