feat: initialer Commit des csv2excel CLI-Tools
Go-CLI-Tool zum Konvertieren von CSV-Dateien in Excel (.xlsx), mit Mage-Build-System und Architektur-Dokumentation.
This commit is contained in:
commit
26b874674f
10 changed files with 328 additions and 0 deletions
127
main.go
Normal file
127
main.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
func main() {
|
||||
sep := flag.String("sep", "auto", "Trennzeichen: auto, ',', ';', '\\t'")
|
||||
enc := flag.String("enc", "utf8", "Encoding: utf8, windows1252")
|
||||
out := flag.String("o", "output.xlsx", "Ausgabedatei")
|
||||
flag.Parse()
|
||||
|
||||
files := flag.Args()
|
||||
if len(files) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Verwendung: csv2xlsx [flags] datei1.csv datei2.csv ...")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println(sep)
|
||||
fmt.Println(enc)
|
||||
fmt.Println(out)
|
||||
|
||||
xlsx := excelize.NewFile()
|
||||
firstSheet := true
|
||||
|
||||
for _, path := range files {
|
||||
|
||||
sheetName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
// Excelize: max 31 Zeichen, keine Sonderzeichen
|
||||
if len(sheetName) > 31 {
|
||||
sheetName = sheetName[:31]
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Fehler beim Öffnen %s: %v\n", path, err)
|
||||
continue
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var reader io.Reader = f
|
||||
if *enc == "windows1252" {
|
||||
reader = transform.NewReader(f, charmap.Windows1252.NewDecoder())
|
||||
}
|
||||
|
||||
delimiter := detectDelimiter(path, *sep)
|
||||
|
||||
csvReader := csv.NewReader(reader)
|
||||
csvReader.Comma = delimiter
|
||||
csvReader.LazyQuotes = true // tolerant bei kaputten CSV-Dateien
|
||||
|
||||
if firstSheet {
|
||||
xlsx.SetSheetName("Sheet1", sheetName)
|
||||
firstSheet = false
|
||||
} else {
|
||||
xlsx.NewSheet(sheetName)
|
||||
}
|
||||
|
||||
row := 1
|
||||
for {
|
||||
record, err := csvReader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warnung Zeile %d in %s: %v\n", row, path, err)
|
||||
continue
|
||||
}
|
||||
for col, val := range record {
|
||||
cell, _ := excelize.CoordinatesToCellName(col+1, row)
|
||||
xlsx.SetCellValue(sheetName, cell, val)
|
||||
}
|
||||
row++
|
||||
}
|
||||
fmt.Printf("✓ %s → Reiter \"%s\" (%d Zeilen)\n", path, sheetName, row-1)
|
||||
}
|
||||
|
||||
if err := xlsx.SaveAs(*out); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Fehler beim Speichern: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Gespeichert: %s\n", *out)
|
||||
|
||||
}
|
||||
|
||||
func detectDelimiter(path, hint string) rune {
|
||||
switch hint {
|
||||
case ";":
|
||||
return ';'
|
||||
case "\t":
|
||||
return '\t'
|
||||
case ",":
|
||||
return ','
|
||||
}
|
||||
// auto-detect: erste Zeile lesen und zählen
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ';'
|
||||
}
|
||||
defer f.Close()
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := f.Read(buf)
|
||||
line := string(buf[:n])
|
||||
if idx := strings.Index(line, "\n"); idx > 0 {
|
||||
line = line[:idx]
|
||||
}
|
||||
sc := strings.Count(line, ";")
|
||||
cc := strings.Count(line, ",")
|
||||
tc := strings.Count(line, "\t")
|
||||
if tc >= sc && tc >= cc {
|
||||
return '\t'
|
||||
}
|
||||
if sc >= cc {
|
||||
return ';'
|
||||
}
|
||||
return ','
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue