首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 解析 ISO 2709 (MARC) 格式資料:標準書目交換格式完整實作

     編輯:關於C語言

前言

ISO 2709(資訊與文獻—書目資訊交換格式)是圖書館界廣泛使用的標準格式,又稱 MARC (MAchine-Readable Cataloging) 通用格式。本文深入解析一個 C# 類別 Nosi.Library.Marc,展示如何將原始位元組流切割為三大區段(Leader、Directory、Variable Fields),並提供欄位/子欄位層級的查詢 API。雖然現代圖書館系統多改用 MARCXML 或 JSON-LD,但理解二進位格式對資料遷移、舊系統整合仍具關鍵價值。

一、ISO 2709 記錄結構速覽

+----------------+----------------+---------------------+
| Leader (24 B)  | Directory      | Variable Fields     |
| 固定長度       | 可變長度       | 可變長度            |
|                | 每條 12 字元   | 以 0x1E 分隔欄位    |
|                | 以 0x1E 結束   | 子欄位以 0x1F 開頭  |
|                |                | 記錄以 0x1D 結束    |
+----------------+----------------+---------------------+
  • Leader (24 bytes):記錄長度、狀態、類型、編碼層級、基址等控制資訊。
  • Directory:每 12 字元一條:Tag(3) + Length(4) + Position(5)。Position 為相對 Variable Fields 起始位移。
  • Variable Fields:控制欄位 (00X) 無子欄位;資料欄位含指標(2) + 子欄位碼(1) + 資料。
  • 控制字元:FLDEND = 0x1E (30) 欄位結束、RECEND = 0x1D (29) 記錄結束、SUBFLD = 0x1F (31) 子欄位指示符。

二、核心類別設計

namespace Nosi.Library
{
    public class Marc
    {
        // 常數
        public const char FLDEND = (char)0x1E;
        public const char RECEND = (char)0x1D;
        public const char SUBFLD = (char)0x1F;
        public const int FLDNAME_LEN = 3;
        public const int MAX_MARCREC_LEN = 100_000;

        private string _marcRecord = "";

        // 屬性:原始記錄字串
        public string MarcRecord { get => _marcRecord; set => _marcRecord = value; }

        // 三大區段存取
        private string GetLeader() => _marcRecord.Substring(0, 24);
        private string GetDirectory() { /* 從 24 讀到第一個 FLDEND */ }
        private string GetVariableFields() { /* 依 Directory 長度切割 */ }

        // 公開查詢 API
        public int GetField(string tag, int occurrence, out string field) { ... }
        public int GetSubField(string field, string subCode, int occurrence, out string subField) { ... }
        public int GetSubField(string fieldGroup, string subCode, out string subField) { ... }
        public int GetFieldGroup(string field, int occurrence, out string group) { ... }

        // 輔助:雙位元組字元長度計算 (Big5/GB 環境)
        private int GetCharLength(char ch) => (ch > 127 || ch < 0) ? 2 : 1;
    }
}

三、關鍵演算法解析

1. Directory 解析與欄位定位

private int GetFieldPos(string tag, int index, out string fieldPos)
{
    string dir = GetDirectory();
    if (dir == null || (dir.Length - 1) % 12 != 0) return -1; // 格式錯誤

    int count = 0;
    for (int i = 0; i < dir.Length; i += 12)
    {
        if (dir.Substring(i, 3) == tag) count++;
        if (count == index)
        {
            fieldPos = dir.Substring(i, 12); // Tag(3) + Len(4) + Pos(5)
            return 1;
        }
    }
    return 0; // 未找到
}

回傳碼:-1 錯誤、0 找不到、1 成功。

2. Variable Fields 位移計算(支援雙位元組字元)


int length = int.Parse(fieldPos.Substring(3, 4));
int startPos = int.Parse(fieldPos.Substring(7, 5));
char[] data = GetVariableFields().ToCharArray();
int bytePos = 0, charIndex = 0;
while (bytePos < data.Length)
{
    charIndex += GetCharLength(data[bytePos]);
    if (charIndex >= startPos && charIndex <= startPos + length)
        field += data[bytePos];
    bytePos++;
}

關鍵點:Directory 記錄的是**字元位移**而非位元組位移,故須逐字元累加 GetCharLength 以正確定位中文字元(雙位元組)。

3. 子欄位提取


public int GetSubField(string field, string subCode, int occurrence, out string subField)
{
    if (subCode.Length != 1) return -1;
    char[] arr = field.ToCharArray();
    int found = 0;
    bool inTarget = false;
    for (int i = 0; i < arr.Length; i++)
    {
        if (arr[i] == SUBFLD && i+1 < arr.Length && arr[i+1].ToString() == subCode)
            found++;
        if (found == occurrence && !inTarget)
        {
            inTarget = true;
            if (arr[i] == SUBFLD) { inTarget = false; break; } // 下一子欄位
            subField += arr[i];
        }
    }
    return subField == null ? 0 : 1;
}

邏輯:掃描 SUBFLD + code 計數,進入目標子欄位後累加字元,遇到下一個 SUBFLD 停止。

4. 欄位群組 (Field Group) 概念

以 $a 為分隔符,將同一 Tag 多次出現的資料切分為多個「群組」(常見於 7XX 责任者欄位)。

四、使用範例


// 假設 rawMarc 為讀取的 ISO 2709 原始字串
var marc = new Marc { MarcRecord = rawMarc };

// 取得 200 欄位 (標題與責任) 第 1 次出現
if (marc.GetField("200", 1, out string f200) == 1)
{
    Console.WriteLine($"200 欄位完整內容: {f200}");
    
    // 取得 $a (標題)
    if (marc.GetSubField(f200, "a", 1, out string title) == 1)
        Console.WriteLine($"標題: {title}");
    
    // 取得 $f (責任者)
    if (marc.GetSubField(f200, "f", 1, out string author) == 1)
        Console.WriteLine($"作者: {author}");
}

// 遍歷所有 600 欄位 (主題)
for (int i = 1; ; i++)
{
    if (marc.GetField("600", i, out string f600) != 1) break;
    if (marc.GetSubField(f600, "a", 1, out string subject) == 1)
        Console.WriteLine($"主題: {subject}");
}

五、現代化改良建議

  1. 改用 Span<byte> / ReadOnlySpan<byte>:避免字串分配,直接在位元組層級解析,支援 UTF-8/MARC-8 自動偵測。
  2. 實作 IEnumerable<MarcField>:提供 LINQ 風格查詢 marc.Fields.Where(f => f.Tag == "200")。
  3. 支援 MARC-8 / UTF-8 編碼偵測:Leader 第 9 字元指示編碼,解碼時需正確處理。
  4. 非同步串流讀取:async IAsyncEnumerable<MarcRecord> 處理大型 MARC 檔案(百萬筆)。
  5. 結構化物件模型:定義 MarcRecord、MarcField、MarcSubField 類別,取代字串操作。
  6. 單元測試覆蓋:含邊界條件(空欄位、缺少 Directory 結束符、超長記錄、巢狀子欄位)。

六、總結

Nosi.Library.Marc 雖然以字串為基礎、程式碼風格屬於 .NET 1.x 時代,但完整實作了 ISO 2709 的核心解析邏輯:Leader/Directory/Variable Fields 三層切割、字元位移定位、子欄位代碼掃描。理解這個實作,有助於開發 MARC 轉換工具(MARC → MARCXML/Dublin Core/BIBFRAME)、批次匯入系統、書目資料品質檢核等圖書館資訊系統核心功能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved