从基础到中级:在 MetaTrader 5 沙箱中处理文件
引言
在上一篇文章“从基础到中级:对象事件(四)”中,我们解释并演示了如何开发一种方法,直接调整图表上某些对象的大小。由于此处的目标并非开发应用程序,而是通过示例展示如何使用 MQL5 来控制和管理 MetaTrader 5 中的流程,因此我认为目前没有必要深入探讨那篇文章中已经涵盖的内容。
我们可以大幅扩展这种实现方式,以实现更多功能。但本文的关注点并不在这里。因此,在你们决定从事其他编程相关活动之前,我们将先研究另一个同样重要的主题。
如往常一样,我们将继续讨论一个新话题,并探讨本文及后续文章将探讨的内容。
处理文件
文件处理这个话题可能会变得极其复杂,这取决于你具体想要做什么。尽管从大多数人习惯做的事或看到的情况来看,很多人认为这个话题简单甚至微不足道,但实际情况与初看上去可能大相径庭。
处理文件无疑是众多需要掌握的技能中最复杂、最困难的一项。这是因为一个文件可以包含任何内容,这是一个简单的事实。确实,就是这么简单。文件处理常常被低估,因为所涉及的信息通常以最简单、最易于访问的形式呈现。
然而,由于文件可以包含二进制信息 — 从数值或应用程序设置到以目标代码形式存在的程序本身,或者更常见的是以机器操作码的形式 — 因此,这一主题很快就会变得极其复杂也就不足为奇了。这主要是由于文件的内部格式造成的。
为了确保每个人都能理解并吸收我们能做什么以及如何实施,我们将从一开始就采用一个更简单的模型,不急不躁。然后,随着你对这些概念的吸收和理解,我们将能够继续探讨更详细和复杂的话题,同时始终努力使解释具有指导性。
首先,我们需要了解一些关于 MetaTrader 5 平台本身以及它如何处理文件的知识。确实,当完全使用纯 MQL5 编程时和使用部分功能在 MQL5 之外实现时,使用 MetaTrader 5 会有所不同。在真正开始处理文件之前,你需要理解这一点。
在纯 MQL5 编程以控制或操作 MetaTrader 5 时,我们仅限于使用所谓的沙箱。“沙箱”一词之所以在当今被广泛使用,得益于现代操作系统,它允许你创建一个相对安全的环境来运行未知应用程序。这是因为系统会让应用程序看起来像是可以执行任意操作,而任何更改都仅限于沙箱内,一旦沙箱关闭,这些更改就会被丢弃。
这确保了即使是潜在的恶意应用程序也不会损坏文件或导致操作系统故障。换言之,沙箱是一个安全区域,在这个区域之外,任何东西都不会受到影响。
MetaTrader 5 的工作原理类似。当使用纯 MQL5 时,我们有一个标准的沙箱。但是,如果我们使用第三方程序甚至 DLL 库,我们或许能够绕过或削弱 MetaTrader 5 沙箱的保护。这样就可以访问 MetaTrader 5 沙箱之外的磁盘或文件系统区域。“但是等等,那岂不是很好?毕竟,我们可以将数据写入设备或文件系统中我们通常无法访问的区域。”
亲爱的读者,现在要解释的是一件相当复杂的事情。访问沙箱外的任何内容都有其利弊。然而,就目前而言,你应该明白,这样做弊大于利。如果 MetaTrader 5 中运行的任何应用程序获得对沙箱外资源的访问权限,它可能会访问到本不应访问的内容。在许多情况下,这可能会威胁到你所有文件的完整性。
设想一下,你正在运行一个处理资金的应用程序。如果不加小心,你可能会在 MetaTrader 5 中运行一个应用程序,该应用程序最终会获取对你某些文件的访问权限,尽管由于沙箱限制,它原本根本不应该获取这些权限。然而,这种情况要真正发生,作为用户,你必须给予该应用程序相应的权限。
当您启用下图所示的选项时,通常会发生这种情况:

图 01
图 01 显示了可能允许应用程序在 MetaTrader 5 中运行时离开沙箱的关键点。甚至还有警告称,启用此选项可能会对系统造成危险。这正是因为在 MetaTrader 5 没有适当控制的情况下操作文件会存在风险。一旦脱离沙箱环境,MetaTrader 5 就无法再控制其运行。在这种情况下,控制权将掌握在应用程序手中,并且可能会调用操作系统的 DLL 库。虽然这看起来很安全,但恶意应用程序可以利用操作系统自身的 DLL 库造成灾难性破坏。所以要小心。
“好吧,但在这种情况下,沙箱是如何保护我们的呢?”亲爱的读者,这就是事情变得最有趣的地方。MetaTrader 5 的开发者们采取了极具前瞻性的方法,将所有内容都划分为彼此隔离的多个小单元,每个小单元都可以被视为一个独立的沙箱,与其他沙箱完全隔离。然而,实际情况并非完全如此。不过,随着你将所演示的方法和材料付诸实践,你会逐渐对这些过程有更深入的理解。
在初始阶段,我们将仅在沙箱内工作,采用尽可能简单的方式,避免不必要的复杂化。这样,我们可以在没有不必要压力或数据丢失风险的情况下创建和修改文件和目录。沙箱正是为此目的而设计的。
好的,那么让我们从一些非常简单且容易理解的内容开始。这是通过以下代码实现的。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. const string szText = "This file was created by a script written in MQL5."; 07. uchar info[]; 08. 09. StringToCharArray(szText, info); 10. 11. FileSave("Hello.txt", info); 12. } 13. //+------------------------------------------------------------------+
代码 01
这是一段非常简单直接的写文件代码。然而,当这段代码执行时,会发生一些可能会让你有点好奇的事情。但我们还是一步一步来吧。首先,代码 01 的第 06 行包含了要写入文件的消息。但是具体会写入哪个文件呢?“好吧,文件名是在第 11 行指定的,但是这个文件会保存在哪里呢?或者更准确地说,我应该在哪一个磁盘目录中寻找,或者期望在哪里找到这个文件呢?”嗯,这取决于某些条件。按照代码的编写方式,它会保存在标准的 MetaTrader 5 沙箱目录中。
目录结构中的确切位置将取决于您是在 MetaTrader 5 中运行代码,还是在测试代理中运行。无论如何,我们假设所有代码都是直接在 MetaTrader 5 中运行的。在这种情况下,文件将保存在 MQL5\Files 子文件夹中。这可以从下图看出:

图 02
现在,在图 02 中,你可以看到箭头指向一个奇怪的字符。这个字符是文件的一部分,因为它被保存在文件中。然而,如果我们查看第 06 行的消息,却看不到这个字符的任何踪迹。那么,它是从哪里来的呢?亲爱的读者,这个字符的出现是因为第 09 行。由于我们将字符串转换为字符数组,以便第 11 行的函数可以将字符串写入文件,因此字符串结束字符也成为了保存内容的一部分,因为它没有从转换为数组的字符串中移除。
好吧,但这会引起问题吗?原则上,如果你知道它的存在,这不会是个问题。然而,由于代码 01 非常简单,仅作为对文件系统的初步介绍,我认为现阶段无需担心。
因此,为了证明在此阶段,图 02 中展示的内容(文件中带有这个奇怪字符)对我们来说不是问题,我们将修改代码 01 来读取写入文件,然后将其内容打印到终端。这一点可以从下面的代码 02 中看出。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. #define szFileName "Hello.txt" 07. 08. const string szText = "This file was created by a script written in MQL5."; 09. 10. uchar W[], R[]; 11. 12. StringToCharArray(szText, W); 13. 14. FileSave(szFileName, W); 15. 16. FileLoad(szFileName, R); 17. 18. Print(CharArrayToString(R)); 19. } 20. //+------------------------------------------------------------------+
代码 02
因此,为了确保我们读取和写入的是同一个文件,我们使用代码 02 第 06 行的定义来指定所使用的文件名。由于前几行已经解释过,我们可以继续看第 16 行。在这里,我们读取文件的内容并将其放入一个数组中。在第 18 行,我们将这个字符数组转换为一个字符串,这样我们就可以打印出与第 08 行相同的消息。
为了验证这一点,当执行代码 02 时,我们可以看到以下结果:

图 03
换句话说,一切都按预期进行。这是最简单的情况,只需使用最少的代码,我们就可以保存并读取文件内容,而不会产生不必要的压力或复杂情况。
现在,让我们让任务变得稍微复杂一些,这样你就能开始理解与读写文件相关的某些细微差别了。首先,让我们更改沙箱。为此,我们只需将上面显示的代码替换为下面的代码:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. #define szFileName "Hello.txt" 07. 08. const string szText = "This file was created by a script written in MQL5."; 09. 10. uchar W[], R[]; 11. 12. StringToCharArray(szText, W); 13. 14. FileSave(szFileName, W, FILE_COMMON); 15. 16. FileLoad(szFileName, R, FILE_COMMON); 17. 18. Print(CharArrayToString(R)); 19. } 20. //+------------------------------------------------------------------+
代码 03
运行代码 03 时,您将看到与图 03 中相同的结果。但是,如果你尝试查找这个文件,你会发现它并非在 MQL5\Files 子文件夹中创建的;它将在其他地方创建。但是,在哪里呢?在讨论这一点之前,请注意,代码 02 和代码 03 之间的唯一区别正是第 14 行和第 16 行添加的额外参数,如代码 03 所示。
亲爱的读者,现在请注意。在第 14 行保存文件时,这个额外的参数告诉 MetaTrader 5 文件将存储在哪个沙箱中。然而,如果在第 16 行读取时未指定此参数,MetaTrader 5 将在标准沙箱(即 MQL5\Files 子文件夹)中查找该文件。因此,这可能会导致与预期不同的结果:读取或访问文件时出错,或在某些情况下,出现与我们预期看到的内容大相径庭的难以理解的内容。
因此,为了确保整个流程正常运作,读取和写入时的文件存储位置必须一致。如果在写入时指定一个沙箱,而在读取时指定另一个,或者反之,都是无意义的。期望 MetaTrader 5 理解您的意图或您试图访问的文件也是没有意义的。它没有义务也没有兴趣去这样做。
尽管在某些情况下,它可能会找到正确的文件,但你不应依赖于此。最有可能的是,你最终会在错误的位置写入或读取文件。这不仅可能无法满足你对所需信息类型的期望,还可能会因为你的错误而损坏已存在于文件中的数据,这些数据会被新数据覆盖。
现在,我们终于可以尝试确切地理解代码 03 的第 14 行和第 16 行的这个额外参数指向哪个沙箱了。这样一来,如果您需要,就可以使用任何其他编辑器找到该文件。FILE_COMMON 参数告诉 MetaTrader 5 使用 \Terminal\Common\Files 子文件夹。这很简单。“等一下,让我理解一下这里发生了什么。如果 FILE_COMMON 参数不存在,我们指向 MQL5\Files 沙箱,如果存在,我们指向 \Terminal\Common\Files 沙箱。就这些吗?”是的,亲爱的读者,就是这样。“但我以为,为了让这个更改生效,我们需要指定一个目录。从某种程度上说,这让我有点困惑。”
事实上,亲爱的读者,这并不复杂。之所以看起来复杂,正是因为你还不了解沙盒的概念。但我们不要着急,这样你就能充分理解这个概念,并在各种情况下加以应用。如果你想在许多不同情况下充分利用 MetaTrader 5,正确理解这一过程非常重要。
了解沙箱
要正确理解沙箱的概念,我们需要了解文件系统的工作原理。我将简单地进行一个简短的解释,以便让那些还不熟悉这些术语的人也能明白后面会用到的术语。
你需要理解的第一个术语是根的概念,即根目录。根目录可以想象成目录树中的起始点。树状结构的概念源于这样一个事实:树的每个分支都相当于同一目录下的一个子文件夹。这些分支上的每个叶子都相当于一个文件。因此,简单来说,根就是初始目录。每个目录树都有一个唯一的根。从这个根开始,我们首先有一个分支。该分支上的任何叶子都是与根直接关联的文件。一棵树与另一棵树之间没有直接层级关系。换句话说,没有哪棵树会有两个不同的根。并且,所有树都立足于同一片“地面”之上。
理解了这一点后,我们可以将沙箱在 MetaTrader 5 中的组织方式做一个直观的类比。如下图所示:

图 04
图 04 显示了两个沙箱,与我们在上一节中使用的沙箱完全相同。请注意,基础文件夹是 MetaQuotes\Terminal 子文件夹。然而,它并非根目录,而是这些树状结构放置的基础位置。
现在请注意以下几点:1 号树有一个根目录,在本例中,该根目录位于 Common\Files 文件夹中。2 号树则有另一个根目录,位于 MQL5\Files 文件夹中。请注意,如果不先经过底层基础,我们无法直接从一个树转移到另一个树。通常,从一个分支转移到另一个分支需要经过底层基础。然而,在这里不能使用同样的转移方式,因为一个树是一个沙箱,而另一个树则是不同的沙箱。
为了让内容更加清晰,同时给你一个机会去研究并尝试理解这一切是如何实现的,我们将修改上一节中讨论的代码,以便我们能在分支中添加目录。这操作起来非常简单,但结果却相当有趣,它们可能有助于你理解沙箱在实际中的工作原理。这是保持一切可控的必要条件。以下是我们一开始会用到的代码:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. #define szFileName "Hello.txt" 07. #define macro_Save(A) Print("Attempting to write in [",A,"] resulted in: ", FileSave(A, buff)); 08. //+----------------+ 09. const string szText = "This file was created by a script written in MQL5.\nThis would be the contents of file number #%02d."; 10. //+----------------+ 11. uchar buff[]; 12. ushort counter = 1; 13. //+----------------+ 14. StringToCharArray(StringFormat(szText, counter++), buff); 15. macro_Save(szFileName); 16. //+----------------+ 17. StringToCharArray(StringFormat(szText, counter++), buff); 18. macro_Save(".\\Test\\" + szFileName); 19. //+----------------+ 20. StringToCharArray(StringFormat(szText, counter++), buff); 21. macro_Save(".\\Temp\\" + szFileName); 22. //+----------------+ 23. StringToCharArray(StringFormat(szText, counter++), buff); 24. macro_Save("..\\SandBox\\" + szFileName); 25. } 26. //+------------------------------------------------------------------+
代码 04
在代码 04 中,我们将尝试向 MQL5\Files 沙箱中的不同分支写入四个不同的文件。然而,在其中一次尝试中,我们将尝试离开沙箱并将数据写入 MQL5 目录。让我们逐步分析这一过程,以便您理解这些尝试是如何进行的,以及如何解读其结果。
首先,在 MetaTrader 5 中运行代码 04 时,我们将在终端中看到类似于下图所示的结果:

图 05
请注意,在三种情况下,文件内容被正确写入。尽管我们没有使用任何其他调用创建任何目录,但在执行代码 04 之前,如果两个新目录不存在,MetaTrader 5 检测到需要创建这两个新目录。
现在,让我们来看看文件中写了哪些内容,以及它们位于何处。这可以从以下图片中看到:

图 06

图 07

图 08
请注意,在这三张图片中,我们用绿色标记了位置和文件名。但仔细查看每张图片的内容,你会发现它们几乎一模一样,除了第二行的数值不同。这个数值的存在只是为了区分文件内容,并表明我们可以用相同的方式写入不同的文件。
不过,让我们回到代码 04 本身。请注意,在第 15、18、21 和 24 行,我们只更改了正在使用的目录的名称。但是,如图 05 所示,在尝试创建第四个文件时发生错误。为什么呢?原因是我们试图在当前沙箱之外创建内容。换句话说,我们正在尝试转移到沙箱根目录之外的分支。当我们尝试这样做时,MetaTrader 5 会将其解释为错误,并阻止任何磁盘读写操作。
我明白,这对许多人来说可能有点奇怪。第18、21 和 24 行之间的唯一区别是第 24 行多了一个点,但它告诉系统朝与当前位置不同的方向移动。然而,由于我们处于沙箱中,并且通过 MQL5 使用 MetaTrader 5 调用,所以我们不允许离开沙箱。如果我们使用操作系统本身的写入函数,则不会发生这种情况。在这种情况下,操作系统不会知道 MetaTrader 5 创建和维护的沙箱。因此,在沙箱之外,可以对任何文件进行写入、读取甚至执行任何操作。
鉴于我不想让这可能是你首次接触的 MetaTrader 5 文件系统变得过于复杂,我们将对代码进行最后的修改,以便你可以测试读取我们应用程序写入的一些文件。
为此,我们将使用以下代码:
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. #define szFileName "Hello.txt" 07. #define macro_Save(A) Print("Attempting to write in [",A,"] resulted in: ", FileSave(A, buff)); 08. #define macro_Load(A) Print("Attempt to read the [",A,"] file resulted in: ", FileLoad(A, buff) != INVALID_HANDLE); 09. //+----------------+ 10. const string szText = "This file was created by a script written in MQL5.\nThis would be the contents of file number #%02d."; 11. //+----------------+ 12. uchar buff[]; 13. ushort counter = 1; 14. //+----------------+ 15. StringToCharArray(StringFormat(szText, counter++), buff); 16. macro_Save(szFileName); 17. //+----------------+ 18. StringToCharArray(StringFormat(szText, counter++), buff); 19. macro_Save(".\\Test\\" + szFileName); 20. //+----------------+ 21. StringToCharArray(StringFormat(szText, counter++), buff); 22. macro_Save(".\\Temp\\" + szFileName); 23. //+----------------+ 24. StringToCharArray(StringFormat(szText, counter++), buff); 25. macro_Save("..\\SandBox\\" + szFileName); 26. //+----------------+ 27. Print("+----------------+"); 28. ArrayResize(buff, 0); 29. macro_Load(".\\Test\\" + szFileName); 30. if (buff.Size() > 0) 31. Print("File content:\n", CharArrayToString(buff)); 32. Print("+----------------+"); 33. ArrayResize(buff, 0); 34. macro_Load(".\\Test 1\\" + szFileName); 35. if (buff.Size() > 0) 36. Print("File content:\n", CharArrayToString(buff)); 37. } 38. //+------------------------------------------------------------------+
代码 05
代码 05 将帮助您了解沙箱系统的工作原理。至少,这是它最初的目的。其中,我们尝试对特定位置的任何文件进行写入和读取操作。虽然这可能看起来令人困惑,但如果你从文章开头就理解了这些内容,一切都会变得简单,如图所示。

图 09
要理解所有这些信息,你需要了解我们在代码 04 中实现了什么以及我们获得了什么结果。鉴于执行代码 04 的结果是图 05,并且每个文件的内容可以在图 06、07 和 08 中看到,因此在代码 05 中,我们只需要考虑几个关键点。实际上,我们只需要解释从第 26 行开始发生的情况。那就是读取或尝试读取文件开始的地方。
如您所见,在第 29 行和第 34 行,我们运行了第 08 行的宏。这里重要的细节是,当执行第 08 行的宏时,终端会输出相应提示信息,指示我们正在尝试读取哪个文件以及结果是否成功。到目前为止,一切都很正常,也很容易理解。但为什么这段代码中使用了第 28 行和第 33 行呢?原因正是为了清空数据数组。由于在尝试读取时可能会出现错误,我们不希望保留任何之前的数据。因此,当执行第 30 行和第 35 行的检查时,只有当数组实际包含任何数据时,我们才会在终端打印一条消息。
鉴于第 34 行我们将尝试读取一个原则上不应该存在的文件,终端将打印一条错误消息,并且第 35 行的检查也将失败。
首先,请记住代码 05 中指定的文件和目录最好都不存在。 这样,结果就会正确,并且会正确显示,如图 09 所示。如果你得到的结果不同,请尝试更改代码 05 中的文件名甚至目录。目标是生成尝试读取不存在文件时发生的错误,以及尝试在指定目录中读取文件时的结果。
还可以尝试修改代码,使用本文中讨论的另一个沙箱,即通过添加额外的 FILE_COMMON 参数。这将帮助您准确理解图 04 中显示的内容,其中展示了两个具有不同根的树。
最后的想法
在本文中,我们初步研究了 MetaTrader 5 的文件系统,以了解沙箱的工作原理,并避免意外修改操作系统目录树中不适当的文件夹。尽管从初学者的角度来看,沙箱的初始使用显得较为复杂,但了解如何使用沙箱对于未来的项目而言是绝对必要的。如果你真的想积极使用文件来存储数据,甚至创建可长期保存并逐步调整的配置,这一点尤为重要。
亲爱的读者,请耐心仔细地学习这些材料,因为这一步必须正确完成。如果你不了解沙箱的工作原理,将来解决某些任务将会非常困难。
| MQ5 文件 | 描述 |
|---|---|
| 代码 01 | 文件访问演示 |
| 代码 02 | 文件访问演示 |
| 代码 03 | 文件访问演示 |
| 代码 04 | 文件访问演示 |
| 代码 05 | 文件访问演示 |
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/16167
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
您应该了解的MQL5向导技巧(第七十九部分):在监督学习中使用鳄鱼震荡指标和A/D震荡指标
新手在交易中的10个基本错误
在线性自回归模型残差上训练非线性 U-Transformer