繼上回以 big5 內碼分區查表方式取得中文字元筆劃數後,因無法納入 big5 字集的難字部分無法處理成為先天限制,使用起來頗為不快,不能滿意原解決方案。
於是繼續尋找可行方案,找到了 Unihan 統漢字資料庫,發現其資料十分豐富,倉頡碼、同義字、注音、筆劃數、部首筆劃數、... 等等資訊(沒全參透,就不完整列舉了),改天會再整理更多應用心得,這邊就先取用我需要的字元筆劃部分。應急 :)
Unihan 資料庫以純文字格式提供為多個檔案,我需要的筆劃資訊都存放在 Unihan 資料庫中的 Unihan_DictionaryLikeData.txt 檔,檔案格式不難解析:
U+3400 kCangjie TM U+3400 kTotalStrokes 5 U+3401 kCangjie MOW U+3401 kCihaiT 37.103 U+3401 kTotalStrokes 6 U+3402 kCangjie PPP U+3402 kTotalStrokes 6
如列表,每行以 tab 分隔,有三個重要資訊:編碼、屬性名稱、屬性資料。
若作為元件而且只是對應筆劃,暫時還不想動用到資料庫,我解析了 Unihan 資料庫中的 Unihan_DictionaryLikeData.txt 檔案,並將筆劃資訊存於 stream 中,並以 Unicode 字碼值,移動指標(查表)來迅速地取得筆劃資訊,主要程式碼如下:
StrokeLookup.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
namespace Unihan
{
public class StrokeLookup : IDisposable
{
private static StrokeLookup _instance;
public static StrokeLookup Instance
{
get
{
if (_instance == null)
{
_instance = new StrokeLookup();
}
return _instance;
}
}
// 利用 stream,存放筆劃資訊,以位移值取得筆劃數
private Stream _stream;
private StrokeLookup()
{
InitialLookupTable();
}
private void InitialLookupTable()
{
var binPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var dataPath = Path.Combine(binPath, "Unihan.Data");
var filePath = Path.Combine(dataPath, "Unihan_DictionaryLikeData.txt");
var lookupPath = Path.Combine(dataPath, "Unihan_DictionaryLikeData.strokes");
// 未曾產生或者 Unihan.Data 目錄中的 Unihan 資料庫有更新,則重新產生查表檔
// 這裡若改以 hash code 去偵測來源檔案是否有變化會更恰當
if (!File.Exists(lookupPath) || File.GetLastWriteTime(filePath) > File.GetLastWriteTime(lookupPath))
{
using (var stream = new FileStream(lookupPath, FileMode.Create, FileAccess.ReadWrite))
{
GenerateStrokeData(filePath, stream);
}
}
// 若改為以 MemoryStream 載入查表資料,也可以善用記憶體優勢
_stream = new FileStream(lookupPath, FileMode.Open, FileAccess.Read);
}
private void GenerateStrokeData(string filePath, Stream outputStream)
{
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
using (var reader = new StreamReader(stream))
{
var line = string.Empty;
while ((line = reader.ReadLine()) != null)
{
// 非有效行
if (string.IsNullOrEmpty(line) || !line.StartsWith("U+"))
{
continue;
}
// 每行只切為三分
var datas = line.Split(new[] { '\t', ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
// 格式不符或不含有筆劃資訊就忽略
if (datas.Length < 3 || datas[1] != "kTotalStrokes")
{
continue;
}
// U+3400 轉為 uint
var hex = datas[0].Substring(2);
var code = uint.Parse(hex, NumberStyles.HexNumber);
// 取得筆劃資訊
var stroke = byte.Parse(datas[2]);
// Padding 補足間隙的不存在字元
var gap = code - outputStream.Length;
if (gap > 1)
{
outputStream.Seek(0, SeekOrigin.End);
while (gap-- > 1)
{
outputStream.WriteByte(0);
}
}
outputStream.Seek(code, SeekOrigin.Begin);
outputStream.WriteByte(stroke);
}
}
}
}
public IEnumerable<CharStroke> GetStrokes(string source)
{
foreach (var chr in source)
{
yield return new CharStroke { Character = chr, Stroke = GetStroke(chr) };
}
}
public int GetStroke(char source)
{
var code = (uint)source;
if (code >= 0 && code < _stream.Length)
{
_stream.Seek(code, SeekOrigin.Begin);
return _stream.ReadByte();
}
return 0;
}
public void Dispose()
{
if (_stream != null)
{
_stream.Dispose();
}
}
}
}
CharStroke.cs
namespace Unihan
{
public class CharStroke
{
public char Character { get; set; }
public int Stroke { get; set; }
}
}
過程中曾經思考直接在類別中以靜態成員 Dictionary<char, byte> 儲存所有字元以及對應筆劃,這樣做在速度上並不慢,程式也好寫,可是頗為消耗記憶體;後來改用了 byte[] 以基底位置加上位移值去計算,想要快速地得到筆劃數,不過耗用記憶體的情形依舊,只是比前者要好一些,後來這兩種方式都放棄了。
範例方案完整原始碼 請由此下載,若有任何問題歡迎提供意見,謝謝! :)
沒有留言:
張貼留言