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,有的要切掉中文字,有的要置右對齊…