1. 程式人生 > >【玩轉Excel】Oracle PLSQL處理生成XLSX檔案

【玩轉Excel】Oracle PLSQL處理生成XLSX檔案

INTRODUCTION介紹
    之前發表了一個研究心得(當然是站在別人的肩膀上的),在Oracle中直接用PL/SQL解析並讀取Excel的內容。很多人都感興趣,按照我的寫法也可以成功實現了。
不過,有很多朋友提出了另外一個要求:讀取Excel是可以了,那是否可以在Oracle資料庫端直接生成Excel檔案?二進位制的檔案。
    開始我對這個問題是不怎麼在意的。因為,在Oracle資料庫中完全可以直接生成逗號或者Tab製表符的分隔符的文字(例如csv),然後用Excel開啟,那是可以完全達到目的(之前我一直就是這樣子做,特別是導資料給User的時候)。----後來,由於這邊老總要接收Excel資料的郵件,然後又必須要求用手機也可以正常檢視資料,所以也只好研究這個產生Excel二進位制檔案的功能了。

實際上,這個辦法也挺好,開發也簡單。只不過,文字的檔案相對於二進位制的檔案,有其優點,也有其缺點。請容我按照我的理解慢慢分析。
2.1        文字檔案的優點
主要的優點,當然是開發簡單,只需要將SQL的資料用遊標獲取,然後用逗號或者tab符號作為列分隔符,再用UTL_FILE將輸出結果到伺服器,產生文字檔案即可。
怎麼開發的我就不多說了,隨便百度一下,資料都一大堆。

2.2        文字檔案的缺點
檔案檔案的缺點,對應的就是Excel二進位制檔案的優點。
1)文字檔案用Excel開啟帶來的問題
雖然它開發簡單,但是由於資料是用逗號分隔符或者Tab製表符作為欄位的列分隔原則,導致了其資料解析的時候,非常容易出錯!假設資料裡面本來就有Tab符號,那麼如果用Excel開啟的時候,資料都亂了(因為多了一列)。如果是產生Excel檔案,就肯定不會有這個額外的問題!

由於文字自動轉換到Excel的特性導致,某些欄位,Excel會自作聰明地進行型別的轉換!不信,你將4-1作為一個文字內容,然後用Excel開啟看看?肯定自動變為日期型別2015/4/1了。
2)產生的檔案容量的問題
通過測試得知,當輸出的內容越多,用文字儲存所佔的容量就越大。而用Excel輸出的檔案就越小(相對於文字來說)。這說明,二進位制的檔案,應該是對資料有一些壓縮的演算法,所以其處理起來更加有效率。(附上實際的測試:162605行資料,文字:98MB,Excel:32.38MB)。
3)使用者的操作體驗
更加方便使用者操作。如果是產生一個excel檔案,當用戶開啟檔案的時候,不會有那個煩人的自動轉換內容的提示;另外,使用者也不需要另存為這個Excel了,因為這個本來就是Excel檔案,修改之後直接儲存即可。細節決定成敗,越細小的地方,只要是可以方便使用者工作的,在能力範圍之內,應該要做得更好的!

另外,現在手機或者平板電腦的使用者越來越多了。如果ERP系統匯出來的資料是一個文字(假設是發郵件給使用者),然後使用者拿手機(或者平板)開啟的時候,格式會很亂(就是一個逗號分隔符的文字內容,不清楚的使用者還以為是亂碼!)。
但是,如果系統匯出來的是一個二進位制的Excel檔案,使用者在手機端用Excel讀取的app看資料的時候,就非常方便了。格式也可以控制得很好。

2.3        PLSQL產生Excel檔案的缺點
優點我不說了。上面提到的文字檔案的缺點就是它的優點!我直接說Excel檔案的缺點,準備使用這個功能的可能要三思,先考慮清楚。
最特出的缺點:產生Xlsx檔案的處理過程,耗記憶體!
這個程式的大概邏輯是,先將資料存到Session的Table變數,然後整理成一個Excel型別的xml檔案(用clob變數儲存),最後經過壓縮,產生一個二進位制的Xlsx檔案。而這些過程,都是在記憶體中完成的,所以,當處理的資料量越大,處理起來就越慢。備註:所產生的結果xlsx文件是一個blob檔案,可以存到資料庫檔案系統裡面。

3        對國外的XLSX生成的指令碼優化
首先必須得說明,這個處理產生Xlsx檔案的PLSQL指令碼,並不是我寫的。我是在國外的一個blob中找到的程式碼(Copyright (C) 2011, 2012 by Anton Scheffer)。但是,我拿來使用之後,發現有一些地方必須得修改的,特別是執行效率方面,必須要優化(幾千行的資料產生XLSX居然要幾分鐘,這個是無法忍受的!)。同時也參考了國外blob的一些寫法做了一些調優,再按照我的習慣,增加一些小功能,得出現在的最終版本。
下面都有提及到。
3.1        匯出Excel的指令碼的優化點
3.1.1        最主要的優化是,處理EXCEL檔案生成的速度大大提高
下面的表格是我實際測試的結果:
生成xlsx的資料量        優化前執行時間        優化後執行時間
2948行資料        2:18        1秒
16500行資料        1:06:52        15秒
目前,這個功能我這邊已經在正式環境中使用,使用者的反饋效果還是很好的。幾千行的 資料的匯出,只需要1秒,這個等待時間還是非常不錯的!
3.1.2        動態SQL支援繫結變數的輸入
繫結變數的作用是什麼大家都清楚。對於查詢量超大的線上事務處理系統來說,這個優化是必須的。
引數        值說明        例子        實際使用語句
COL_ID        繫結變數的ID        1        WHERE COL = :1
COL_VALUE        值        106        106
具體看程式碼:query2sheet的輸入引數:p_col_value_tab
3.1.3        增加了一個過程CURSOR2SHEET
增加了一個過程cursor2sheet(參考別人分享的)。該過程的作用是將遊標的結果輸出到Excel,也是很實用的東西。不過目前沒測試過~暫時沒用到。
3.1.4        程式增加簡單的除錯模式
除錯模式的開關在包頭變數裡面。所以,要除錯的時候,只需要在PLSQL裡面增加這個程式碼:
SMT_XLSX_MAKER_PKG.G_DEBUG_MODE := 'TRUE';
除錯方式G_DEBUG_TYPE也可以改:DBMS_OUTPUT直接輸出(預設)/FILE_OUTPUT文件輸出/REQUEST_OUTPUT請求日誌輸出/CONTEXT_OUTPUT 將日誌改為上下文輸出
具體看程式碼。除錯的內容也主要是執行的時間。簡單的除錯。
備註:如果系統用的不是EBS,由於我這邊有另外的客戶化開發處理生成Log的文件,所以,這個我先remark掉了。用output做除錯也是很簡單的。
3.1.5        產生EXCEL檔案的日期系統改為預設是1900日期系統
也是一個非常重要的改動:產生Excel檔案的日期系統改為預設是1900日期系統!因為Windows系統的Excel的預設日期系統就是這個1900。
(如果不知道什麼是Excel的日期系統的兄臺請看這個連結http://blog.sina.com.cn/s/blog_3fc594a201016p89.html)

另外說一下,因為這個問題,在這個功能剛上線使用的時候,使用者還反饋了一個問題:為什麼我這裡的日期是2015年,複製到別的Excel檔案,就自動變為2011年?神奇的變化!
所以,這裡也說明了一點:國外的程式碼,如果用在國內的系統,或多或少都有一些是需要改動的。就算這並不是程式本身的bug!
-------------------------------------------------------------------
----------華麗的分割線:
程式測試部分:

declare

  l_sql varchar2(30000);

begin

l_sql := 'SELECT120 + TRUNC (RN / 12) + ROUND (DBMS_RANDOM.VALUE (1, 10)) SAM_NUMBER

       ,RN||''資料Excel匯出'' SAM_CHAR

       ,SYSDATE+ TRUNC (RN / 12)+ROUND(DBMS_RANDOM.VALUE (1, 10)) SAM_DATE

  FROM (   SELECT LEVEL, ROWNUM RN

              FROM DUAL

        CONNECT BY ROWNUM <= 12000)';

SMT_XLSX_MAKER_PKG.query2sheet(l_sql,true,'ECX_UTL_LOG_DIR_OBJ','Export2.xlsx');

end;

結果:


begin

  SMT_XLSX_MAKER_PKG.clear_workbook;

  SMT_XLSX_MAKER_PKG.new_sheet;

  SMT_XLSX_MAKER_PKG.cell( 5, 1, 5 );

  SMT_XLSX_MAKER_PKG.cell( 3, 1, 3 );

  SMT_XLSX_MAKER_PKG.cell( 2, 2, 45 );

  SMT_XLSX_MAKER_PKG.cell( 3, 2, 'AntonScheffer', p_alignment => SMT_XLSX_MAKER_PKG.get_alignment( p_wraptext =>true ) );

  SMT_XLSX_MAKER_PKG.cell( 1, 4, sysdate,p_fontId => SMT_XLSX_MAKER_PKG.get_font( 'Calibri', p_rgb => 'FFFF0000' ));

  SMT_XLSX_MAKER_PKG.cell( 2, 4, sysdate,p_numFmtId => SMT_XLSX_MAKER_PKG.get_numFmt( 'dd/mm/yyyy h:mm' ) );

  SMT_XLSX_MAKER_PKG.cell( 3, 4, sysdate,p_numFmtId => SMT_XLSX_MAKER_PKG.get_numFmt(SMT_XLSX_MAKER_PKG.orafmt2excel( 'dd/mon/yyyy' ) ) );

  SMT_XLSX_MAKER_PKG.cell( 5, 5, 75, p_borderId=> SMT_XLSX_MAKER_PKG.get_border( 'double', 'double', 'double', 'double' ));

  SMT_XLSX_MAKER_PKG.cell( 2, 3, 33 );

  SMT_XLSX_MAKER_PKG.hyperlink( 1, 6,'http://www.amis.nl', 'Amis site' );

  SMT_XLSX_MAKER_PKG.cell( 1, 7, 'Some mergedcells', p_alignment => SMT_XLSX_MAKER_PKG.get_alignment( p_horizontal =>'center' ) );

  SMT_XLSX_MAKER_PKG.mergecells( 1, 7, 3, 7 );

  for i in 1 .. 5

  loop

    SMT_XLSX_MAKER_PKG.comment( 3, i + 3, 'Row' || (i+3), 'Anton' );

  end loop;

  SMT_XLSX_MAKER_PKG.new_sheet;

  SMT_XLSX_MAKER_PKG.set_row( 1, p_fillId =>SMT_XLSX_MAKER_PKG.get_fill( 'solid', 'FFFF0000' ) ) ;

  for i in 1 .. 5

  loop

    SMT_XLSX_MAKER_PKG.cell( 1, i, i );

    SMT_XLSX_MAKER_PKG.cell( 2, i, i * 3 );

    SMT_XLSX_MAKER_PKG.cell( 3, i, 'x ' || i *3 );

  end loop;

  SMT_XLSX_MAKER_PKG.query2sheet( 'selectrownum, x.*

, case when mod(rownum, 2 ) = 0 then rownum * 3 end demo

, case when mod(rownum, 2 ) = 1 then ''demo '' || rownum end demo2 from dual x connect byrownum <= 5' );

  SMT_XLSX_MAKER_PKG.save('ECX_UTL_LOG_DIR_OBJ', 'Export3.xlsx' );

end;