2023年8月15日 星期二

.NET C# 第一次 NLog 的使用說明

.NET C# 第一次 NLog 的使用說明

在上一篇文章 探討在沒有使用任何一種 Log 套件下,想要做到系統活動的設計做法 中,說明了當程式設計師想要一個日誌機制這樣功能需求的時候,往往是自己打造輪子,認為沒有人可以寫得比我好,沒想到這僅是讓費更多的時間與成本,並且造成未來程式碼維護上的困擾與難以維護狀況。

從這篇文章開始,將會是一系列探討 NLog 這個套件使用方式的文章,NLog 是一個強大的日誌架構,可用於 .NET 應用程式。它提供多種功能,包括日誌級別、日誌格式和日誌目的地。NLog.Schema 套件可用於在 NLog 日誌配置中使用 XML 語法,這使其更易於使用和維護。

雖然 NLog 存在已經有段時間了,不過,這個套件內存在著許多令人驚豔與值得探索的價值,因此,就先以一個小白的身分,對於從未摸過 NLog 的程式設計師,說明如何入門使用 NLog 這個套件與如何利用它來完成日誌記錄功能。

建立自行發明的寫入日誌之類別服務的測試專案

請依照底下的操作,建立起這篇文章需要用到的練習專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [主控台]
  • 在中間的專案範本清單中,找到並且點選 [主控台應用程式] 專案範本選項

    專案,用於建立可在 Windows、Linux 及 macOS 於 .NET 執行的命令列應用程式

  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 csLog02 作為專案名稱
  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 7.0 (標準字詞支援)
  • 在這個練習中,需要去勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

    這裡的這個操作,可以由讀者自行決定是否要勾選這個檢查盒控制項

  • 請點選右下角的 [建立] 按鈕

稍微等候一下,這個主控台專案將會建立完成

安裝要用到的 NuGet 開發套件

因為開發此專案時會用到這些 NuGet 套件,請依照底下說明,將需要用到的 NuGet 套件安裝起來。

安裝 NLog 套件

這個套件將會是 NLog 日誌架構的核心套件,它提供 NLog 日誌架構的核心功能。

  • 滑鼠右擊 [方案總管] 視窗內的 [專案節點] 下方的 [相依性] 節點
  • 從彈出功能表清單中,點選 [管理 NuGet 套件] 這個功能選項清單
  • 此時,將會看到 [NuGet: csLog02] 視窗
  • 切換此視窗的標籤頁次到名稱為 [瀏覽] 這個標籤頁次
  • 在左上方找到一個搜尋文字輸入盒,在此輸入 NLog
  • 點選 [NLog] 套件名稱,請選擇作者為 [Jarek Kowalski,Kim Christensen,Julian Verdurmen] 的套件
  • 在視窗右方,將會看到該套件詳細說明的內容,其中,右上方有的 [安裝] 按鈕
  • 點選這個 [安裝] 按鈕,將這個套件安裝到專案內

安裝 NLog.Schema 套件

NLog.Schema 是一個 .NET NuGet 套件,它提供 NLog 日誌架構的 XML 定義。此定義可用於在 NLog 日誌配置中使用 XML 語法。

  • 滑鼠右擊 [方案總管] 視窗內的 [專案節點] 下方的 [相依性] 節點

  • 從彈出功能表清單中,點選 [管理 NuGet 套件] 這個功能選項清單

  • 此時,將會看到 [NuGet: csLog02] 視窗

  • 切換此視窗的標籤頁次到名稱為 [瀏覽] 這個標籤頁次

  • 在左上方找到一個搜尋文字輸入盒,在此輸入 NLog.Schema

  • 點選 [NLog.Schema] 套件名稱,請選擇作者為 [Jarek Kowalski,Kim Christensen,Julian Verdurmen] 的套件

  • 在視窗右方,將會看到該套件詳細說明的內容,其中,右上方有的 [安裝] 按鈕

  • 點選這個 [安裝] 按鈕,將這個套件安裝到專案內

  • 此時,從方案總管視窗內,將會看到有個 [NLog.xsd] 檔案,這個檔案是 NLog.Schema 套件安裝後,自動產生的檔案

  • 在 [方案總管] 內找到並且點選 [NLog.xsd] 檔案這個節點

  • 從 [屬性] 視窗中,將 [複製到輸出目錄] 屬性值改為 [有更新時才複製],這樣才能讓 [NLog.xsd] 檔案在執行時,能夠被複製到執行目錄內

    若沒有發現到 [屬性] 視窗,請在 [Visual Studio] 功能表中,點選 [檢視] > [屬性視窗] 功能選項

建立 NLog.config 設定檔

  • 滑鼠右擊 [方案總管] 視窗內的 [專案節點]

  • 從彈出功能表清單中,點選 [新增項目] 這個功能選項清單

  • 此時,將會看到 [新增項目 - csLog02] 視窗

  • 在左方的清單選項中,點選 [已安裝] > [C# 項目] > [資料] 節點

  • 在該對話窗的中間區域,找到並點選 [XML 檔案]

  • 在下方 [名稱] 欄位內,輸入 NLog.config 作為檔案名稱

  • 點選右下方 [新增] 按鈕,將這個檔案加入到專案內

  • 在 [方案總管] 內找到並且點選 [NLog.config] 檔案這個節點

  • 從 [屬性] 視窗中,將 [複製到輸出目錄] 屬性值改為 [有更新時才複製],這樣才能讓 [NLog.config] 檔案在執行時,能夠被複製到執行目錄內

    若沒有發現到 [屬性] 視窗,請在 [Visual Studio] 功能表中,點選 [檢視] > [屬性視窗] 功能選項

  • 使用底下的 XML 內容來替換掉這個檔案內的內容

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	  autoReload="true"
	  throwConfigExceptions="true"
	  internalLogLevel="Info"
	  internalLogFile="c:\temp\Sample-internal-nlog.txt"
	  >

	<!-- 在這裡宣告要用到的 Target 項目 -->
	<targets>
		<!-- 宣告 File 目標,將會把 Log 內容寫入到檔案內 -->
		<!--更多 Layout Renderer 變數,可以參考 https://nlog-project.org/config/?tab=layout-renderers-->
		<!--https://github.com/NLog/NLog/wiki/Layouts-->
		<target xsi:type="File" name="allfile" 
				fileName="c:\temp\AllLog.log"
				layout="${longdate}|${uppercase:${level}}|${logger}|[${threadname:whenEmpty=${threadid}}]|${message} ${exception:format=type,message,method:maxInnerExceptionLevel=5:innerFormat=shortType,message,method}"
				/>

		<!--宣告 Console 目標,將會把 Log 內容寫入到 命令提示字元視窗內 -->
		<target xsi:type="Console" name="lifetimeConsole"
				layout="${level:truncate=4:lowercase=true}: ${logger} [${threadname:whenEmpty=${threadid}}]${newline}      ${message}${exception:format=tostring}"
		        />
	</targets>

	<rules>

		<!--紀錄剩下其他的日誌項目 (視為黑洞)-->
		<logger name="*" minlevel="Warn"
				writeTo="lifetimeConsole,allfile" />
	</rules>
</nlog>

NLog.config 設定檔的說明

在 NLog.config 設定檔內,有幾個重要的屬性,這些屬性可以控制 NLog 的行為

  • autoReload : 指定是否要自動重新載入 NLog 設定檔
  • throwConfigExceptions : 指定是否要在 NLog 設定檔內發生錯誤時,拋出例外
  • internalLogLevel : 指定 NLog 內部的日誌級別
  • internalLogFile : 指定 NLog 內部的日誌檔案名稱

在 targets 節點內,宣告了兩個 Target 項目,分別是 allfile 和 lifetimeConsole,這兩個 Target 項目都是用來寫入日誌內容的目標。

對於 allfile 這個目標,它會將日誌內容寫入到 c:\temp\AllLog.log 這個檔案內,並且使用 ${longdate}|${uppercase:${level}}|${logger}|[${threadname:whenEmpty=${threadid}}]|${message} ${exception:format=type,message,method:maxInnerExceptionLevel=5:innerFormat=shortType,message,method} 這個格式來寫入日誌內容。

其格式為 : [日期和時間] | [日誌級別] | [日誌來源] | [執行緒名稱] | [訊息] | [例外]

對於 lifetimeConsole 這個目標,它會將日誌內容寫入到命令提示字元視窗內,並且使用 ${level:truncate=4:lowercase=true}: ${logger} [${threadname:whenEmpty=${threadid}}]${newline} ${message}${exception:format=tostring} 這個格式來寫入日誌內容。

其格式為 : [日誌級別] | [日誌來源] | [執行緒名稱] | [訊息] | [例外]

透過這兩個目標,可以了解到如何宣告 NLog.config 設定檔案,讓 NLog 系統將日誌內容寫入到檔案內和命令提示字元視窗內。

而在 rules 節點內,宣告了一個 logger 項目,這個項目的名稱為 *,這個名稱代表所有的日誌來源,而這個項目的 minlevel 屬性值為 Warn,這個屬性值代表只有日誌級別為 Warn 以上的日誌才會被寫入到 allfile 和 lifetimeConsole 這兩個目標內。

而在 rules 節點內

  • 這個 rule 元素會將所有日誌訊息寫入 lifetimeConsole 和 allfile 目標。
  • logger name="*" minlevel="Warn" 部分,會將所有日誌訊息寫入,只要它們的級別至少為警告 (Warn)。
  • writeTo="lifetimeConsole,allfile" 部分,會將日誌訊息寫入 lifetimeConsole 和 allfile 目標。
  • lifetimeConsole 目標會將日誌訊息寫入命令提示字元視窗。
  • allfile 目標會將日誌訊息寫入檔案 c:\temp\AllLog.log。
  • 您可以使用 minlevel 元素來指定日誌訊息的最低級別。日誌訊息只有在其級別至少為指定的級別時才會被寫入目標。
  • 您可以使用 writeTo 元素來指定日誌訊息要寫入的目標。您可以指定一個或多個目標。
  • 您可以使用 layout 元素來自訂日誌訊息的格式。

建立要使用 NLog 套件的程式碼

  • 在 [方案總管] 內找到並且開啟 [Program.cs] 檔案這個節點
  • 使用底下 C# 程式碼,將原本的程式碼取代掉
using NLog;

namespace csLog02
{
    internal class Program
    {
        // 取得當前執行這個方法的類別對應的 Logger 物件
        public static Logger logger =
            LogManager.GetCurrentClassLogger();
        static void Main(string[] args)
        {
            // 請觀察 Console & Log File 所寫入的內容為何?
            Console.WriteLine($"寫入各種不同層級的 日誌項目");

            logger.Trace("我是追蹤:Trace");
            logger.Debug("我是偵錯:Debug");
            logger.Info("我是資訊:Info");
            logger.Warn("我是警告:Warn");
            logger.Error("我是錯誤:error");
            logger.Fatal("我是致命錯誤:Fatal");
        }
    }
}
  • 在這個程式碼中,首先建立一個型別為 [Logger] 的靜態變數 [logger],這個變數是用來記錄 NLog 系統的 Logger 物件
  • 而在 [Main] 程式進入點方法內,將會依序呼叫 [logger] 這個物件的 [Trace] , [Debug] , [Info] , [Warn] , [Error] , [Fatal] 這六個方法,來寫入不同級別的日誌內容
  • 觀察這樣的使用方式,比對 探討在沒有使用任何一種 Log 套件下,想要做到系統活動的設計做法 文章內的作法,將會有天壤之別,因為,使用了類似 [NLog] 這樣的套件之後,讓整個應用程式變得更加簡單、清爽
  • 若想要指定不同日誌層級要寫入到指定目標內,僅需要修正 [NLog.config] 檔案即可,而在自行設計的程式碼中,也僅需要根據當時執行情況,寫入指定層級的日誌內容即可
  • 因此,之前提到的問題,都已經迎刃而解了。

執行程式,觀察結果

請先確認這台電腦上在 C:\ 根目錄下,有一個 temp 資料夾,以便可以讓 NLog 系統寫入相關 Log 資訊。

  • 按下 F5 鍵,開始執行這個程式

  • 請觀察 Console 視窗內的內容

warn: csLog02.Program [1]
      我是警告:Warn
erro: csLog02.Program [1]
      我是錯誤:error
fata: csLog02.Program [1]
      我是致命錯誤:Fatal

從執行後的螢幕輸出內容,可以看到 NLog 系統已經將日誌內容寫入到命令提示字元視窗內;並且僅有 [warn] , [error] , [fata] 這三個日誌級別的日誌內容被寫入到命令提示字元視窗內,這樣的結果是符合 NLog.config 設定檔內的規則。

  • 請觀察 C:\temp 目錄下,應該有兩個檔案,分別是 [AllLog.log] 和 [Sample-internal-nlog.txt] 這兩個檔案

  • 請使用記事本開啟 [Sample-internal-nlog.txt] 檔案,應該可以看到底下的內容

2023-08-15 10:57:18.7570 Info Registered target NLog.Targets.FileTarget(Name=allfile)
2023-08-15 10:57:18.7711 Info Registered target NLog.Targets.ConsoleTarget(Name=lifetimeConsole)
2023-08-15 10:57:18.7853 Info NLog, Version=5.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c. File version: 5.2.3.1999. Product version: 5.2.3+a5ddef92a8afb22508450803e37c001f4a3ba52a. GlobalAssemblyCache: False
2023-08-15 10:57:18.8079 Info Validating config: TargetNames=lifetimeConsole, allfile, ConfigItems=40, FilePath=C:\Vulcan\Projects\csLog02\csLog02\bin\Debug\net7.0\NLog.config
2023-08-15 10:57:18.8180 Info Configuration initialized.
2023-08-15 10:57:18.8481 Info AppDomain Shutting down. LogFactory closing...
2023-08-15 10:57:18.8696 Info LogFactory has been closed.
  • 請使用記事本開啟 [AllLog.log] 檔案,應該可以看到底下的內容
2023-08-15 10:57:18.8180|WARN|csLog02.Program|[1]|我是警告:Warn 
2023-08-15 10:57:18.8481|ERROR|csLog02.Program|[1]|我是錯誤:error  

2023-08-15 10:57:18.8481|FATAL|csLog02.Program|[1]|我是致命錯誤:Fatal  






2023年8月9日 星期三

探討在沒有使用任何一種 Log 套件下,想要做到系統活動的設計做法

探討在沒有使用任何一種 Log 套件下,想要做到系統活動的設計做法

當在進行任何軟體專案開發的時候,在開發階段,對於所設計的專案程式碼有任何疑慮,或者所執行的結果不正確的時候,隨時可以透過 IDE 工具來設定一個中斷點,一旦程式執行到這個中斷點的時候,就會停止執行,並且進入到中斷點的程式碼區段,這時候可以透過觀察程式碼的變數值,來判斷程式碼的執行是否正確,或者是透過這個中斷點,來進行程式碼的逐步執行,觀察程式碼的執行流程,這些都是在開發階段,可以透過 IDE 工具來進行的操作。另外,也可以透過程式碼來將各種物件資訊,輸出到 Console 上或者使用類似 MessageBox 這樣的 API 來彈跳出一個對話窗,藉此觀察到更多詳細資訊。

可是,當這個軟體系統開發完成之後,這些在開發階段可以透過 IDE 工具來觀察的資訊,就不再適用了,在軟體系統正式上線之後,便無法使用 IDE 這類工具來取得相關資訊;可是,當這個軟體系統運作不如預期或者發生了異常行為,身為開發者要如何解讀與找出真正發生的原因呢?

這時候,就需要透過 Log 這樣的機制來記錄系統的活動,這些活動的資訊,可以透過 Log 這樣的機制,將這些資訊寫入到檔案內,或者是寫入到資料庫內,這樣的做法,就可以讓該軟體開發者或者一線維運工程師,可以透過這些 Log 資訊,來觀察系統的活動,進而判斷或者解讀,甚至能夠找出當時系統發生了甚麼問題。

然而,對於要提供那些機制或者要將資料寫到哪裡,對於系統開發而言,一旦這些都是需求要由開發者自行開發出來,此時,就需要面對如何設計出一個好的 Log 的機制,這個機制的設計,就是要讓系統開發者可以透過這個機制,來將系統的活動資訊,寫入到某個地方,這個地方可以是檔案,也可以是資料庫,或者遠端的 Web API,甚至是其他的地方,看到這裡,相信你也會覺得這是一個相當負責且不容易做到的事情。

然而,對於軟體設計師而言,對於這樣的狀況都是覺得自己最厲害、自己最棒棒,凡事自己動手做,才能夠凸顯出自己的厲害, 殊不知這樣的決定將會造成更多的災害與更多問題出來。

由於這是一個系列文章,主要是要探討 Log 的機制的使用與設計,所以,就會有這篇開始文章,從一切錯誤的決定或者自以為視判斷開始,了解到前人的經驗與貢獻有多麼的重要,徹底了解重新打造輪胎這樣的原則之背後意義。

在這篇文章中,將會探討在沒有使用任何一種 Log 套件下,想要做到系統活動的設計做法,這裡所說的沒有使用任何一種 Log 套件,是指的不使用任何一種第三方的 Log 套件,而是自行設計一個 Log 的機制,這樣的做法,可以讓讀者了解到 Log 的機制是如何運作的,進而可以更加了解到 Log 的機制。

建立自行發明的寫入日誌之類別服務的測試專案

請依照底下的操作,建立起這篇文章需要用到的練習專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [主控台]
  • 在中間的專案範本清單中,找到並且點選 [主控台應用程式] 專案範本選項

    專案,用於建立可在 Windows、Linux 及 macOS 於 .NET 執行的命令列應用程式

  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 csLog01 作為專案名稱
  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 7.0 (標準字詞支援)
  • 在這個練習中,需要去勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

    這裡的這個操作,可以由讀者自行決定是否要勾選這個檢查盒控制項

  • 請點選右下角的 [建立] 按鈕

稍微等候一下,這個主控台專案將會建立完成

自行設計寫入日誌的功能

  • 在專案中找到與打開 [Program.cs] 這個檔案
  • 使用底下 C# 程式碼取代原始的程式碼
```csharp
namespace csLog01
{
    /// <summary>
    /// 不要發明輪子,自己做 Log 機制
    /// 在底下的做法,你看到了甚麼問題?
    /// </summary>
    internal class Program
    {
        public static Logger logger = new Logger();
        static void Main(string[] args)
        {
            logger.Trace("我是追蹤:Trace");
        }
    }

    public class Logger
    {
        public void Trace(string message)
        {
            #region 想要將日誌訊息寫入螢幕
            Console.WriteLine(message);
            #endregion

            #region 想要將日誌訊息寫入到檔案內
            string filePath = "example.txt";

            using (StreamWriter writer = new StreamWriter(filePath, true))
            {
                writer.Write(message + Environment.NewLine);
            }
            #endregion
        }
    }
}

在 Logger 類別內的 Trace 方法內,將會接收到一個字串參數,這個 message 參數,將會是日誌內容,當要把這個 message 寫入到檔案內的時候,該檔案的名稱與路徑,將會是寫死在這個類別內,無法透過外部檔案或者設定來變更,當然,這樣的日誌類別就會顯得很沒有彈性。

任何程式設計師當有這樣日誌需求的時候,大家都會想到的是,這一個非常簡單的工作,那不就是把程式碼中的物件或者當時的狀態,輸出到任何一個裝置上,例如:螢幕、檔案內,好的,打完,收工。

越是簡單的工作,就會存在著更多的風險與問題,若選擇要將各種日誌內容寫入到螢幕上,此時,當在開發階段,程式設計師在自己點腦上來執行這個系統,當然可以在自己的螢幕上看到這些日誌內容,然而,若這個系統在正式 Production 環境上運行的時候,若將這些內容寫入到螢幕上呢?這時候,就需要透過遠端連線的方式,來連線到 Production 環境的機器上,然後再透過某種方式,來觀察這些日誌內容,這樣的做法,就會造成更多的問題,例如:在 Production 環境上,這些日誌內容,可能會被其他人看到,這樣的做法,就會造成資訊安全的問題。最重要的是,當有問題發生的時候,如何取得之前某個時間點發生的日誌內容,因為,當開發者收到這樣問題回報的時候,這些日誌內容,就會被覆蓋掉,甚至沒有永久保存的問題,這樣的做法,就會造成無法追蹤問題的發生原因。

不管如何,在這篇文章中,將會假設一個程式設計師,想要硬做出這樣日誌需求的功能,會經歷要寫出那些程式碼,這會有助於之後的系列文章,知道市面上的各種日誌工具或者開發套件的價值。

為了要提供日誌這樣的服務,當然要先設計一個類別,這個類別就是要提供日誌服務的類別,這個類別的名稱,就叫做 Logger,這個類別內,將會提供一個 Trace 方法,這個方法的功能就是要將日誌訊息寫入到螢幕上,這個方法的實作,就是透過 Console.WriteLine 這個 API 來將日誌訊息寫入到螢幕上,另外,也可以將這些日誌內容同時也要寫入到檔案內。

另外,從這裡也看到其他的問題,首先,就是這個類別沒有一個抽象介面存在,這將會造成未來需求擴增與調整的問題,這裡將會無法符合 SOLID 物件導向程式設計原則指引中的提到的OCP Open Closed Principle 開放封閉原則 ,造成這樣的問題,將會導致未來會需要修改到這個類別的程式碼,使得造成這個類別的程式碼,造成不穩定問題,這樣的做法,將會造成這個類別的程式碼,將會不易於維護。

另外,從 [Trace] 這個方法中,也看到這裡違反了 SPR Single Responsibility Principle ,因為在這個方法內同時具備了兩個責任,一個要寫入到螢幕上,一個要寫入日誌內容到檔案內,這也同時會使得這樣的程式碼會很難維護。

除了上述提到的兩個設計原則問題,還存在著更多的問題,其中一個就是執行緒安全,這個問題將會於當有多個執行緒需要寫入日誌內容的時候,造成問題,或者因為檔案的存取權限問題,造成無法寫入到檔案內,這些都是需要考量的問題。

因為,只是一個需求,可以先看看市面上都有著各種的 Log 套件,這些軟體都已經經過時間與社群的考驗,這樣的軟體一定比起自己開發起來的 Log 套件,要來的更加的穩定與可靠,這樣的軟體,也一定會有更多的功能,例如:可以將日誌內容寫入到資料庫內,或者是可以將日誌內容寫入到遠端的 Web API 內,這些都是自己開發起來的 Log 套件,所無法做到的事情。

不管如何,這裡還是把這個日誌類別與服務都設計出來了,使用起來也相當的方便,首先建立一個 Logger 類別,這裡使用 public static Logger logger = new Logger() 這個敘述,一旦取得這個物件,便可以使用這樣的方式 logger.Trace("我是追蹤:Trace") 將日誌內容寫入到螢幕與檔案上。