Tommy 碎碎念

Tommy Wu's blog

Google
 

« 上一篇 | 下一篇 »

使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案
post by tommy @ 11 八月, 2006 19:22
PEAR 中, 有個可以讓 PHP 產生 Excel 檔案的東西: Spreadsheet_Excel_Writer, 透過這個物件, 我們可以產生一個正確的 Excel 檔案出來. 而在 0.9.0 版中, 加上了 Unicode 的支援, 我們只要把 BIFF 的版本設為 8, 然後用 setInputEncoding() 指定要使用的編碼就可以存成一個 UNICODE 的 Excel 檔案.

不過, 我們發現, 在資料筆數較少, 檔案較小的時候, 運作十分正常. 不過, 當筆數大到一定的大小之後, 所產生的檔案, 使用 Microsoft Excel 打開時, 會出現損毀的情形, 其中的內容是正確無誤, 不過格式都會不見. (據說用 OpenOffice 打開時會告知是 SST 錯誤)

今天在 PEAR 的網站中, 看到幾個月之前, 有人提出了一個解決方法, 修改 Workbook.php 中的下列兩個 function 如下:

    /**
* Calculate
* Handling of the SST continue blocks is complicated by the need to include an
* additional continuation byte depending on whether the string is split between
* blocks or whether it starts at the beginning of the block. (There are also
* additional complications that will arise later when/if Rich Strings are
* supported).
*
* @access private
*/
function _calculateSharedStringsSizes()
{
/* Iterate through the strings to calculate the CONTINUE block sizes.
For simplicity we use the same size for the SST and CONTINUE records:
8228 : Maximum Excel97 block size
-4 : Length of block header
-8 : Length of additional SST header information
-8 : Arbitrary number to keep within _add_continue() limit
= 8208
*/
$continue_limit = 8208;
$block_length = 0;
$written = 0;
$this->_block_sizes = array();
$continue = 0;

foreach (array_keys($this->_str_table) as $string) {
$string_length = strlen($string);
$headerinfo = unpack("vlength/Cencoding", $string);
$encoding = $headerinfo["encoding"];
$split_string = 0;

// Block length is the total length of the strings that will be
// written out in a single SST or CONTINUE block.
$block_length += $string_length;

// We can write the string if it doesn't cross a CONTINUE boundary
if ($block_length < $continue_limit) {
$written += $string_length;
continue;
}

// Deal with the cases where the next string to be written will exceed
// the CONTINUE boundary. If the string is very long it may need to be
// written in more than one CONTINUE record.
while ($block_length >= $continue_limit) {

// We need to avoid the case where a string is continued in the first
// n bytes that contain the string header information.
$header_length = 3; // Min string + header size -1
$space_remaining = $continue_limit - $written - $continue;


/* TODO: Unicode data should only be split on char (2 byte)
boundaries. Therefore, in some cases we need to reduce the
amount of available
*/
$align = 0;

# Only applies to Unicode strings
if ($encoding == 1) {
# Min string + header size -1
$header_length = 4;

if ($space_remaining > $header_length) {
# String contains 3 byte header => split on odd boundary
if (!$split_string && $space_remaining % 2 != 1) {
$space_remaining--;
$align = 1;
}
# Split section without header => split on even boundary
else if ($split_string && $space_remaining % 2 == 1) {
$space_remaining--;
$align = 1;
}

$split_string = 1;
}
}


if ($space_remaining > $header_length) {
// Write as much as possible of the string in the current block
$written += $space_remaining;

// Reduce the current block length by the amount written
$block_length -= $continue_limit - $continue - $align;

// Store the max size for this block
$this->_block_sizes[] = $continue_limit - $align;

// If the current string was split then the next CONTINUE block
// should have the string continue flag (grbit) set unless the
// split string fits exactly into the remaining space.
if ($block_length > 0) {
$continue = 1;
} else {
$continue = 0;
}
} else {
// Store the max size for this block
$this->_block_sizes[] = $written + $continue;

// Not enough space to start the string in the current block
$block_length -= $continue_limit - $space_remaining - $continue;
$continue = 0;

}

// If the string (or substr) is small enough we can write it in the
// new CONTINUE block. Else, go through the loop again to write it in
// one or more CONTINUE blocks
if ($block_length < $continue_limit) {
$written = $block_length;
} else {
$written = 0;
}
}
}

// Store the max size for the last block unless it is empty
if ($written + $continue) {
$this->_block_sizes[] = $written + $continue;
}


/* Calculate the total length of the SST and associated CONTINUEs (if any).
The SST record will have a length even if it contains no strings.
This length is required to set the offsets in the BOUNDSHEET records since
they must be written before the SST records
*/

$tmp_block_sizes = array();
$tmp_block_sizes = $this->_block_sizes;

$length = 12;
if (!empty($tmp_block_sizes)) {
$length += array_shift($tmp_block_sizes); # SST
}
while (!empty($tmp_block_sizes)) {
$length += 4 + array_shift($tmp_block_sizes); # CONTINUEs
}

return $length;
}

/**
* Write all of the workbooks strings into an indexed array.
* See the comments in _calculate_shared_string_sizes() for more information.
*
* The Excel documentation says that the SST record should be followed by an
* EXTSST record. The EXTSST record is a hash table that is used to optimise
* access to SST. However, despite the documentation it doesn't seem to be
* required so we will ignore it.
*
* @access private
*/
function _storeSharedStringsTable()
{
$record = 0x00fc; // Record identifier
$length = 0x0008; // Number of bytes to follow
$total = 0x0000;

// Iterate through the strings to calculate the CONTINUE block sizes
$continue_limit = 8208;
$block_length = 0;
$written = 0;
$continue = 0;

// sizes are upside down
$tmp_block_sizes = $this->_block_sizes;
// $tmp_block_sizes = array_reverse($this->_block_sizes);

# The SST record is required even if it contains no strings. Thus we will
# always have a length
#
if (!empty($tmp_block_sizes)) {
$length = 8 + array_shift($tmp_block_sizes);
}
else {
# No strings
$length = 8;
}



// Write the SST block header information
$header = pack("vv", $record, $length);
$data = pack("VV", $this->_str_total, $this->_str_unique);
$this->_append($header . $data);




/* TODO: not good for performance */
foreach (array_keys($this->_str_table) as $string) {

$string_length = strlen($string);
$headerinfo = unpack("vlength/Cencoding", $string);
$encoding = $headerinfo["encoding"];
$split_string = 0;

// Block length is the total length of the strings that will be
// written out in a single SST or CONTINUE block.
//
$block_length += $string_length;


// We can write the string if it doesn't cross a CONTINUE boundary
if ($block_length < $continue_limit) {
$this->_append($string);
$written += $string_length;
continue;
}

// Deal with the cases where the next string to be written will exceed
// the CONTINUE boundary. If the string is very long it may need to be
// written in more than one CONTINUE record.
//
while ($block_length >= $continue_limit) {

// We need to avoid the case where a string is continued in the first
// n bytes that contain the string header information.
//
$header_length = 3; // Min string + header size -1
$space_remaining = $continue_limit - $written - $continue;


// Unicode data should only be split on char (2 byte) boundaries.
// Therefore, in some cases we need to reduce the amount of available
// space by 1 byte to ensure the correct alignment.
$align = 0;

// Only applies to Unicode strings
if ($encoding == 1) {
// Min string + header size -1
$header_length = 4;

if ($space_remaining > $header_length) {
// String contains 3 byte header => split on odd boundary
if (!$split_string && $space_remaining % 2 != 1) {
$space_remaining--;
$align = 1;
}
// Split section without header => split on even boundary
else if ($split_string && $space_remaining % 2 == 1) {
$space_remaining--;
$align = 1;
}

$split_string = 1;
}
}


if ($space_remaining > $header_length) {
// Write as much as possible of the string in the current block
$tmp = substr($string, 0, $space_remaining);
$this->_append($tmp);

// The remainder will be written in the next block(s)
$string = substr($string, $space_remaining);

// Reduce the current block length by the amount written
$block_length -= $continue_limit - $continue - $align;

// If the current string was split then the next CONTINUE block
// should have the string continue flag (grbit) set unless the
// split string fits exactly into the remaining space.
//
if ($block_length > 0) {
$continue = 1;
} else {
$continue = 0;
}
} else {
// Not enough space to start the string in the current block
$block_length -= $continue_limit - $space_remaining - $continue;
$continue = 0;
}

// Write the CONTINUE block header
if (!empty($this->_block_sizes)) {
$record = 0x003C;
$length = array_shift($tmp_block_sizes);

$header = pack('vv', $record, $length);
if ($continue) {
$header .= pack('C', $encoding);
}
$this->_append($header);
}

// If the string (or substr) is small enough we can write it in the
// new CONTINUE block. Else, go through the loop again to write it in
// one or more CONTINUE blocks
//
if ($block_length < $continue_limit) {
$this->_append($string);
$written = $block_length;
} else {
$written = 0;
}
}
}
}
 

我們試用後, 目前看起來一切都正常.

Del.icio.us Furl HEMiDEMi Technorati MyShare
commons icon [1] 請問如何輸出成字串 ?? [ 回覆 ]

請問一下, Excel 產生後, 要如何轉成字串,
才能用Email 的方式寄出!
我上網找了很久,都沒找到!
因為轉成字串, 就能每天自動跑報表出來,Email給所有人
!!

commons icon [2] [ 回覆 ]

都用 pear 的 Spreadsheet_Excel_Writer 了, 應該也不差再用一個 Mail_mime 物件.
http://pear.php.net/manual/en/package.mail.mail-mime.php

commons icon [3] [ 回覆 ]

http://tw.myblog.yahoo.com/c9993/article?mid=20&prev=157&next=155&l=f&fid=7

搭配一下應該就OK了

commons icon [4] PEAR 的 Excel 檔案 的問題 [ 回覆 ]

tommy~
請問一下你試用後正常~那你的檔案有測到多大的呢??
我有用你介紹的方式執行,初期正常,但我今天檔案到54kb就又出現原來的問題~!故有此一問!!謝謝!

commons icon [5] [ 回覆 ]

幾百 K 吧, 可能有上到幾 MB 吧. 我們的客戶之後都沒反應過有不正常的情形了.
如果只是 54K 就有問題, 比較像是沒改之前的情形.

我記得我們沒改之前, 當初好像多數在 200K 左右會出問題.

commons icon [6] [ 回覆 ]

謝謝!這讓我頭痛了半天......

commons icon [7] RE:PEAR 的 Excel 檔案 的問題 [ 回覆 ]

sorry~!!我打錯了~!
是154kb~!!
謝謝tommy的提醒,確實是我沒有改過去!現在ok了(我一直以為我有改過!)

commons icon [8] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

tommy 您好:

當我在使用 Spreadsheet_Excel_Writer 時,
每當把 *.xls 下載完後,想要直接開啟,
都會出現一個 Microsoft Excel 的警告視窗,
內容為
"Microsoft Excel 無法使用 *.xls , 可能是因為這個檔案是唯讀的,或者存放它的地方是唯讀的,或是伺服器沒有回應"

當我按"取消"的時候,會出現 2 個狀況,

1.直接修復,並且提示"Excel 嘗試回復您的公式和數值,但是部份資料可能已經遺失或損毀",但是檔案內容都正確;

2.修復失敗,我得使用開啟舊檔,選擇"開啟並修復"-->"抽選資料"-->"復原公式",
之後 Excel 直接修復,並出現 敘述 1 的狀況;

我的測試程式

require_once 'Spreadsheet/Excel/Writer.php';

// Creating a workbook
$workbook = new Spreadsheet_Excel_Writer();
// Creating a worksheet
$worksheet =& $workbook->addWorksheet('Reporter');
$workbook->setVersion(8);
$worksheet->setInputEncoding('utf-8');

$x = 0;
$y = 0;
$worksheet->write($x, $y++, '群組');
$worksheet->write($x, $y++, '報表月份');
$worksheet->write($x, $y++, '應發獎金');

// hide gridlines so they don't mess with our Excel art.
$worksheet->hideGridLines();

// sending HTTP headers
$workbook->send('sales_reporter.xls');

$workbook->close();

這個狀況我一直處理不掉,請問這個狀況該如何處理呢??

commons icon [9] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

我用你上頭寫的程式, 在我的環境並不會有問題.
不過建議 $workbook->setVersion(8); 寫在產生 worksheet 之前 (不過好像之後也不會有問題).

另外, 下載的檔案, 不要直接用 excel 打開來, 先存起來. 然後再看看格式是不是正確的 excel 檔案.
也許用 notepad 先看一下, 也許是你的系統的 php 會有些錯誤 (警告? 提示?) 訊息出來吧, 這樣子會變成在那 excel 檔案前面出現那些字串, 格式自然會有問題.

commons icon [10] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

對了, 程式我放這兒, 可以自己試看看:
http://www.teatime.com.tw/~tommy/test/excel.php
source code 可以用這個看 (只是一個 soft link 到同一個檔案):
http://www.teatime.com.tw/~tommy/test/excel.phps

commons icon [11] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

tommy 您好:

真的是ok;
怪事了,為什麼在我的環境卻是有問題的,
請問可否給我一些想法呢???

是否安裝pear套件時,有什麼地方應該注意的呢??

謝謝您;

commons icon [12] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

另外補充:
我都是先存檔之後再開啟檔案的;

commons icon [13] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

我有使用notepad打開檔案,
開頭是
"俵遄??",
看不出是否是錯誤...

commons icon [14] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

我猜測是你的那個 php 檔案因為是 UTF-8 編碼的, 在 Windows 下頭, 多數的編輯器會在檔案前面加上 BOM 字元 (3 bytes), 而那些字元會被送出, 所以產生的 excel 檔案會多那幾個 bytes, 格式自然也就不對了.

如果你確定不是因為 BOM 造成的問題. 就把你有問題的檔案與我這兒產生正常的檔案用 binary mode 比較一下看看吧. 或者把有問題的檔案 email 給我看看吧.

commons icon [15] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

謝謝您寶貴的意見,我再試試看..

commons icon [16] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

非常感謝....問題解決!!!!

commons icon [17] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

Tommy大大你好
請教,如何在Linux環境安裝Spreadsheet_Excel_Writer
我server上已用yum安裝了PHP,也download了OLE及Spreadsheet_Excel_Writer的tar檔
查了google都是教人用win環境安裝,Linux環境要怎麼安裝呢?
謝謝

commons icon [18] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

linux 下頭與 window 下沒什麼不同吧.
如果你是用 tarball 裝的, 那只好裝了一個之後跑看看有少什麼再裝缺的東西吧. 或者學著用 pear 裝吧. 這些東西本來就是 pear 裡頭的東西, 一個指令應該就會把缺的都裝上去.
http://pear.php.net/manual/en/

或者直接找有包含這些東西的 linux 發行版本吧. 例如 Debian (UBuntu 應該也有, 反正也可以用 Debian 的套件庫).

commons icon [19] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

感謝Tommy大大
我install pear就可以把OLE及Spreadsheet_Excel_Writerj都安裝起來了
也可以正常運作了,
感謝

commons icon [20] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

Tommy大大,
我是從Spreadsheet_Excel_Writer的那個bug討論的thread裡面找到你這裡來的。我安裝好了Spreadsheet_Excel_Writer以及OLE後按照你的http://www.teatime.com.tw/~tommy/test/excel.phps 裡面的方法生成出來的excel文檔,正文內容看上去都OK,但是下面sheetname裡面包含的中文都亂掉了。請問您有遇見過這種情況么?
謝謝!

commons icon [21] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

名稱的問題請參考另外一篇.
http://blog.teatime.com.tw/1/post/301

commons icon [22] Re:使用 Spreadsheet_Excel_Writer 產生 UNICODE 的 Excel 檔案 [ 回覆 ]

太贊了!感謝T大大!

迴響
暱稱:
標題:
個人網頁:
電子郵件:
authimage

迴響

  

Bad Behavior 已經阻擋了 6929 個過去 7 天試圖闖關的垃圾迴響與引用。
Power by LifeType. Template design by JamesHuang. Valid XHTML and CSS