(免責聲明:本文根據真實經歷改編,時間線跨度比較大,有些問題的時效性有待驗證)前端
CSV——字符分隔值文件格式。是在數據類應用中很是常見的文件格式,有着以下諸多好處:web
說完了CSV種種好處,下面,我和CSV(Excel)的羈絆,拉開了序幕~typescript
靠天吃飯!行不行我不肯定,但應該能行!windows
(牛逼哄哄的新手團,第一集就團滅了)瀏覽器
所謂前端導出,無外乎就是這樣的代碼。由於「看似」太簡單,初版實現時天然也就沒借助「開源世界」的力量:bash
const lines: string[][] = [ //... ];
const content: string[] = [];
// 將每一行數據插入
for (const line of lines) {
content.push(line.join(','));
}
// 模擬a標籤點擊
const a = document.createElement('a');
// 文本內容增長換行
const blob = new Blob([content.join('\n')], {
type: 'text/csv',
});
a.download = title;
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href);
複製代碼
開發:「測試了一下,效果正確。」函數
產品:「開發完了,趕忙上線!」佈局
...(上線一天後)...post
用戶A:「怎麼回事?導出的文件都亂碼了?」學習
在需求開發過程當中,爲了快速上線,每每只是爲了知足眼前的效果,沒有從根源上去了解技術背後的完整做用機理(例如,開發的同時,真的去了解了CSV的相關規範等知識嗎?)。 僅僅根據幾個隨手模擬的case進行測試,運氣好一點,可能實現了一部分用戶場景。但剩餘的、未覆蓋的場景,隨着使用者的增多和深刻,無一例外的會打臉開發者。
初版代碼,由於沒有照顧到中文,或者說,沒有照顧到Excel是如何識別文件編碼的。
在學習了字符編解碼的知識後,須要在生成的文件頭部,識趣的加上utf-8的BOM,現象就都正確了。
// 修復中文亂碼
const blob = new Blob([new Uint8Array([0xef, 0xbb, 0xbf]), content.join('\n')], {
type: 'text/csv',
});
複製代碼
出現了套路~我打過補丁的地方,必定行!
用戶B:「咦?我下載的怎麼是這個?」
沒多久,有的用戶又出現了下載的文件無後綴的問題(一個Chrome Bug),致使系統沒法默認使用Excel打開;此外,也發現過Chrome某些版本上會出現:若是文件名爲空時,下載的文件後綴會變成zip。
修復方式就是,在download這個attribute上寫上完整後綴,以及確保文件名存在。
// 修復文件後綴不正確
a.download = `${title || '未命名'}.csv`;
複製代碼
用戶C:「下載的表格佈局錯亂了!」
排查用戶的CSV發現,這是由於:前端生成的CSV默認使用了,
分隔符,而當用戶數據的某一列文本里也存在,
時,會致使最終Excel展現時,分列錯誤。
諸如:
產品,銷量
桌子,100
容器,箱子,500
複製代碼
它的展現效果是:
產品 | 銷量 | |
---|---|---|
桌子 | 100 | |
容器 | 箱子 | 500 |
修復方式也不復雜,就是在每一格數據外,包一層雙引號,這樣就能夠正確的展現分列。
// 修復數據中包含","致使的錯列
content.push(line.map(v => `"${v}"`).join(','));
複製代碼
...(補丁3號發佈後,不到1小時)...
用戶D:「導出的文件內容怪怪的,我昨天用的時候仍是好的呀」
開發:「這。。。」
上一個補丁纔剛兼容了文本中包含,
的狀況,結果卻引發了另外一個場景的bug。由於上個補丁引入了"
來包裹文本內容,但若是文本中同時存在"
,那就會出現另外一個場景中的錯列現象。
諸如:
產品,銷量
"顯示器15"","200" "顯示器17"","100"
複製代碼
它的展現效果是:
產品 | 銷量 |
---|---|
顯示器15",200" | |
顯示器17",100" |
修復方式仍是很簡單,就是須要特地對引號進行轉義,"
須要變爲""
。
// 修復數據中包含"致使的錯列
content.push(line.map(v => `"${v.replace(/"/g, '""')}"`).join(','));
複製代碼
這一時期,持續不斷的線上bug修復確實酸爽的不行,堵一個,又漏了另外一個,補丁打的不亦樂乎,此起彼伏。其中,重點回顧兩次錯列問題,其實就是由於沒有認真瞭解CSV規範所致。仔細閱讀規範能夠發現,wiki已經很是清晰地說明了這幾種場景,以及對應的處理方式。
值得慶幸的是,通過這個時期的重重補丁轟炸。如今生成的CSV,已是100%符合標準的規範產物了。
左右開弓,逐步精細~新的風暴已經出現~
生成的產物確實是規範了,但它的「運行環境」卻沒那麼規範。Excel爲了處理CSV中的種種邊界問題,偷偷夾雜了點私貨(私有規範)。
可是,這些私有規範並非萬能的,即便遵照它們,有時候又不能完美解決全部問題。此外,Excel做爲一個閉源軟件,一旦它出現不符合預期的問題,排查起來,難於上青天。
這個後門在早期版本中,已經體會過了。若是究其緣由,推測是Excel爲了快速識別文件的編碼格式,而偷偷加了個規範。這裏就不贅述了。
有一個問題,其實從初版功能中就一直存在,只是一直沒有花精力解決。問題:代碼中使用的列分隔符是,
,可是用戶的Excel配置並不必定將,
視爲分隔符。不少時候,用戶打開CSV看到的並非分好的一列列數據,而是未分割的一整行。
從產品角度出發,初期能夠經過文檔指引,讓用戶經過Excel的功能「數據tab -> 分列」來自行實現數據分割。不過,做爲注重用戶體驗的開發人員,仍是但願可以達到用戶無感知的,自動化分列效果。
這時候,就接觸到了Excel的第二個後門——Excel metadata。CSV的頭部能夠增長諸如這樣的信息:sep={實際使用的分隔符}
,來顯式聲明分隔符。
諸如:
sep=-
key-value
a-1
b-2
複製代碼
即便是很是特殊的分隔符,Excel也能正確識別並完成分列,並自動忽略meta內容不作展現:
key | value |
---|---|
a | 1 |
b | 2 |
看上去,彷佛是個很美好的後門~
然而,後門和天坑每每相輔相成。使用黑科技的時候,每每只專一了眼前的部分,部分場景確實得到便利的同時,另外一部分場景,可能再也沒法支持了~
若是使用了Meta,那不管在文件頭部,仍是在Meta結束位置插入BOM,都會通通失效,Excel沒法再經過BOM識別文件編碼了,也就是,亂碼問題又回來了。
但機智的開發是不會那麼容易屈服的,既然不能插入BOM,那整個文件都使用locale編碼方式便可,只須要在生成文件的過程當中對內容進行一次編碼(轉碼)便可。
中文環境下,使用gb18030編碼方式:
// 須要引入依賴庫,上文說起的字符編解碼文章中說起過
import { TextEncoder } from 'text-encoding';
// 將字符串content進行編碼
const buf = new TextEncoder('gb18030', {
NONSTANDARD_allowLegacyEncoding: true,
}).encode(`sep=\t\r\n${content}`);
// 後續邏輯照舊
const blob = new Blob([buf], { type: 'text/csv' });
複製代碼
隨着業務拓展,上面的方案持續時間不久,就又碰到了不兼容場景。
一位俄羅斯大漢客戶:「……&%##@!***……」(大意是,他下載的文件俄文亂碼了。此外,他還好心的告知:在俄羅斯,本地編碼通常爲windows-1251。最後,他還建議使用Unicode字符集)
開發:「Спасибо」(謝謝)
真的是太難了。。。環環相掛,躲一個坑,就會進另外一個坑,這一路摸爬滾打,已經摔得體無完膚。
我嘗試搜索瞭如何經過瀏覽器獲取本地編碼的方式,但沒有找到解法(確實這二者沒有必然聯繫)。爲了儘量的保留Meta能力(由於它確實有價值),最終只能經過判斷語言環境再打了個補丁。但這畢竟不是長久之計,哪天業務發展到其餘國家,勢必又是一坨亂碼。
編碼的問題雖然一直如鯁在喉,但在沒有新解法以前,只能暫且擱置。由於,Excel這個大Boss還有許多其餘難關在等着開發者~
電商相關的行爲數據中,每每存在這麼一列——訂單ID,該數據列自己是文本,生成的文件內容以下:
sep=,
order_id,order_amt
"201901010000123123","100"
複製代碼
用Excel打開時,第一列會被識別爲數值,最終展現結果是20190101000012300,丟失了精度。
究其緣由,是由於Excel的數值類型用的是遵循IEEE 754規範的雙精度浮點數(同js的Number,有關知識能夠參閱數據精度文章),這裏的有效位數只能達到15位,剩餘的數據沒法存儲,致使精度丟失(大數問題)。
其實,這個問題在平常運營中也會碰到,還算是個常見問題。用戶給出的意見是:在這些超長的數值文本前,加上一些特殊符號,以免Excel將該文本識別爲數值類型。
不過此時,我不由在想,既然都有私有Meta了,爲啥不擴充點能力,容許指定列類型呢?
這個問題之坑,真的讓人印象深入。
用戶Z:「爲何我沒法對這列數據進行求和?結果都是#VALUE!?」
開發:「這???」
我一直都沒太在乎這個問題,一開始覺得:那列數據中存在了異常字符而已。直到我親眼看到了這個現象,確實是很是詭異。正常來講,Excel會將識別爲數值類型的列進行右對齊,文本類型的列進行左對齊。用戶的那一列經仔細確認,確實都是數值,但Excel卻沒有將該列識別爲數值類型,甚至,它也不是文本類型,由於其餘轉型函數均會報類型錯誤。
這個問題我跟蹤了2~3周,用了各類方法,也對比了不少Excel版本的行爲,一次機緣巧合下,我發現這個問題只會出如今英文環境下的Mac Office 16,若是系統語言切換爲中文,Excel也能正確識別。
從最先發現問題的Mac Office 16.15,到後面升級成最新的16.16.8,這問題都在。測試bug的方式很簡單:Excel有一個轉型函數「VALUE」,該函數的官方示例是=VALUE("$1,000")
,結果應該是數值型的1000,而在這些存在問題的Excel版本(英文環境)下,會顯示類型錯誤(#VALUE!)。
因而,我向Office官方反饋了這個現象,遺憾的是,技術人員沒法在他們本地復現這個問題。由於用戶的數據是真實業務數據,我不方便轉發那份彷佛「有問題」的CSV。再加上溝通效率過低,該問題不了了之~
(近日,在最新的16.16.15版本中測試,該問題已經修復~)
黎明的曙光?彷佛都和諧了~
雖然整個技術方案已經穩定運行了好幾個月。但心中總有那麼個心結:若是業務拓展到其餘語言環境,那怎麼辦?開發不可能枚舉全部的語言對編碼的映射;並且,如今的方案也沒法兼顧中文環境下,用戶看其餘語言文本的場景。
這段期間,斷斷續續也在關注着相關問題,直到,我看到了這位大仙多年前的瘋狂測試:做者嘗試了各類編碼+BOM+分隔符的組合,而後,測試生成的CSV在Excel 2003和Excel 2007下的展現狀況。
功夫不負有心人,在這十幾種組合的測試下,他發現了,只有UTF16LE + BOM + \t的組合,可以完美地實現:
真的是給大神跪下了,依據這條價值連城的信息,核心代碼最終變成了:
/** 產生符合規範的CSV單元格內容 */
function getCSVString(v: string): string;
/** 將字符串轉爲utf16le編碼 */
function encodeUtf16le(s: string): Uint8Array;
// 1. 生成規範的CSV
for (const line of lines) {
content.push(line.map(getCSVString).join('\t'));
}
// 2. 將文本內容轉爲utf16le編碼
const buf = encodeUtf16le(content.join('\n'));
// 3. 在文件頭部增長utf16le的BOM
new Blob([new Uint8Array([0xff, 0xfe]), buf], { type: 'text/csv' });
複製代碼