固定寬度 (Fixed-width) 文字檔常見於銀行匯款、主機下載、政府開放資料。本文提供 Fixed2Csv 類別繼承 SplFileObject,利用迭代器逐行轉換,支援欄位位置/寬度陣列、分隔符/換行符自訂、欄位選擇性輸出、記憶體友善,適合大檔案 ETL、資料庫匯入前處理。
Fixed2Csv<?php
/**
* 固定寬度檔案轉 CSV 轉換器
* 繼承 SplFileObject 利用內建迭代器,記憶體效率高
*/
class Fixed2Csv extends SplFileObject {
private $fields = []; // [開始位置 => 寬度]
private $separator = ','; // CSV 分隔符
private $eol = "\n"; // 換行符
public function __construct($filename) {
parent::__construct($filename, 'r');
}
// 動態屬性設定
public function __set($name, $value) {
switch ($name) {
case 'fields':
case 'separator':
case 'eol':
$this->$name = $value;
break;
default:
throw new Exception("無法設定屬性: $name");
}
}
// 動態屬性讀取 (提供預設值)
public function __get($name) {
switch ($name) {
case 'fields': return [];
case 'separator': return ',';
case 'eol': return "\n";
default: throw new Exception("無法讀取屬性: $name");
}
}
/**
* 覆寫 current():將固定寬度行轉為 CSV 格式
* @return string|false CSV 行或 false (檔案結束)
*/
public function current() {
$line = parent::current();
if ($line === false) return false;
$csv = '';
$fieldIter = new CachingIterator(new ArrayIterator($this->fields));
foreach ($fieldIter as $width) {
$start = $fieldIter->key();
$value = trim(substr($line, $start, $width));
$csv .= $value;
$csv .= $fieldIter->hasNext() ? $this->separator : $this->eol;
}
return $csv;
}
}
?>
假設 data.txt 為固定寬度格式:
ID(10)NAME(15)AMOUNT(12)DATE(8)
0000000001John Doe 00001234.5620240115
0000000002Jane Smith 00000987.0020240116
<?php
require 'Fixed2Csv.class.php';
try {
// 1. 完整轉換所有欄位
$file = new Fixed2Csv('data.txt');
$file->fields = [
0 => 10, // ID: 位置 0, 寬度 10
10 => 15, // NAME: 位置 10, 寬度 15
25 => 12, // AMOUNT: 位置 25, 寬度 12
37 => 8, // DATE: 位置 37, 寬度 8
];
$file->separator = ',';
$file->eol = "\n";
echo "完整轉換:\n";
foreach ($file as $line) {
echo $line;
}
// 2. 僅輸出 ID 與 AMOUNT (欄位選擇)
echo "\n--- 僅 ID 與 AMOUNT ---\n";
$file2 = new Fixed2Csv('data.txt');
$file2->fields = [
0 => 10, // ID
25 => 12, // AMOUNT
];
$file2->separator = '|';
foreach ($file2 as $line) {
echo $line;
}
// 3. 直接寫入檔案 (大檔案建議)
$output = fopen('output.csv', 'w');
foreach ($file as $line) {
fwrite($output, $line);
}
fclose($output);
echo "\n已輸出至 output.csv\n";
} catch (Exception $e) {
echo "錯誤: " . $e->getMessage();
}
?>
完整轉換:
0000000001,John Doe,00001234.56,20240115
0000000002,Jane Smith,00000987.00,20240116
--- 僅 ID 與 AMOUNT ---
0000000001|00001234.56
0000000002|00000987.00
<?php
class AdvancedFixed2Csv extends Fixed2Csv {
private $hasHeader = false;
private $headerMap = []; // 欄位名 => [start, width]
private $typeMap = []; // 欄位名 => callable 轉換函式
public function setHeaderMap(array $map) {
$this->headerMap = $map;
// 將關聯陣列轉為位置索引陣列供父類別使用
$this->fields = [];
foreach ($map as $name => $def) {
$this->fields[$def['start']] = $def['width'];
}
ksort($this->fields); // 確保位置順序
}
public function setTypeMap(array $map) {
$this->typeMap = $map;
}
public function current() {
$csv = parent::current();
if ($csv === false) return false;
// 若需型別轉換,這裡可進一步處理 (需解析 CSV 再組裝)
// 為效率考量,建議在消費端處理型別轉換
return $csv;
}
// 靜態工廠方法:從標題列自動偵測欄位寬度
public static function fromHeaderLine($filename, $headerLineNum = 0) {
// 實作略:讀取標題列,分析欄位邊界 (連續空白/特定分隔符)
}
}
?>
| 技術 | 效益 | |
|---|---|---|
| 繼承 SplFileObject | 內建檔案指標管理、迭代器介面、逐行讀取不佔記憶體 | |
| CachingIterator + ArrayIterator | 單次遍歷即知是否為最後欄位,決定分隔符/換行符 | |
| trim() 移除填充空白 | 固定寬度常以空白填充,trim 乾淨去除 | |
| 可選欄位輸出 | 僅處理需要的欄位,減少 CPU/IO | |
| 分隔符/換行符可配置 | 支援 CSV/TSV/自訂格式,直接餵資料庫 LOAD DATA INFILE |
| 來源格式 | 特徵 | 轉換關鍵 |
|---|---|---|
| COBOL 複製本 | PIC 9(10) / PIC X(20) 定義 | 解析 Copybook 產生 fields 陣列 |
| 銀行 ACH/匯款 | 固定 94/120/... 字元/行 | 依規格書定義位置寬度 |
| 主機報表列印 | 對齊欄位、空白填充 | trim() 處理、注意數字前導零 |
| 政府開放資料 | 常提供 CSV 已轉換 | 優先用現成 CSV,避免重複造輪 |