📊 用 ClosedXML 讀取三行合併表頭的 Excel 資料(含欄位名稱查找技巧)

| CSharp | 5 Reads

前言

在處理 Excel 檔時,表頭不一定總是單行的。有些報表的表頭可能會長成這樣:

資産情報 担当者
氏名 年齢
氏名 年齢

這樣的三行表頭、跨列合併,對程式來說根本就是「惡夢」。

你想做的事可能很簡單:

根據「欄位名稱」去讀取對應的資料列,比如 "担当者 > 氏名 > 年齢"

但 ClosedXML 並不會自動幫你解析這種結構,我們要自己動手,把「表頭→欄位編號」的對照表建立出來。


一、ClosedXML 不會幫你自動識別合併表頭

ClosedXML 是一個操作 Excel 的 C# 函式庫,它很強大,但它對於多行表頭+合併儲存格

  • 不會展開合併儲存格的所有位置;

  • 不會知道「這一欄實際上應該叫什麼名字」。

也就是說,你要自己判斷:

  • 哪個 cell 是被合併的;

  • 如果是合併儲存格,那該去看哪一格的值(左上角);

  • 最後要「拼接」表頭值來產出像 "担当者 > 氏名 > 年齢" 這種「邏輯欄位名」。


二、怎麼解析三行合併表頭?

我們先給出封裝好的工具類 ExcelHeaderReader,讓你不用每次都寫重複邏輯。

工具類 ExcelHeaderReader.cs

using System;
using System.Collections.Generic;
using System.Linq;
using ClosedXML.Excel;

public class ExcelHeaderReader
{
    private readonly IXLWorksheet _worksheet;
    private readonly int _headerRows;
    private readonly Dictionary<int, string> _columnHeaders = new();

    public ExcelHeaderReader(IXLWorksheet worksheet, int headerRows = 3)
    {
        _worksheet = worksheet;
        _headerRows = headerRows;
        BuildColumnHeaders();
    }

    private void BuildColumnHeaders()
    {
        var mergedLookup = new Dictionary<(int row, int col), IXLCell>();

        foreach (var range in _worksheet.MergedRanges)
        {
            var topLeft = range.FirstCell();
            foreach (var cell in range.Cells())
                mergedLookup[(cell.Address.RowNumber, cell.Address.ColumnNumber)] = topLeft;
        }

        int maxCol = _worksheet.LastColumnUsed().ColumnNumber();

        for (int col = 1; col <= maxCol; col++)
        {
            var parts = new List<string>();
            for (int row = 1; row <= _headerRows; row++)
            {
                var cell = _worksheet.Cell(row, col);
                if (cell.IsMerged() || mergedLookup.ContainsKey((row, col)))
                    cell = mergedLookup[(row, col)];

                var value = cell.GetString().Trim();
                if (!string.IsNullOrEmpty(value))
                    parts.Add(value);
            }

            _columnHeaders[col] = string.Join(" > ", parts);
        }
    }

    public string? GetValueByHeaderName(string headerName, int dataRow)
    {
        var match = _columnHeaders.FirstOrDefault(kvp => kvp.Value == headerName);
        return match.Key != 0 ? _worksheet.Cell(dataRow, match.Key).GetString() : null;
    }

    public Dictionary<int, string> GetAllHeaders() => _columnHeaders;

    public int? GetColumnIndexByHeader(string headerName)
    {
        return _columnHeaders.FirstOrDefault(kvp => kvp.Value == headerName).Key;
    }
}

三、使用方式

1. 載入 Excel 並建立 reader:

var wb = new XLWorkbook("三行合併表頭範例.xlsx");
var ws = wb.Worksheet(1);
var reader = new ExcelHeaderReader(ws, headerRows: 3);

2. 查找欄位名稱:

var colIndex = reader.GetColumnIndexByHeader("担当者 > 氏名 > 年齢");
Console.WriteLine($"該欄在第 {colIndex} 欄");

3. 根據欄位名稱取值:

for (int row = 4; row <= ws.LastRowUsed().RowNumber(); row++)
{
    var value = reader.GetValueByHeaderName("担当者 > 氏名 > 年齢", row);
    Console.WriteLine($"Row {row}: {value}");
}

四、解析出來的欄位可能長這樣

欄位位置 組合欄位名稱
A 資産情報 > 資産情報 > 資産情報
B 担当者 > 氏名 > 氏名
C 担当者 > 氏名 > 年齢
D 担当者 > 年齢 > 年齢

你就可以根據這些名稱來查欄、讀值。


五、進階建議

你也可以進一步封裝:

  • 去掉連續重複名稱(如 氏名 > 氏名氏名

  • 加入模糊搜尋(如包含 氏名 就匹配)

  • 加上 UI ComboBox 下拉選擇


結語

處理多層合併表頭的 Excel 是一項挑戰,但只要你掌握:

  • 合併儲存格如何定位

  • 多行表頭如何組合成欄位名

  • 如何建立「欄位名 ↔ 欄位編號」映射

你就可以優雅地從 Excel 中精準取值,像讀 JSON 一樣舒服。

This article was last edited at