12c8098b5f
leo 兩句定調:
①「要思考 Excel 和 csv 的問題,因為企業用很多」
②「企業的 excel 通常不會是 1 萬行,人工做不出這麼多,但你可以轉結構資料丟進去」
→ 修正我先前「CSV 會變數字牆、先不加」的判斷——那是拿機器產生的百萬列當前提。
實測 60列x6欄 ≈ 2,200 token,對 LLM 完全不是問題。
統一中間格式=Markdown(leo 提 n8n 對照後定調):
n8n 一切轉 JSON 是對的,因為它下游是程式(.欄位 取值);
我們下游是 LLM ⇒ Markdown 表格更好。實測同一份表格
JSON 6,872 字元 vs Markdown 3,341=JSON 多花 2.1 倍 token(每列重複寫欄位名)。
測試 8/8,用 openpyxl 產的真 Excel 檔(非自捏 XML):
⭐ 測試抓到兩個真 bug(讀原始碼看不出來):
① rels 的 Target 是絕對路徑 /xl/... 我卻又補 xl/ 前綴 → 變 xl/xl/...
② r:id 帶 namespace,寫 xml:"id,attr" 抓不到,要用完整 namespace URI
修好後分頁名從 ## sheet1 變成 ## 維修紀錄(企業分頁名本身就是語意)
CSV:去 BOM(Excel 另存必帶)/引號內逗號不拆欄/| 跳脫/欄數不齊不整份失敗
XLSX:多工作表全讀(只取第一頁會漏)/sharedStrings 索引/壞檔報錯
防呆:超過 500 列截斷並明說截斷了(不靜默丟資料);單格超 500 字截斷。
126 lines
4.0 KiB
Go
126 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const realXLSX = "/private/tmp/claude-501/-Users-youlinhsieh-Documents-tech-projects-InkStoneCo/92a75156-c295-4c79-bfeb-de1a20e4ed26/scratchpad/office-poc/real.xlsx"
|
|
|
|
func TestCSV_轉成Markdown表格(t *testing.T) {
|
|
csv := "工單號,船名,金額\nWO-1001,海運三號,\"350,000\"\nWO-1002,長榮七號,\"128,500\"\n"
|
|
got, err := ConvertToText("維修.csv", []byte(csv))
|
|
if err != nil {
|
|
t.Fatalf("不該出錯: %v", err)
|
|
}
|
|
// 標題行 + 分隔線 = Markdown 表格的辨識特徵
|
|
if !strings.Contains(got, "| 工單號 | 船名 | 金額 |") {
|
|
t.Errorf("應有標題行,實得:\n%s", got)
|
|
}
|
|
if !strings.Contains(got, "| --- |") {
|
|
t.Errorf("應有分隔線(否則 LLM 認不出是表格),實得:\n%s", got)
|
|
}
|
|
// 引號包住的逗號數字不可被拆成兩欄
|
|
if !strings.Contains(got, "350,000") {
|
|
t.Errorf("引號內的逗號不該被當欄位分隔,實得:\n%s", got)
|
|
}
|
|
t.Logf("CSV 轉出:\n%s", got)
|
|
}
|
|
|
|
func TestCSV_去BOM(t *testing.T) {
|
|
// Excel 另存 CSV 幾乎一定帶 BOM,不去掉第一個欄名會多出看不見的字元 → 標題對不上
|
|
withBOM := append([]byte{0xEF, 0xBB, 0xBF}, []byte("欄一,欄二\n值1,值2\n")...)
|
|
got, err := ConvertToText("a.csv", withBOM)
|
|
if err != nil {
|
|
t.Fatalf("不該出錯: %v", err)
|
|
}
|
|
if !strings.Contains(got, "| 欄一 |") {
|
|
t.Errorf("BOM 應被去掉,實得: %q", got[:min(60, len(got))])
|
|
}
|
|
}
|
|
|
|
func TestCSV_欄位含直線要跳脫(t *testing.T) {
|
|
// 儲存格內容有 | 會把 Markdown 表格欄位切錯位
|
|
got, err := ConvertToText("a.csv", []byte("名稱,備註\n產品A,\"甲|乙\"\n"))
|
|
if err != nil {
|
|
t.Fatalf("不該出錯: %v", err)
|
|
}
|
|
if !strings.Contains(got, `甲\|乙`) {
|
|
t.Errorf("儲存格內的 | 應被跳脫,實得:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestCSV_每列欄數不同不該整份失敗(t *testing.T) {
|
|
// 手工表格常見:尾列缺欄
|
|
got, err := ConvertToText("a.csv", []byte("A,B,C\n1,2,3\n4,5\n"))
|
|
if err != nil {
|
|
t.Fatalf("欄數不齊不該整份失敗: %v", err)
|
|
}
|
|
if !strings.Contains(got, "4") || !strings.Contains(got, "5") {
|
|
t.Errorf("缺欄的列仍應保留內容,實得:\n%s", got)
|
|
}
|
|
}
|
|
|
|
func TestXLSX_真Excel檔含多工作表(t *testing.T) {
|
|
data, err := os.ReadFile(realXLSX)
|
|
if err != nil {
|
|
t.Skip("無真 xlsx 測資,跳過")
|
|
}
|
|
got, err := ConvertToText("real.xlsx", data)
|
|
if err != nil {
|
|
t.Fatalf("真 Excel 檔應該讀得了: %v", err)
|
|
}
|
|
// 第一個工作表的內容
|
|
for _, want := range []string{"工單號", "海運三號", "主機大修", "350,000"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("缺少第一工作表的 %q,實得:\n%s", want, got)
|
|
}
|
|
}
|
|
// 第二個工作表——只取第一頁會漏資料,企業檔案常一檔多分頁
|
|
for _, want := range []string{"料號", "主軸承", "45,000"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("缺少第二工作表的 %q(多工作表沒被讀到),實得:\n%s", want, got)
|
|
}
|
|
}
|
|
t.Logf("真 Excel 轉出:\n%s", got)
|
|
}
|
|
|
|
func TestXLSX_壞檔要報錯不當機(t *testing.T) {
|
|
_, err := ConvertToText("a.xlsx", []byte("這不是 zip,是舊版 .xls 改名"))
|
|
if err == nil {
|
|
t.Error("壞檔應該報錯")
|
|
}
|
|
}
|
|
|
|
func TestTable_空表格回ErrNoText(t *testing.T) {
|
|
_, err := ConvertToText("empty.csv", []byte(""))
|
|
if err == nil {
|
|
t.Error("空 CSV 應回錯而不是空字串(否則會靜默送空卡給 LLM)")
|
|
}
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestXLSX_要用使用者取的分頁名而非sheet1(t *testing.T) {
|
|
data, err := os.ReadFile(realXLSX)
|
|
if err != nil {
|
|
t.Skip("無真 xlsx 測資")
|
|
}
|
|
got, _ := ConvertToText("real.xlsx", data)
|
|
// 企業分頁名本身就是語意(「維修紀錄」「報價單」),用 sheet1 會讓 LLM 少掉脈絡
|
|
for _, want := range []string{"## 維修紀錄", "## 報價單"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("應使用分頁名 %q,實得:\n%s", want, got)
|
|
}
|
|
}
|
|
if strings.Contains(got, "## sheet1") {
|
|
t.Errorf("不該退回檔名 sheet1,實得:\n%s", got)
|
|
}
|
|
}
|