package mainimport ("encoding/csv""flag""fmt""io""log""net/http""os""strconv""strings""sync""time""golang.org/x/text/encoding/simplifiedchinese"
)// StockData 股票数据结构
type StockData struct {Code string // 股票代码Name string // 股票名称Price float64 // 当前价Change float64 // 涨跌额ChangePercent float64 // 涨跌幅Volume int64 // 成交量Amount float64 // 成交额High float64 // 最高价Low float64 // 最低价Open float64 // 开盘价PreClose float64 // 前收盘价
}// 获取股票代码列表
func getStockCodes() ([]string, error) {// 模拟股票代码列表var codes []string// 添加一些上海市场的股票代码for i := 0; i < 10000; i++ {code := fmt.Sprintf("sh%06d", 600000+i)codes = append(codes, code)}// 添加一些深圳市场的股票代码for i := 0; i < 999; i++ {code := fmt.Sprintf("sz%06d", 1+i)codes = append(codes, code)}return codes, nil
}// 获取单个股票数据
func fetchStockData(code string) (*StockData, error) {url := fmt.Sprintf("http://qt.gtimg.cn/q=%s", code)// 创建HTTP客户端client := &http.Client{Timeout: 5 * time.Second,}req, err := http.NewRequest("GET", url, nil)if err != nil {return nil, err}// 设置适当的请求头req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")req.Header.Set("Accept", "*/*")resp, err := client.Do(req)if err != nil {return nil, err}defer resp.Body.Close()// 读取响应体body, err := io.ReadAll(resp.Body)if err != nil {return nil, err}// 将GB2312/GBK编码的响应内容转换为UTF-8var content stringdataUTF8, err := simplifiedchinese.GBK.NewDecoder().Bytes(body)if err != nil {// 如果转换失败,使用原始内容content = string(body)} else {content = string(dataUTF8)}// 检查数据有效性if !strings.Contains(content, "~") {return nil, fmt.Errorf("数据格式异常")}// 提取数据部分 - 优化提取逻辑start := strings.Index(content, "=\"")end := strings.LastIndex(content, "\"")if start == -1 || end == -1 {return nil, fmt.Errorf("无法提取有效数据")}// 提取数据部分data := content[start+2 : end]return parseStockData(code, data)
}// 解析股票数据
func parseStockData(code, data string) (*StockData, error) {fields := strings.Split(data, "~")if len(fields) < 40 {return nil, fmt.Errorf("数据字段不足")}stockName := fields[1]stock := &StockData{Code: code,Name: stockName,}// 解析数值字段if price, err := strconv.ParseFloat(fields[3], 64); err == nil {stock.Price = price}if change, err := strconv.ParseFloat(fields[4], 64); err == nil {stock.Change = change}if changePercent, err := strconv.ParseFloat(strings.TrimSuffix(fields[5], "%"), 64); err == nil {stock.ChangePercent = changePercent}if volume, err := strconv.ParseInt(fields[6], 10, 64); err == nil {stock.Volume = volume}if amount, err := strconv.ParseFloat(fields[7], 64); err == nil {stock.Amount = amount}if high, err := strconv.ParseFloat(fields[33], 64); err == nil {stock.High = high}if low, err := strconv.ParseFloat(fields[34], 64); err == nil {stock.Low = low}if open, err := strconv.ParseFloat(fields[35], 64); err == nil {stock.Open = open}if preClose, err := strconv.ParseFloat(fields[36], 64); err == nil {stock.PreClose = preClose}return stock, nil
}// 处理股票数据(带重试)
func processStockData(code string, maxRetries int, retryDelay time.Duration) (*StockData, error) {for i := 0; i < maxRetries; i++ {// 尝试从API获取数据stock, err := fetchStockData(code)if err == nil && stock != nil && stock.Name != "" && len(stock.Name) > 1 {return stock, nil}if i < maxRetries-1 {time.Sleep(retryDelay)}}return nil, fmt.Errorf("获取股票 %s 数据失败", code)
}// 保存数据到CSV文件
func saveToCSV(stocks []*StockData, filename string) error {file, err := os.Create(filename)if err != nil {return err}defer file.Close()// 写入UTF-8 BOM,确保Excel能正确识别编码_, err = file.WriteString("\xEF\xBB\xBF")if err != nil {return err}// 创建CSV写入器writer := csv.NewWriter(file)writer.Comma = ','writer.UseCRLF = true// 写入表头headers := []string{"代码", "名称", "当前价", "涨跌额", "涨跌幅", "成交量", "成交额", "最高价", "最低价", "开盘价", "前收盘价"}if err := writer.Write(headers); err != nil {return err}// 写入数据行for _, stock := range stocks {row := []string{stock.Code,stock.Name, // 已修复的股票名称fmt.Sprintf("%.2f", stock.Price),fmt.Sprintf("%.2f", stock.Change),fmt.Sprintf("%.2f", stock.ChangePercent),fmt.Sprintf("%d", stock.Volume),fmt.Sprintf("%.2f", stock.Amount),fmt.Sprintf("%.2f", stock.High),fmt.Sprintf("%.2f", stock.Low),fmt.Sprintf("%.2f", stock.Open),fmt.Sprintf("%.2f", stock.PreClose),}if err := writer.Write(row); err != nil {return err}}writer.Flush()return writer.Error()
}// 显示股票数据
func displayStocks(stocks []*StockData) {fmt.Println("\n=== 股票数据统计 ===")fmt.Printf("获取到 %d 只股票数据\n\n", len(stocks))// 优化列宽,确保中文显示正常fmt.Printf("%-10s %-12s %-10s %-10s %-8s %-10s\n","代码", "名称", "当前价", "涨跌额", "涨跌幅", "成交额")fmt.Println("----------------------------------------------------------------------")// 只显示前5只股票limit := 5if len(stocks) < limit {limit = len(stocks)}for i := 0; i < limit; i++ {stock := stocks[i]// 确保股票名称不会溢出列宽displayName := stock.Nameif len([]rune(displayName)) > 6 {displayName = string([]rune(displayName)[:6]) + "..."}// 修复涨跌幅显示格式,确保百分号直接跟在数字后面,没有空格fmt.Printf("%-10s %-12s %-10.2f %-10.2f %5.2f %-10.2f\n",stock.Code, displayName, stock.Price, stock.Change, stock.ChangePercent, stock.Amount)}fmt.Println()
}func main() {// 定义命令行参数maxCount := flag.Int("n", 10, "获取股票数量")concurrency := flag.Int("c", 5, "并发数")flag.Parse()// 参数验证if *concurrency <= 0 || *concurrency > 20 {*concurrency = 5}if *maxCount <= 0 || *maxCount > 100 {*maxCount = 10}fmt.Println("开始获取A股数据...")startTime := time.Now()// 获取股票代码codes, err := getStockCodes()if err != nil {log.Fatalf("获取股票代码失败: %v", err)}fmt.Printf("过滤后共有 %d 个股票代码\n", len(codes))fmt.Printf("限制获取 %d 只股票数据\n", *maxCount)// 限制数量if len(codes) > *maxCount {codes = codes[:*maxCount]}fmt.Printf("开始批量获取数据(并发数: %d)...\n", *concurrency)// 并发获取数据resultChan := make(chan *StockData, len(codes))errChan := make(chan error, len(codes))var wg sync.WaitGroupsemaphore := make(chan struct{}, *concurrency)for _, code := range codes {wg.Add(1)semaphore <- struct{}{}go func(code string) {defer wg.Done()defer func() { <-semaphore }()stock, err := processStockData(code, 3, 300*time.Millisecond)if err != nil {errChan <- fmt.Errorf("股票 %s: %v", code, err)return}resultChan <- stock}(code)}// 等待所有协程完成go func() {wg.Wait()close(resultChan)close(errChan)}()// 收集数据var validStocks []*StockDatafor stock := range resultChan {validStocks = append(validStocks, stock)}// 输出错误for err := range errChan {log.Printf("错误: %v", err)}duration := time.Since(startTime)fmt.Printf("数据获取完成,耗时: %v\n", duration)// 显示数据displayStocks(validStocks)// 保存到CSV文件filename := fmt.Sprintf("stock_data_%s.csv", time.Now().Format("20060102_150405"))if err := saveToCSV(validStocks, filename); err != nil {log.Fatalf("保存CSV文件失败: %v", err)}fmt.Printf("数据已保存到: %s\n", filename)fmt.Println("提示:CSV文件已使用UTF-8编码(带BOM)保存,可以直接在Excel中打开查看中文内容。")
}