TIPS-固定欄寬資料檔的解析
| | | 1 | |
跟傳統系統打交道時,XML、JSON等格式多半無用武之地,往往得透過"固定欄寬資料格式"進行資料交換。
在撰寫程式解析固定欄寬資料時,有幾點注意事項:
- 欄位寬度計算與中文編碼有關,實務上使用BIG5編碼還是大宗(阿公級系統很少能支援Unicode)。
- BIG5編碼中,半形英數字佔一個Byte,全形符號及中文佔兩個Byte,欄寬規格書中CHAR(n),n多指Bytes,所以計算長度時,需把握中文算2或英數字算1的原則。
- 建議不要再用比對第一個Byte ASCII的方法自己判別全半形/英數/中文,請愛用Encoding.GetEncoding(950)(或Encoding.GetEncoding("big5"))的現成方法。
- 由於Unicode裡中英文的長度都算1,與BIG5中文計2英數算1有差別,故無法直接用SubString()精準截取預期位置的字串。建議做法是先GetBytes()轉成byte[]後,一一計算各欄位置及長度再用Encoding.GetString(byte[], startPos, length)取回內容。
以下是簡單範例,假設某BIG5編碼固定欄寬資料檔有四個欄位,分別是姓名(10)、註冊日期(8)、電話(11)、備註(10) (括號內為各欄長度),程式中示範如何利用Encoding.GetBytes() + Encoding.GetString(byte[], startPos, len)技巧一一取出各欄位內容。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Tester { class Program { static string rawText =
@"姓名 註冊日期電話 備註 Jeffrey 197001010800092000 人員
黑暗執行緒200502010988123456 部落格名稱
中文ABC 20101201021234567 無 ";
static void Main(string[] args)
{ //入門玩法 string[] lines = rawText.Split('\n');
Encoding big5 = Encoding.GetEncoding(950);
foreach (string line in lines)
{ byte[] data = big5.GetBytes(line.TrimEnd('\r'));
string name = big5.GetString(data, 0, 10).Trim(); string regDate = big5.GetString(data, 10, 8).Trim(); string tel = big5.GetString(data, 18, 11).Trim(); string remark = big5.GetString(data, 29, 10).Trim(); Console.WriteLine("{0},{1},{2},{3}", name, regDate, tel, remark);
}
Console.ReadLine();
//進階玩法 FixedWidthTextParser fwtp = new FixedWidthTextParser(big5, new FieldDef("姓名", 10), new FieldDef("註冊日期", 8),
new FieldDef("電話", 11), new FieldDef("備註", 10));
foreach (var dict in fwtp.Parse(rawText))
{ Console.WriteLine("{0},{1},{2},{3}", dict["姓名"], dict["註冊日期"], dict["電話"], dict["備註"]
);
}
Console.ReadLine();
}
}
}
不過在實務經驗中,用一串Encoding.GetString(byte[], startPos, len)取出各欄位有個小困擾: 因前一欄長度將決定下一欄的startPos,當欄位數較多時,常常得靠心算或敲計算機計算各欄起始位置(要命的是這種小木頭都會的加法練習,我還老算錯...),而且一旦有欄位長度調整或是順序調動,就有一堆startPos得重算...
上述程中示範的進階玩法,這是我後來較愛用的做法。宣告一個通用的固定欄寬文字解析類別,建立時定義各欄位名稱與長度,接著送入文字內容,就能用Dictionary["欄位名稱"]取用解析後的各欄內容,若要修改欄位長度或順序,只需修改建構式中的欄位宣告,不用再苦哈哈重算startPos,很方便吧!
以下是FixedWidthTextParser程式範例,未來遇有固定欄寬文字解析時,不妨試試這種新做法。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; /// <summary> /// 欄位定義 /// </summary> public class FieldDef
{ /// <summary> /// 欄位名稱 /// </summary> public string FieldName;
/// <summary> /// 起啟位置 /// </summary> public int StartPos = -1;
/// <summary> /// 欄位長度 /// </summary> public int Length;
/// <summary> /// 建構式 /// </summary> /// <param name="fldName">欄位名稱</param> /// <param name="len">欄位長度</param> public FieldDef(string fldName, int len)
{ FieldName = fldName;
Length = len;
}
}
/// <summary> /// 固定寬度文件解析 /// </summary> public class FixedWidthTextParser
{ List<FieldDef> fields = new List<FieldDef>(); Encoding encoding;
int lineLength = 0; /// <summary> /// 建構式,傳入文件定義 /// </summary> /// <param name="enc">文字編碼</param> /// <param name="def">欄位定義</param> public FixedWidthTextParser( Encoding enc,
params FieldDef[] def) { encoding = enc;
int startPos = 0; foreach (FieldDef fd in def)
{ fd.StartPos = startPos;
fields.Add(fd);
startPos += fd.Length;
lineLength += fd.Length;
}
}
/// <summary> /// 傳入單行資料字串,解析為多欄值 /// </summary> /// <param name="line">原始單行資料字串</param> /// <returns>各欄位值之雜湊表</returns> public Dictionary<string, string> ParseLine(string line)
{ //資料字串轉為byte[] byte[] data = encoding.GetBytes(line); //檢查長度是否吻合? if (data.Length != lineLength) throw new ApplicationException(
string.Format("字串長度({0}Bytes)不符要求({1}Bytes)!",
data.Length, lineLength));
//宣告雜湊結構存放資料 var result = new Dictionary<string, string>();
//依欄位位置、長度逐一取值 foreach (var fd in fields)
result.Add(fd.FieldName, //由指定位置取得內容並截去尾端空白 encoding.GetString(data, fd.StartPos, fd.Length).Trim());
return result; }
/// <summary> /// 解析多行固定欄寬資料 /// </summary> /// <param name="text">多行文字資料</param> /// <returns>解析結果</returns> public List<Dictionary<string, string>> Parse(string text)
{ var all = new List<Dictionary<string, string>>();
using (StringReader sr = new StringReader(text))
{ string line = null;
while ((line = sr.ReadLine()) != null)
all.Add(ParseLine(line));
}
return all; }
/// <summary> /// 解析固定欄寬資料檔 /// </summary> /// <param name="path">檔案路徑</param> /// <returns>解析結果</returns> public List<Dictionary<string, string>> ParseFile(string path)
{ if (!File.Exists(path)) throw new ApplicationException(path + "不存在!");
return Parse(File.ReadAllText(path, encoding)); }
}
Comments
# by 小熊子
太好了,可以再許個願嗎? 之前專案的需求,除了解析文字檔外,還需要將資料轉出成固定長度,有的時候文字要補空白,數字要補0,有的要切掉中文字,有的要置右對齊…