轉(zhuǎn)自:Php導(dǎo)入導(dǎo)出csv文件時(shí)的生僻字處理和格式控制 - 班登 - 博客
應(yīng)用場(chǎng)景
Web應(yīng)用中導(dǎo)入導(dǎo)出報(bào)表是一個(gè)非常常見的需求,而普通用戶使用最多的表格文件都是用Excel保存的。
一般來(lái)說(shuō),對(duì)于數(shù)據(jù)量較小的導(dǎo)入導(dǎo)出,使用xls/xlsx文件和用戶交互是最佳選擇,即有相應(yīng)函數(shù)庫(kù)作為支撐,用戶的學(xué)習(xí)成本也是最低的。但是,對(duì)于數(shù)據(jù)量較大的業(yè)務(wù)來(lái)說(shuō)就不適用了。因?yàn)椋笈繑?shù)據(jù)在導(dǎo)入時(shí),函數(shù)庫(kù)會(huì)將表中所有數(shù)據(jù)載入到內(nèi)存中,由于xls/xlsx的文件本身在原始數(shù)據(jù)基礎(chǔ)上就包含了一些冗余信息(如格式),加上數(shù)據(jù)量本身的擴(kuò)充,導(dǎo)致內(nèi)存消耗會(huì)非常大,在php環(huán)境中使用phpExcel這個(gè)最熱門的庫(kù),讀取一個(gè)5M左右的xls數(shù)據(jù)就有可能導(dǎo)致512M的php可用內(nèi)容消耗殆盡。
因此,考慮到以下的應(yīng)用場(chǎng)景:
大數(shù)據(jù)量交互:導(dǎo)入數(shù)據(jù)行數(shù)在5-10萬(wàn),列數(shù)在10-100,文件大小5M-20M之間
用戶依賴:用戶只習(xí)慣用Excel處理數(shù)據(jù)
無(wú)需表格格式:無(wú)論導(dǎo)入導(dǎo)出,用戶均對(duì)數(shù)據(jù)表格格式?jīng)]有要求(邊框、底紋、對(duì)齊等),僅對(duì)數(shù)據(jù)格式有基本要求(數(shù)字、文本、浮點(diǎn)數(shù)等)
后端對(duì)xls/xlsx大文件的導(dǎo)入性能較差
在這種情況下,考慮csv文件作為數(shù)據(jù)導(dǎo)入導(dǎo)出的載體,是我認(rèn)為較為合適的一個(gè)選擇。相對(duì)于xls/xlsx,csv文件的優(yōu)勢(shì)如下:
體積小,上傳速度快
Mysql原生支持Load data infile from csv,可以快速dump數(shù)據(jù)
用戶學(xué)習(xí)成本不高,只需要告訴用戶保存文件時(shí)選擇csv格式即可
由于文件本身是純文本,可以在導(dǎo)入前對(duì)數(shù)據(jù)做驗(yàn)證和其他處理,既降低了內(nèi)存消耗,又提高了讀取速度
所以,最近一段時(shí)間系統(tǒng)都采用csv進(jìn)行數(shù)據(jù)導(dǎo)入導(dǎo)出。但是,最近發(fā)現(xiàn)一些導(dǎo)入導(dǎo)出時(shí)的問(wèn)題,主要是出在字符處理上。
問(wèn)題起因
最開始發(fā)現(xiàn)的問(wèn)題是這樣的:首先我在網(wǎng)站上導(dǎo)出一個(gè)數(shù)據(jù)表,其中有一列是“名單”,也就是一些姓名。然后,我再將這個(gè)表原樣導(dǎo)入,發(fā)現(xiàn)其中有些記錄中的姓名居然和我導(dǎo)出之前不一樣了。比如說(shuō),王玥變成了王h;還有堃這個(gè)字不見了等等。很快,我就發(fā)現(xiàn)這幾個(gè)問(wèn)題主要是出現(xiàn)在一些生僻字上,google一番之后得到了答案:字符集不正確。
問(wèn)題排查
最開始的搜索結(jié)果把我引入了數(shù)據(jù)導(dǎo)入階段的問(wèn)題排查環(huán)節(jié)。一種說(shuō)法是數(shù)據(jù)庫(kù)的字符集不正確,那么無(wú)論你的原始數(shù)據(jù)是怎樣的肯定都不會(huì)支持這些生僻字。我們知道,中文系統(tǒng)常見字符集有以下幾種:
utf-8:國(guó)標(biāo)字符集,目前應(yīng)用最廣泛,中文字庫(kù)也是較全的,不存在生僻字問(wèn)題。
gb2312:中文字符集,很多人都很熟,然而也是這次背鍋的主角,生僻字顯示錯(cuò)誤或者不顯示就是因?yàn)樽址撬?/p>
gbk/gb18030:還是中文字符集,對(duì)上面的生僻字也支持,因此不會(huì)出現(xiàn)問(wèn)題。
上面看似是三種字符集,其實(shí)可以分為兩類。utf-8和gbk系列的根本區(qū)別在于他們的編碼方式不同,即兩套完全不一樣的16進(jìn)制編碼表示雙字節(jié)字符(漢字);而gbk和gb2312基本相當(dāng)于超集和子集的關(guān)系,只是字庫(kù)大小有別,編碼方式是一樣的。
于是,我檢查了數(shù)據(jù)庫(kù)編碼,以及php的編碼,很顯然,都是utf-8,因此不存在不支持生僻字的問(wèn)題。那么問(wèn)題出在哪兒了呢?
回顧一下現(xiàn)有的業(yè)務(wù)邏輯,我的導(dǎo)出和導(dǎo)入流程基本是這樣的:
php讀取mysql數(shù)據(jù),循環(huán)寫入csv文件,在寫入時(shí)用iconv函數(shù)從utf-8轉(zhuǎn)換為gbk編碼;
打開文件,填入其他需要的數(shù)據(jù),保存;
導(dǎo)入數(shù)據(jù),其中l(wèi)oad data命令已經(jīng)設(shè)置字符集為gbk。
這個(gè)時(shí)候我就開始百思不得其解了:既然utf-8和gbk都不存在生僻字問(wèn)題,為什么我就是導(dǎo)入不成功呢?難道是我的轉(zhuǎn)換/導(dǎo)入命令有問(wèn)題?為了解決這個(gè)疑問(wèn),我覺(jué)得放棄gbk轉(zhuǎn)換,直接使用utf-8進(jìn)行全程數(shù)據(jù)交換。于是,重新導(dǎo)出數(shù)據(jù)后,我先用文本編輯器查看了一下,文件內(nèi)容正常!但是當(dāng)我用Excel打開之后……
明明是正常的文件,為什么Excel里變成亂碼了呢?
CSV與UTF-8,微軟
很顯然,Excel對(duì)于我的文件編碼識(shí)別是錯(cuò)誤的。第二次google一番之后了解了,csv本身只是純文本,微軟在打開csv文件時(shí),默認(rèn)都將文件的格式判定為Windows平臺(tái)的標(biāo)準(zhǔn)格式,即ANSI,所以自然識(shí)別不了utf-8的字符。那么怎樣才能識(shí)別呢?答案很簡(jiǎn)單,加BOM。
BOM簡(jiǎn)單的來(lái)說(shuō)就是一個(gè)文件頭,在文件頭部加入0xEF 0xBB 0xBF之后,utf-8就變成了帶BOM的文件,再次用Excel打開,果然正常顯示了。至此,是否問(wèn)題已經(jīng)解決了呢?數(shù)據(jù)庫(kù)支持生僻字,導(dǎo)出utf-8文件,加BOM后打開……并沒(méi)有。因?yàn)橹筮€需要做數(shù)據(jù)導(dǎo)入,由于額外的文件頭存在,導(dǎo)入時(shí)會(huì)直接報(bào)錯(cuò)(因?yàn)橐呀?jīng)不是原來(lái)的純文本了)。那么,很自然的想到在導(dǎo)入之前去除BOM頭。好了,這下導(dǎo)入也可以了。然而,現(xiàn)在又出現(xiàn)幾個(gè)新的問(wèn)題:
用戶在Excel中直接新建一個(gè)文件,保存為csv時(shí)的格式為ANSI,因此導(dǎo)入時(shí)需要判斷文件是否是utf-8,確定字符集
在已經(jīng)是utf-8時(shí),還需要判斷是否是帶BOM的,那么就需要讀取文件做一下檢測(cè),并去除BOM。
帶BOM文件僅僅是顯示正常,如果直接保存,文件中的列分隔符從默認(rèn)的逗號(hào)變成了空格,直接導(dǎo)致文件內(nèi)容變得完全不可控了。
因此,在當(dāng)前的技術(shù)方案下,需要考慮編碼格式、是否有BOM、分隔符判斷等……感覺(jué)就像是為了補(bǔ)鍋砸了一個(gè)更大的窟窿,需要花費(fèi)更多的精力去填。那么,這條路行不通的話,就回到上一節(jié)的問(wèn)題,如果還是將文件轉(zhuǎn)換為gbk,為何還會(huì)有生僻字的問(wèn)題呢?按照研究得到的資料,gbk應(yīng)該是完全支持這些字符的啊?
原來(lái)是Mac的鍋
回到最開始我們導(dǎo)出的那個(gè)文件。我將文件放到Windows平臺(tái)打開,發(fā)現(xiàn)里面顯示的字符是正確的!這時(shí)我才想起來(lái),是最近開始在Mac上做導(dǎo)入導(dǎo)出,才發(fā)現(xiàn)這個(gè)生僻字的問(wèn)題,難道是Mac系統(tǒng)的字符集有問(wèn)題?經(jīng)過(guò)第三次google之后,終于找到了本次問(wèn)題的最終答案:
Excel在打開一個(gè)csv文件時(shí),實(shí)際上使用了系統(tǒng)默認(rèn)的字符集和默認(rèn)分隔符進(jìn)行了一次數(shù)據(jù)導(dǎo)入,而過(guò)程對(duì)用戶是隱藏的。而Mac中的默認(rèn)字符集的中文字庫(kù)不支持生僻字。
我們可以做一個(gè)實(shí)驗(yàn):打開Excel,選擇導(dǎo)入數(shù)據(jù)(不是直接打開csv文件),選擇之前的文件,可以看到第一步選擇字符集的界面:
可以看到,默認(rèn)字符集是Mactonish,這是Mac的默認(rèn)字符集。這個(gè)字符集具體是什么,我沒(méi)有搜索到,但是我把字符集改為第二項(xiàng)Windows(ANSI)之后,生僻字立刻顯示正常了。隨后,我又使用簡(jiǎn)體中文的三個(gè)字符集(如上圖所示)進(jìn)行測(cè)試,發(fā)現(xiàn)Simplified Chinese(GB 2312)類型確實(shí)是不支持生僻字的,而Simplified Chinese(Mac OS)同樣如此,只有Simplified Chinese(Windows, DOS)格式可以支持。
所以,其實(shí)不是我的系統(tǒng)不支持生僻字,而是Mac中的Excel在打開csv文件時(shí)的文件導(dǎo)入過(guò)程改變了字符集,縮小了字庫(kù),導(dǎo)致我的生僻字直接丟掉了。既然這樣,問(wèn)題解決就變得比較簡(jiǎn)單了:對(duì)于Windows用戶,不用擔(dān)心本問(wèn)題的發(fā)生,因?yàn)槟J(rèn)字符集就支持生僻字;而對(duì)于Mac用戶,需要注意下載的數(shù)據(jù)表不要直接打開,選擇用導(dǎo)入數(shù)據(jù)的方式打開,并選擇正確的字符集即可。
生僻字問(wèn)題的總結(jié)
綜上,雖然我的問(wèn)題原因相對(duì)比較小眾,但問(wèn)題排查過(guò)程中對(duì)整個(gè)導(dǎo)入導(dǎo)出環(huán)節(jié)的字符集理解過(guò)程我覺(jué)得還是很有意義的。我的流程中其他環(huán)節(jié)其實(shí)已經(jīng)避免了生僻字的問(wèn)題,但其他開發(fā)者在做類似流程時(shí)可能會(huì)忽略或者錯(cuò)誤處理這些環(huán)節(jié)。因此,總結(jié)一下,為了避免導(dǎo)入導(dǎo)出時(shí)的生僻字問(wèn)題,需要注意以下幾點(diǎn):
檢查數(shù)據(jù)庫(kù)和后端文件的編碼,即數(shù)據(jù)來(lái)源的編碼,必須為utf-8或gbk等支持中文生僻字的編碼。
導(dǎo)出時(shí),如果為utf-8,則轉(zhuǎn)換成gbk編碼。
導(dǎo)入時(shí)設(shè)定字符集,load data infile加上charset關(guān)鍵字。
非Windows系統(tǒng)打開文件使用數(shù)據(jù)導(dǎo)入功能,而非直接打開,以便確定字符集,防止錯(cuò)誤導(dǎo)入。
至此,生僻字的問(wèn)題得到完整解決。
導(dǎo)出時(shí)的格式問(wèn)題
這次花了一些時(shí)間解決生僻字問(wèn)題后,我也順便處理了一些導(dǎo)出數(shù)據(jù)時(shí)的格式控制問(wèn)題。當(dāng)然,這些問(wèn)題都是直接能搜到解決方案的,我在這里只是整理歸納分享一下,希望對(duì)有同樣問(wèn)題的人能提供快速幫助。主要問(wèn)題如下:
科學(xué)計(jì)數(shù)法問(wèn)題:導(dǎo)出數(shù)據(jù)中存在超過(guò)12位的數(shù)字時(shí),在Excel中將以科學(xué)計(jì)數(shù)法的形式顯示,而這往往不是用戶需要的。
前導(dǎo)0問(wèn)題:導(dǎo)出數(shù)據(jù)中存在第一位為0的數(shù)字時(shí),Excel會(huì)忽略掉這個(gè)0,將它直接抹去。
逗號(hào)問(wèn)題:默認(rèn)分隔符是逗號(hào),假如原始數(shù)據(jù)里也有逗號(hào),則會(huì)導(dǎo)致列分隔出錯(cuò)。
為什么會(huì)存在這些問(wèn)題呢?逗號(hào)問(wèn)題的原因顯而易見,只要想辦法和作為分隔符的逗號(hào)區(qū)分開就可以了。關(guān)于科學(xué)計(jì)數(shù)法和前導(dǎo)0的問(wèn)題,原因是這樣的:csv是純文本文件,相對(duì)于xls來(lái)說(shuō)沒(méi)有數(shù)據(jù)類型的信息,所以Excel在打開csv時(shí)根據(jù)數(shù)據(jù)的內(nèi)容自動(dòng)進(jìn)行了類型判斷。對(duì)于一串?dāng)?shù)字,Excel認(rèn)為是數(shù)字類型,超出12位就會(huì)以科學(xué)計(jì)數(shù)法表示;而既然是純數(shù)字,很顯然沒(méi)有以0開頭的表示法,所以這個(gè)0也就被抹掉了。
解決問(wèn)題的方法也很簡(jiǎn)單:加引號(hào)。即將文本用=""包裹起來(lái),=在最前,會(huì)被Excel識(shí)別為一個(gè)函數(shù),而雙引號(hào)包裹表示一個(gè)字符串,那么Excel不會(huì)對(duì)字符串里的內(nèi)容做任何改動(dòng),于是就可以原樣輸出了。代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23此轉(zhuǎn)換函數(shù)可以判斷字符或數(shù)字形式的數(shù)字、浮點(diǎn)數(shù)、逗號(hào),將需要包裹的文本用=""包裹起來(lái),并對(duì)其中的逗號(hào)和已經(jīng)存在的雙引號(hào)做轉(zhuǎn)義處理。測(cè)試數(shù)據(jù)如下:
1轉(zhuǎn)換后的文本內(nèi)容如下:
1Excel中顯示正常。
轉(zhuǎn)自:Posted by 班登 Dec 6th, 2016 11:32 amphp