package importer import ( "context" "encoding/csv" "errors" "io" "strconv" "strings" "github.com/google/uuid" "proxyrotator/internal/model" ) // Importer 代理导入器 type Importer struct{} // NewImporter 创建导入器 func NewImporter() *Importer { return &Importer{} } // ParseText 解析文本格式的代理列表 func (im *Importer) ParseText(ctx context.Context, in model.ImportInput, text string) ([]model.Proxy, []model.InvalidLine) { lines := strings.Split(text, "\n") var proxies []model.Proxy var invalid []model.InvalidLine seen := make(map[string]bool) for _, raw := range lines { raw = strings.TrimSpace(raw) if raw == "" || strings.HasPrefix(raw, "#") { continue } p, err := ParseProxyLine(raw, in.ProtocolHint) if err != nil { invalid = append(invalid, model.InvalidLine{ Raw: raw, Reason: err.Error(), }) continue } // 设置默认值 p.ID = uuid.New().String() p.Group = coalesce(p.Group, in.Group, "default") p.Tags = mergeTags(p.Tags, in.Tags) p.Status = model.StatusUnknown // 内存去重 key := dedupKey(p) if seen[key] { continue } seen[key] = true proxies = append(proxies, *p) } return proxies, invalid } // ParseCSV 解析 CSV 格式的代理列表 // 期望列: protocol,host,port,username,password,group,tags func (im *Importer) ParseCSV(ctx context.Context, in model.ImportInput, r io.Reader) ([]model.Proxy, []model.InvalidLine) { reader := csv.NewReader(r) reader.FieldsPerRecord = -1 // 允许不定列数 reader.TrimLeadingSpace = true var proxies []model.Proxy var invalid []model.InvalidLine seen := make(map[string]bool) // 读取表头 header, err := reader.Read() if err != nil { return nil, []model.InvalidLine{{Raw: "", Reason: "failed to read CSV header"}} } // 解析列索引 colIdx := parseHeader(header) lineNum := 1 for { record, err := reader.Read() if err == io.EOF { break } lineNum++ if err != nil { invalid = append(invalid, model.InvalidLine{ Raw: strings.Join(record, ","), Reason: err.Error(), }) continue } p, err := parseCSVRecord(record, colIdx, in) if err != nil { invalid = append(invalid, model.InvalidLine{ Raw: strings.Join(record, ","), Reason: err.Error(), }) continue } // 内存去重 key := dedupKey(p) if seen[key] { continue } seen[key] = true proxies = append(proxies, *p) } return proxies, invalid } // columnIndex CSV 列索引 type columnIndex struct { protocol int host int port int username int password int group int tags int } // parseHeader 解析 CSV 表头 func parseHeader(header []string) columnIndex { idx := columnIndex{ protocol: -1, host: -1, port: -1, username: -1, password: -1, group: -1, tags: -1, } for i, col := range header { switch strings.ToLower(strings.TrimSpace(col)) { case "protocol": idx.protocol = i case "host": idx.host = i case "port": idx.port = i case "username", "user": idx.username = i case "password", "pass": idx.password = i case "group": idx.group = i case "tags": idx.tags = i } } return idx } // parseCSVRecord 解析 CSV 记录 func parseCSVRecord(record []string, idx columnIndex, in model.ImportInput) (*model.Proxy, error) { get := func(i int) string { if i >= 0 && i < len(record) { return strings.TrimSpace(record[i]) } return "" } // 如果没有表头,尝试按位置解析 if idx.host == -1 && len(record) >= 2 { // 假设格式: host,port 或 host,port,username,password line := strings.Join(record, ":") if len(record) >= 4 { line = record[2] + ":" + record[3] + "@" + record[0] + ":" + record[1] } else { line = record[0] + ":" + record[1] } p, err := ParseProxyLine(line, in.ProtocolHint) if err != nil { return nil, err } p.ID = uuid.New().String() p.Group = coalesce(in.Group, "default") p.Tags = in.Tags p.Status = model.StatusUnknown return p, nil } // 根据表头解析 protocol := get(idx.protocol) host := get(idx.host) portStr := get(idx.port) if host == "" { return nil, errors.New("missing host") } var p model.Proxy p.ID = uuid.New().String() p.Host = host p.Status = model.StatusUnknown // 解析协议 switch strings.ToLower(protocol) { case "http": p.Protocol = model.ProtoHTTP case "https": p.Protocol = model.ProtoHTTPS case "socks5": p.Protocol = model.ProtoSOCKS5 default: p.Protocol = model.ProtoHTTP } // 解析端口 if portStr != "" { port := 0 for _, c := range portStr { if c >= '0' && c <= '9' { port = port*10 + int(c-'0') } } if port > 0 && port < 65536 { p.Port = port } else { p.Port = 80 } } else { p.Port = 80 } p.Username = get(idx.username) p.Password = get(idx.password) p.Group = coalesce(get(idx.group), in.Group, "default") // 解析 tags tagsStr := get(idx.tags) if tagsStr != "" { p.Tags = strings.Split(tagsStr, ";") } p.Tags = mergeTags(p.Tags, in.Tags) return &p, nil } // dedupKey 生成去重键 func dedupKey(p *model.Proxy) string { return string(p.Protocol) + ":" + p.Host + ":" + strconv.Itoa(p.Port) + ":" + p.Username } // coalesce 返回第一个非空字符串 func coalesce(values ...string) string { for _, v := range values { if v != "" { return v } } return "" } // mergeTags 合并去重 tags func mergeTags(a, b []string) []string { seen := make(map[string]bool) var result []string for _, t := range a { t = strings.TrimSpace(t) if t != "" && !seen[t] { seen[t] = true result = append(result, t) } } for _, t := range b { t = strings.TrimSpace(t) if t != "" && !seen[t] { seen[t] = true result = append(result, t) } } return result }