package main import ( "encoding/csv" "encoding/json" "errors" "flag" "fmt" "html/template" "log" "net/http" "os" "path/filepath" "strings" "sync" ) type record map[string]string var ( addr = flag.String("addr", ":8080", "HTTP listen address") csvPath = flag.String("csv", "data.csv", "path to CSV with header row") keyCol = flag.String("key", "", "column name to search in (default: first column)") valueCol = flag.String("value", "", "column name to return (default: second column)") suffixLen = flag.Int("suffix", 0, "if >0, also match by last N characters of the scanned code") sep = flag.String("sep", ",", "CSV field separator (e.g. ',' or ';')") records []record headers []string mu sync.RWMutex exactMap map[string]record // canon(key) -> record suffixMap map[string][]record // lastN(canon(key), N) -> records ) func main() { flag.Parse() if err := loadCSV(*csvPath); err != nil { log.Fatalf("CSV laden fehlgeschlagen: %v", err) } log.Printf("CSV geladen: %s (%d Zeilen, %d Spalten) | key=%q value=%q sep=%q suffix=%d", *csvPath, len(records), len(headers), *keyCol, *valueCol, *sep, *suffixLen) http.HandleFunc("/", handleIndex) http.HandleFunc("/search", handleSearch) fmt.Println(headers) fmt.Println(records) log.Printf("Starte Server auf %s …", *addr) if err := http.ListenAndServe(*addr, nil); err != nil { log.Fatal(err) } } func errUnknownCol(kind, name string, headers []string) error { return errors.New(kind + " column '" + name + "' nicht gefunden. Verfügbare Spalten: " + strings.Join(headers, ", ")) } func loadCSV(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() r := csv.NewReader(f) if *sep != "" { r.Comma = []rune(*sep)[0] } else { r.Comma = ',' } r.FieldsPerRecord = -1 rows, err := r.ReadAll() if err != nil { return err } if len(rows) == 0 { return errors.New("leere CSV") } headers = rows[0] if len(headers) < 2 { return errors.New("mindestens zwei Spalten benötigt") } // Spaltenindizes bestimmen (EqualFold) idxKey := 0 idxVal := 1 if *keyCol != "" { if i := indexOf(headers, *keyCol); i >= 0 { idxKey = i } else { return errUnknownCol("key", *keyCol, headers) } } if *valueCol != "" { if i := indexOf(headers, *valueCol); i >= 0 { idxVal = i } else { return errUnknownCol("value", *valueCol, headers) } } if *keyCol == "" { *keyCol = headers[idxKey] } if *valueCol == "" { *valueCol = headers[idxVal] } // Zeilen einlesen + trimmen var recs []record for i := 1; i < len(rows); i++ { row := rows[i] if len(row) < len(headers) { tmp := make([]string, len(headers)) copy(tmp, row) row = tmp } m := make(record, len(headers)) for c, h := range headers { val := "" if c < len(row) { val = strings.TrimSpace(row[c]) } m[h] = val } recs = append(recs, m) } // Indizes bauen ex := make(map[string]record, len(recs)) var sx map[string][]record if *suffixLen > 0 { sx = make(map[string][]record, len(recs)) } for _, rec := range recs { k := rec[*keyCol] if k == "" { continue } ck := canon(k) ex[ck] = rec if *suffixLen > 0 { suf := lastN(ck, *suffixLen) if suf != "" { sx[suf] = append(sx[suf], rec) } } } mu.Lock() records = recs exactMap = ex suffixMap = sx mu.Unlock() // kleine Sichtprobe für Debug log.Printf("Header: %v", headers) if len(recs) > 0 { log.Printf("Sample Key raw=%q canon=%q", recs[0][*keyCol], canon(recs[0][*keyCol])) } return nil } func handleIndex(w http.ResponseWriter, r *http.Request) { tpl := template.Must(template.New("index").Parse(indexHTML)) data := struct { File string KeyCol string ValCol string Suffix int HasSuf bool }{ File: filepath.Base(*csvPath), KeyCol: *keyCol, ValCol: *valueCol, Suffix: *suffixLen, HasSuf: *suffixLen > 0, } _ = tpl.Execute(w, data) } func handleSearch(w http.ResponseWriter, r *http.Request) { q := strings.TrimSpace(r.URL.Query().Get("q")) qc := canon(q) // WICHTIG: normalisieren w.Header().Set("Content-Type", "application/json; charset=utf-8") if q == "" { _ = json.NewEncoder(w).Encode(searchResp{ Query: q, Message: "Gib einen Barcode ein.", Headers: headers, SuffixLen: *suffixLen, SearchKeyCol: *keyCol, ReturnValueCol: *valueCol, }) return } mu.RLock() defer mu.RUnlock() resp := searchResp{ Query: q, Headers: headers, SuffixLen: *suffixLen, SearchKeyCol: *keyCol, ReturnValueCol: *valueCol, } // 1) Exakter Treffer (case/whitespace-insensitive) if rec, ok := exactMap[qc]; ok { resp.ExactHit = &item{ Key: rec[*keyCol], Value: rec[*valueCol], FullRow: rec, } _ = json.NewEncoder(w).Encode(resp) return } // 2) Suffix-Treffer (letzte N Zeichen) mit canon if *suffixLen > 0 && len(qc) >= *suffixLen { suf := lastN(qc, *suffixLen) if cands, ok := suffixMap[suf]; ok && len(cands) > 0 { resp.SuffixUsed = true resp.TopCandidate = &item{ Key: cands[0][*keyCol], Value: cands[0][*valueCol], FullRow: cands[0], } if len(cands) > 1 { limit := len(cands) if limit > 10 { limit = 10 } resp.Candidates = toItems(cands[1:limit]) } _ = json.NewEncoder(w).Encode(resp) return } } // 3) Teiltreffer (Substring) – in der Key-Spalte var partial []item for _, rec := range records { if strings.Contains(canon(rec[*keyCol]), qc) { partial = append(partial, item{ Key: rec[*keyCol], Value: rec[*valueCol], FullRow: rec, }) if len(partial) >= 20 { break } } } if len(partial) > 0 { resp.TopCandidate = &partial[0] if len(partial) > 1 { if len(partial) > 10 { resp.Candidates = partial[1:10] } else { resp.Candidates = partial[1:] } } } else { resp.Message = "Kein Treffer." } _ = json.NewEncoder(w).Encode(resp) } func canon(s string) string { return strings.ToLower(strings.TrimSpace(s)) } func indexOf(ss []string, s string) int { for i, v := range ss { if strings.EqualFold(strings.TrimSpace(v), strings.TrimSpace(s)) { return i } } return -1 } func lastN(s string, n int) string { if n <= 0 { return "" } r := []rune(s) if len(r) <= n { return s } return string(r[len(r)-n:]) } type searchResp struct { Query string `json:"query"` ExactHit *item `json:"exact_hit,omitempty"` SuffixUsed bool `json:"suffix_used,omitempty"` TopCandidate *item `json:"top_candidate,omitempty"` Candidates []item `json:"candidates,omitempty"` Message string `json:"message,omitempty"` Headers []string `json:"headers,omitempty"` SuffixLen int `json:"suffix_len,omitempty"` SearchKeyCol string `json:"search_key_col,omitempty"` ReturnValueCol string `json:"return_value_col,omitempty"` } type item struct { Key string `json:"key"` Value string `json:"value"` FullRow record `json:"full_row,omitempty"` } func toItems(rs []record) []item { out := make([]item, 0, len(rs)) for _, rec := range rs { out = append(out, item{ Key: rec[*keyCol], Value: rec[*valueCol], FullRow: rec, }) } return out } const indexHTML = `
Datei: {{ .File }} • Suche in {{ .KeyCol }} → zeige {{ .ValCol }}{{if .HasSuf}} • Suffix: {{ .Suffix }}{{end}}