From 2ffe4e729e73435d77e868ca23e9a5293cabb51a Mon Sep 17 00:00:00 2001 From: s4kura <1761015783@qq.com> Date: Sun, 13 Sep 2026 16:48:59 +0800 Subject: [PATCH] fix: handle UTF-8 BOM in stock imports (#2333) --- docs/CHANGELOG.md | 1 + docs/full-guide.md | 1 + src/services/import_parser.py | 2 +- tests/test_import_parser.py | 5 +++++ 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index eef561fe2..23f7a934f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] - [测试] 修复股票名称解析冷启动超时并发测试的同步竞态:在放行后台抓取前确认两个等待者均已结束并返回空结果,避免 Docker 发布门禁偶发失败。 +- [修复] 智能导入兼容带 UTF-8 BOM 的 CSV 与剪贴板文本,避免 `code` 表头被误当成数据并丢失有效股票代码。 diff --git a/docs/full-guide.md b/docs/full-guide.md index b7b600f47..d9f2b1cd0 100644 --- a/docs/full-guide.md +++ b/docs/full-guide.md @@ -1472,6 +1472,7 @@ LITELLM_FALLBACK_MODELS=anthropic/claude-sonnet-4-6,openai/gpt-5.4-mini - **AkShare 名称解析缓存**:名称→代码解析使用 AkShare 在线 fallback 时,结果缓存 1 小时(TTL),避免频繁请求;首次调用或缓存过期后会自动刷新。 - **CSV/Excel 列名**:支持 `code`、`股票代码`、`代码`、`name`、`股票名称`、`名称` 等(不区分大小写);无表头时默认第 1 列为代码、第 2 列为名称。 +- **文本编码**:CSV 与剪贴板文本支持 UTF-8(含 BOM)和 GBK。 - **常见解析失败**:文件过大(>2MB)、编码非 UTF-8/GBK、Excel 工作表为空或损坏、CSV 分隔符/列数不一致时,API 会返回具体错误提示。 - **模型优先级**:`VISION_MODEL` > `LITELLM_MODEL` > 根据已有 API Key 推断(`OPENAI_VISION_MODEL` 已废弃,请改用 `VISION_MODEL`) diff --git a/src/services/import_parser.py b/src/services/import_parser.py index f4559f90f..da7ed95b2 100644 --- a/src/services/import_parser.py +++ b/src/services/import_parser.py @@ -179,7 +179,7 @@ def parse_import_from_bytes(data: bytes, filename: Optional[str] = None) -> List raise ValueError("仅支持 .xlsx 格式,请将 .xls 另存为 .xlsx 后重试") # CSV / text - for encoding in ("utf-8", "gbk"): + for encoding in ("utf-8-sig", "gbk"): try: text = data.decode(encoding) break diff --git a/tests/test_import_parser.py b/tests/test_import_parser.py index f26343dd2..48bc636ce 100644 --- a/tests/test_import_parser.py +++ b/tests/test_import_parser.py @@ -39,6 +39,11 @@ class TestParseImportFromBytesCsv: result = parse_import_from_bytes(data, "a.csv") assert result[0] == ("600519", "贵州茅台", "medium") + def test_parses_utf8_bom_csv_header(self): + data = "\ufeffcode,name\n600519,".encode("utf-8") + result = parse_import_from_bytes(data, "a.csv") + assert result == [("600519", None, "medium")] + def test_parses_csv_no_header(self): # Use 300750 instead of 00700 to avoid pandas stripping leading zeros data = "600519,贵州茅台\n300750,宁德时代".encode("utf-8")