[ PHP ] Filesystem encoding and PHP
許多 PHP 應用程式會將檔案儲存本地端的檔案系統當中。 對於大部分讀者而言在多數的情況下,妳很可能只使用 US-ASCII 編碼的方式儲存檔案, 可能是因為妳的檔名是基於資料庫欄位 ( 這也是大部分情形妳應該使用的方式 ), 或者是因為妳的使用者不需要使用非英文以外的字元。 當妳需要處理不同字元,很重要的就是去瞭解不同作業系統之間對字元的處理方式。 而令人不感到意外的是,他們之間的確使用不同的方式處理。 為了描述這些差異,我將透過 Ubuntu,OS/X 10.6.3 以及 Windows XP 和 7 做相同的測試。
Linux
在 Linux 檔案名稱是以二進制的方式存在。 Linux 不在乎妳使用何種編碼方式,因此它允許任何字元除了 0x00。 這代表檔案名稱可以包含換行字元 (n),跳格字元 (t) 甚至鈴響符號( ascii code 07 )。 為了描述這一點,我打算透過 PHP 撰寫一個簡短的程式:
<?php file_put_contents("saved by the x07.txt","contents");
?>
<?php print_r(glob('saved\*'));
?>
<?php list($file) = glob('test_\*');
echo urlencode($file) . "n";
?>
<?php file_put_contents("uumlaut_xFC.txt","contents");
?>
OS/X
在 OS/X 上面,所有的檔名以 UTF-16 的方式儲存。妳不需要特別去瞭解這個, 因為 PHP 的 API 所使用的是 UTF-8,並且會自動幫妳轉換的動作。 我們會從響鈴符號開測試。結果跟 linux 上的相同。響鈴字元被表示成 ?。 使用查找器搜尋的時候,這個字元則是完全消失了。當使用下列程式顯示的時候它卻存在:
<?php list($filename) = glob('saved\*');
echo urlencode($filename) . "n";
?>
<?php file_put_contents("uumlaut_xFC.txt","contents");
?>
drwxr-xr-x 10 evert2 staff 340 16 Apr 17:08 . drwxr-xr-x 32 evert2 staff 1088 16 Apr 16:53 .. -rw-r--r-- 1 evert2 staff 8 16 Apr 16:54 saved by the ?.txt -rw-r--r-- 1 evert2 staff 121 16 Apr 16:54 test1.php -rw-r--r-- 1 evert2 staff 8 16 Apr 16:54 test2.php -rw-r--r-- 1 evert2 staff 101 16 Apr 17:07 test3.php -rw-r--r-- 1 evert2 staff 57 16 Apr 17:08 test4.php -rw-r--r-- 1 evert2 staff 8 16 Apr 17:08 uumlaut_%FC.txt
<?php file_put_contents("uumlaut2_xC3xBC.txt","contents");
?>
<?php list($file) = glob('uumlaut2_\*');
echo urlencode($file) . "n";
?>
<?php $before = "xC3xBC";
$after = Normalizer::normalize($before, Normalizer::FORM_D);
echo 'Before: ', urlencode($before), "n";
echo 'After: ', urlencode($after), "n";
?>
Windows
Windows 同樣使用 UTF-16 儲存檔案名稱( 透過 NTFS )。 跟 OS/X 相同,在PHP使用檔案系統的 API 時,轉換會自動進行。 我們將從響鈴測試開始:
<?php file_put_contents("saved by the x07.txt","contents");
?>
<?php file_put_contents("uumlaut_xFC.txt","contents");
list($file) = glob('uumlaut_\*');
echo urlencode($file) . "n";
?>
<?php file_put_contents("uumlaut2_xC3xBC.txt","contents");
list($file) = glob('uumlaut2_\*');
echo urlencode($file) . "n";
?>
<?php $files = glob('\*');
foreach($files as $file) { echo urlencode($file), "n";
} echo "total: " . count($files) . "n";
?>
<?php $files = scandir('.');
foreach($files as $file) { echo urlencode($file), "n";
} echo "total: " . count($files) . "n";
?>
Conclusion
在檔案名稱當中使用非拉丁字元會產生難以理解的結果。 如果不是在 Windows 下使用,其結果都可能一樣。 Windows 擁有完整的 API 處理各國的檔案名稱,但我猜 PHP 可能不完全支援。 我確信在 PHP6 已經針對該問題在規劃中,但目前卻有這方面的問題。 我希望在整個語言都 unicode 之前,檔案系統的 api 可以被替換掉。 至於 Linux( 使用二進制除儲存每一個東西,並允許任何 0x00 以外字元 )可能是最簡潔的方法, 最終的檔名能滿足人們讀寫上的需求,這也就表示檔名會經由編碼處理。 在這些案例當中最好的系統確實是 OS/X,不但使用 UTF-8 處理每一個部份, 並且將不正確的字元序列處理的很好,並且將相同意思但不同形式的字元以相同的方式儲存( 採用正規化 )。 這裡我是建議的方法: 如果妳希望在不同的作業系統上使用共通的作法處理所有的字元, 除了使用預先編碼( intermediate encoding )沒有其他的方式。 舉個例子妳可以在寫入磁碟之前簡單的透過 urlencode 處理所有的檔名。 使用 url 編碼不代表妳就不需要考慮編碼的問題。 url 編碼代表妳用不同的方式儲存這些位元,但這些字元仍然保持一致的意義。 所以妳必須確保妳的檔名保持有效的 UTF-8 字元序列。 UTF-8 已是今日編碼的首要選擇。 如果妳肯定妳只會用到 ISO-8859-1/latin-1 字集底下的字元, 那下列的表格既適用於這種情形:
Windows
使用 ISO-8859-1 編碼
Linux
使用 UTF-8 編碼 (允許其他的編碼形式,但不推薦).
OS/X
使用 UTF-8 編碼。會自動根據 D 形式的正規化進行轉換
底下表格順序說明各種作業系統的表現情況:
url 編碼的檔案名稱
描述
Linux
OS/X
Windows
%07
響鈴
磁碟上顯示為 %07
磁碟上顯示為 %07
產生錯誤並且無法儲存該檔名
%FC
ISO-8859-1 當中的 ü
在磁碟上顯示 %FC,視窗介面下顯示問號
在磁碟上顯示 %25FC ( %25 = %,所以在磁碟上表示為 %FC 的字串 )
在磁碟上顯示 %FC,視窗介面下也顯示正確的結果
%C3%BC
UTF-8 C 形式正規化中的 ü
在磁碟上顯示 %C3%BC,視窗介面下也顯示正確的結果
在磁碟上顯示 u%CC%88,視窗介面下也顯示正確的結果
在磁碟上顯示 %C3%BC,視窗介面下也顯示 ü
u%CC%88
UTF-8 D 形式正規化中的 ü
在磁碟上顯示 u%CC%88,視窗介面下也顯示正確的結果
在磁碟上顯示 u%CC%88,視窗介面下也顯示正確的結果
未測試,但猜測跟上一個測試結果類似
Configuration list
最後,我所使用的相關軟體列表:
- Windows
- 同時在 XP SP3 跟 7 上測試
- 由 windows.php.net 所建置的 PHP 5.3.2 VC9 x86
- NTFS 檔案系統
- Linux
- Ubuntu 9.10
- 由 ubuntu package repository 取得的 PHP 5.2.10
- ext3 檔案系統
- OS/X
- v10.6.3
- OS/X 上運作的 PHP 5.3.1
- HFS+ 檔案系統