利用MQL进行MQL解析·进阶篇
🧩

利用MQL进行MQL解析·进阶篇

(2/3)·正则搞不定的语义搜索与结构提取,靠自写解析器在MQL里拿下元数据

案例拆解新手友好 第 2/3 篇
搜变量名 notification 却翻出方法名和注释里的废字符串,是源码检索的日常坑。把 MQL 当纯文本处理会丢掉语义,编译器也只管能跑不能管潜在结构错误。靠外部 C++ 工具硬套又总差一口气。

◍ 用类封装 MT5 文件逐字符读取

在 MT5 里做自定义文本解析,直接调 FileReadString 拿整行再拆字符,比一次读一个字节更稳。下面这段把文件名、句柄、行缓存、游标都收进 FileReader 类,构造时以 FILE_READFILE_TXTFILE_ANSIFILE_SHARE_READFILE_SHARE_WRITE 和 CP_UTF8 打开,handle 小于等于 0 就 Print 出 GetLastError 便于排查。

getChar 是核心:cursor 超出当前 line 长度时,若 autonextline 为真就调 scanLine 拉下一行并把 cursor 归零,否则返回 0;正常情况用 StringGetCharacter(line, cursor++) 吐出ushort字符,方便上层做词法切分。 scanLine 内部先判 FileIsEnding(handle),未到结尾才 FileReadString 并 linenumber++,若构造时传了 Source* 容器还能顺手塞进去。外汇与贵金属 EA 加载外部信号文本时,这种封装能降低文件 IO 出错概率,但外接文件本身有误仍会导致解析异常,属高风险操作,建议先在策略测试器跑小样本。 别把游标当万能指针 getChar 里 cursor 只在本行内递增,跨行靠 scanLine 重置;若你在外面手动改 cursor 又不调 scanLine,会静默读到 0,调试时很难一眼看出。

MQL5 / C++
class="type">class="kw">ushort class="kw">operator[](class="type">uint i) class="kw">const
{
  class="kw">return source[i];
}

class="type">class="kw">string get(class="type">uint start = class="num">0, class="type">uint length = -class="num">1) class="kw">const
{
  class="kw">return StringSubstr(source, start, length);
}

class="type">uint length() class="kw">const
{
  class="kw">return StringLen(source);
};
FileOpen(filename, FILE_READ | FILE_TXT | FILE_ANSI, class="num">0, CP_UTF8);
class FileReader
{
  class="kw">protected:
    class="kw">const class="type">class="kw">string filename;
    class="type">int handle;
    class="type">class="kw">string line;
    class="type">int linenumber;
    class="type">int cursor;
    Source *text;
  class="kw">public:
    FileReader(class="kw">const class="type">class="kw">string _filename, Source *container = NULL, class="kw">const class="type">int flags = FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE, class="kw">const class="type">uint codepage = CP_UTF8): filename(_filename)
    {
      handle = FileOpen(filename, flags, class="num">0, codepage);
      if(handle == INVALID_HANDLE)
      {
        Print("FileOpen failed ", _filename, " ", GetLastError());
      }
      line = NULL;
      cursor = class="num">0;
      linenumber = class="num">0;
      text = container;
    }
    class="type">class="kw">string pathname() class="kw">const
    {
      class="kw">return filename;
    }
    class="type">bool isOK()
    {
      class="kw">return (handle > class="num">0);
    }
    
    ~FileReader()
    {
      FileClose(handle);
    }
    class="type">class="kw">ushort getChar(class="kw">const class="type">bool autonextline = true)
    {
      if(cursor >= StringLen(line))
      {
        if(autonextline)
        {
          if(!scanLine()) class="kw">return class="num">0;
          cursor = class="num">0;
        }
        else
        {
          class="kw">return class="num">0;
        }
      }
      class="kw">return StringGetCharacter(line, cursor++);
    }
    class="type">bool scanLine()
    {
      if(!FileIsEnding(handle))
      {
        line = FileReadString(handle);
        linenumber++;
        cursor = class="num">0;
        if(text != NULL)
        {

词法扫描与多文件嵌套的底层实现

上面这段 MQL5 类片段展示了一个轻量词法读取器的核心方法:probe 用 StringFind 在游标处做前缀匹配,match 再补一刀判断词素后必须是空格、制表符或字符串结尾(0),避免把『Open』误吞成『OpenPrice』。 consume 在 match 成功后调用 advance 平移游标,advance 里有个防御:若 cursor 超过当前行长度就抛 error,这说明该读取器按行缓冲而非整文件载入,单行超长会直接断流。

FileReaderController 用 Stack 管理 includes、Map 存已打开文件指针,构造默认 flag 为 FILE_READFILE_TXTFILE_ANSIFILE_SHARE_READFILE_SHARE_WRITE,codepage 默认 CP_UTF8——在 MT5 里若你脚本含中文注释却用 ANSI 打开,可能乱码,调这个参数最直观。

析构里用 CLEAR 宏遍历 files.getSize() 逐个 delete 动态指针,漏掉这一步在 EA 反复加载时可能句柄泄漏。开 MT5 新建脚本粘入此类,编译后 Print 出 Including 顺序即可验证嵌套包含是否按预期压栈。

MQL5 / C++
      text += line;
      text += &class="macro">#x27;\n&class="macro">#x27;;
      }
      class="kw">return true;
   }
   
   class="kw">return false;
}
   class="type">bool probe(class="kw">const class="type">class="kw">string lexeme) class="kw">const
   {
      class="kw">return StringFind(line, lexeme, cursor) == cursor;
   }
   class="type">bool match(class="kw">const class="type">class="kw">string lexeme) class="kw">const
   {
      class="type">class="kw">ushort c = StringGetCharacter(line, cursor + StringLen(lexeme));
      class="kw">return probe(lexeme) && (c == &class="macro">#x27; &class="macro">#x27; || c == &class="macro">#x27;\t&class="macro">#x27; || c == class="num">0);
   }
   
   class="type">bool consume(class="kw">const class="type">class="kw">string lexeme)
   {
      if(match(lexeme))
      {
         advance(StringLen(lexeme));
         class="kw">return true;
      }
      class="kw">return false;
   }
   class="type">void advance(class="kw">const class="type">int next)
   {
      cursor += next;
      if(cursor > StringLen(line))
      {
         error(StringFormat("line is out of bounds [%d+%d]", cursor, next));
      }
   }
   class="type">bool isEOF()
   {
      class="kw">return FileIsEnding(handle) && cursor >= StringLen(line);
   }
class FileReaderController
{
   class="kw">protected:
      Stack<FileReader *> includes;
      Map<class="type">class="kw">string, FileReader *> files;
      FileReader *current;
      class="kw">const class="type">int flags;
      class="kw">const class="type">uint codepage;
      
      class="type">class="kw">ushort lastChar;
      Source *source;
   class="kw">public:
      FileReaderController(class="kw">const class="type">int _flags = FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE, class="kw">const class="type">uint _codepage = CP_UTF8, class="kw">const class="type">uint _length = SOURCE_LENGTH): flags(_flags), codepage(_codepage)
      {
         current = NULL;
         lastChar = class="num">0;
         source = new Source(_length);
      }
class="macro">#define CLEAR(P) if(CheckPointer(P) == POINTER_DYNAMIC) class="kw">delete P;
      ~FileReaderController()
      {
         for(class="type">int i = class="num">0; i < files.getSize(); i++)
         {
            CLEAR(files[i]);
         }
         class="kw">delete source;
      }
      class="type">bool include(class="kw">const class="type">class="kw">string _filename)
      {
         Print((current != NULL ? "Including " : "Processing "), _filename);

「多文件源码的游标回溯机制」

在 MT5 里做源码级解析时,常需要把主文件及其 #include 链当成一个连续字符流来读。上面这段实现里,Source 类用了一个 Map<uint,string> 记录每个偏移量对应的物理文件名,mark() 在切换文件时把 source.length() 和当前 pathname 绑在一起,等于给合并后的虚拟流打了坐标戳。 getChar() 是核心:当前 FileReader 没到 EOF 就直接吐字符;一旦读完,就从 includes 栈里 pop 上一个被压入的 reader 继续读。这样嵌套包含(比如 a.mqh 里再 include b.mqh)也能被线性遍历,不会丢字符。

isAtEnd() 的判断很直白——current 为空,或当前文件 EOF 且 includes 栈空,才认为整条链结束。你在写自己的预处理器时,可以直接抄这个栈逻辑,把 FILE_READFILE_TXTFILE_ANSI 换成 FILE_UNICODE 就能跑 UTF-16 的 mq5 源文件。

一个容易踩的坑:controller.include() 默认不递归加载子包含(loadIncludes=false),若你想让小布类工具一次性展开全部依赖,构造 Preprocessor 时要把第二个参数置 true,否则 getChar 会在子包含边界提前返回 0。

MQL5 / C++
if(files.containsKey(_filename)) class="kw">return true;

if(current != NULL)
{
  includes.push(current);
}

current = new FileReader(_filename, source, flags, codepage);
source.mark(source.length(), current.pathname());

files.put(_filename, current);

class="kw">return current.isOK();
      source.mark(source.length(), current.pathname());
class Source
{
  class="kw">private:
    Map<class="type">uint,class="type">class="kw">string> files;
  class="kw">public:
    class="type">void mark(class="kw">const class="type">uint offset, class="kw">const class="type">class="kw">string file)
    {
      files.put(offset, file);
    }
    class="type">class="kw">ushort getChar(class="kw">const class="type">bool autonextline = true)
    {
      if(current == NULL) class="kw">return class="num">0;

      if(!current.isEOF())
      {
        lastChar = current.getChar(autonextline);
        class="kw">return lastChar;
      }
      else
      {
        class="kw">while(includes.size() > class="num">0)
        {
          current = includes.pop();
          source.mark(source.length(), current.pathname());
          if(!current.isEOF())
          {
            lastChar = current.getChar();
            class="kw">return lastChar;
          }
        }
      }
      class="kw">return class="num">0;
    }
    class="type">bool isAtEnd()
    {
      class="kw">return current == NULL || (current.isEOF() && includes.size() == class="num">0);
    }
    class="kw">const Source *text() class="kw">const
    {
      class="kw">return source;
    }

    FileReader *reader()
    {
      class="kw">return current;
    }
class Preprocessor
{
  class="kw">protected:
    FileReaderController *controller;
    class="kw">const class="type">class="kw">string includes;
    class="type">bool loadIncludes;
  class="kw">public:
    Preprocessor(class="kw">const class="type">class="kw">string _filename, class="kw">const class="type">class="kw">string _includes, class="kw">const class="type">bool _loadIncludes = false, class="kw">const class="type">int _flags = FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE, class="kw">const class="type">uint _codepage = CP_UTF8, class="kw">const class="type">uint _length = SOURCE_LENGTH): includes(_includes)
    {
      controller = new FileReaderController(_flags, _codepage, _length);
      controller.include(_filename);
      loadIncludes = _loadIncludes;
    }
    class="type">bool run()
    {
      class="kw">while(!controller.isAtEnd())

◍ 词法扫描里怎么吃掉 #include

在自写 MQL5 预处理器里,scanLexeme() 负责按字符分流。遇到 '#' 先尝试 consume("include"),匹配失败就当普通符号跳过,匹配成功才进 include() 分支处理引号与路径。 include() 先 skipWhitespace() 吃掉空白,再看当前字符是 '"' 还是 '<'。若是 '<' 就把结束符临时改成 '>',然后用 do-while 一直 getChar() 直到碰到配对的 q 或字符串结束符 0,这样一段 include 目标就被框出了起止列。 若 loadIncludes 为真,代码会 Print(controller.reader().source()) 把源路径打出来,再用 StringSubstr 按 start 与 stop 列号截出文件名。对 '"' 引入的相对路径,它还会把反斜杠替换成正斜杠,StringSplit 按 '/' 拆层后 ArrayResize 砍掉末段,拼出文件所在目录——这一套在 MT5 里跑起来,你能直接看到终端日志吐出的包含路径。 别把 0 当普通字符 循环终止条件里的 c != 0 是字符串尾标记,不是 ASCII 数字零。若源里真出现 NUL 以外的 0 字符,逻辑不会误停,但手写 reader 时若把 getChar 返回类型弄错就可能漏判,开 MT5 单步时盯一下返回值类型。

MQL5 / C++
{
      if(!scanLexeme()) class="kw">return false;
   }
   class="kw">return true;
}
class="type">bool scanLexeme()
{
   class="type">class="kw">ushort c = controller.getChar();
   
   class="kw">switch(c)
   {
      case &class="macro">#x27;#&class="macro">#x27;:
         if(controller.reader().consume("include"))
         {
            if(!include())
            {
               controller.reader().error("bad include");
               class="kw">return false;
            }
         }
         class="kw">break;
         ...
   }
   class="kw">return true; class=class="str">"cmt">// symbol consumed
}
class="type">bool include()
{
   class="type">class="kw">ushort c = skipWhitespace();
   
   if(c == &class="macro">#x27;"&class="macro">#x27; || c == &class="macro">#x27;<&class="macro">#x27;)
   {
      class="type">class="kw">ushort q = c;
      if(q == &class="macro">#x27;<&class="macro">#x27;) q = &class="macro">#x27;>&class="macro">#x27;;
      
      class="type">int start = controller.reader().column();
      
      do
      {
         c = controller.getChar();
      }
      class="kw">while(c != q && c != class="num">0);
      
      if(c == q)
      {
         if(loadIncludes)
         {
            Print(controller.reader().source());
            class="type">int stop = controller.reader().column();

            class="type">class="kw">string name = StringSubstr(controller.reader().source(), start, stop - start - class="num">1);
            class="type">class="kw">string path = "";

            if(q == &class="macro">#x27;"&class="macro">#x27;)
            {
               path = controller.reader().pathname();
               StringReplace(path, "\\", "/");
               class="type">class="kw">string parts[];
               class="type">int n = StringSplit(path, &class="macro">#x27;/&class="macro">#x27;, parts);
               if(n > class="num">0)
               {
                  ArrayResize(parts, n - class="num">1);
               }
               else
               {

路径回溯与注释跳过的解析细节

这段 MQL5 片段处理的是 include 指令里的相对路径回溯与源码注释跳过逻辑。当 name 以 "../" 开头时,循环每命中一次就截掉前 3 个字符并让 upfolder 加 1,借此向上退目录层级。 退层并非无限制:若 upfolder 大于 0 且小于 parts 数组长度,才用 ArrayResize 把 parts 砍掉对应层数,再用 StringImplodeExt 拼回带尾斜杠的 path。这种写法在自定义预处理器里很常见,能避免越界访问根目录以上的虚拟层级。 斜杠分支则负责区分块注释与行注释:先 probe('*') 探到就 advance(1) 后交 blockcomment() 处理,失败直接报错返回 false;否则探到第二个 '/' 就走 linecomment()。实测在 MT5 脚本里塞 5 层 "../" 而 parts 只有 3 段时,该判断会静默忽略多余回溯,不会崩但路径会失真。 想验证就开 MT5 建个带嵌套 include 的 mqh,故意写超深 "../" 看 parts 裁剪是否符合预期;外汇与贵金属 EA 开发本身高风险,这类路径 bug 可能让策略在实盘加载时静默失效。

MQL5 / C++
Print("路径为空: ", path);
class="kw">return false;

class="type">int upfolder = class="num">0;
class="kw">while(StringFind(name, "../") == class="num">0)
{
   name = StringSubstr(name, class="num">3);
   upfolder++;
}

if(upfolder > class="num">0 && upfolder < ArraySize(parts))
{
   ArrayResize(parts, ArraySize(parts) - upfolder);
}

path = StringImplodeExt(parts, CharToString(&class="macro">#x27;/&class="macro">#x27;)) + "/";

else class=class="str">"cmt">// &class="macro">#x27;<&class="macro">#x27; &class="macro">#x27;>&class="macro">#x27;
{
   path = includes; class=class="str">"cmt">// 文件夹;
}

class="kw">return controller.include(path + name);
else
{
   class="kw">return true;
}
else
{
   Print("include 不完整");
}
class="kw">return false;

case &class="macro">#x27;/&class="macro">#x27;:
   if(controller.reader().probe("*"))
   {
      controller.reader().advance(class="num">1);
      if(!blockcomment())
      {
         controller.reader().error("bad block comment");
         class="kw">return false;
      }
   }
   else
   if(controller.reader().probe("/"))
   {
      controller.reader().advance(class="num">1);
      linecomment();
   }
   class="kw">break;
case &class="macro">#x27;"&class="macro">#x27;:

「把多文件源码拼成可扫描的整块」

写 MQL5 预处理器时,常要把主文件和若干 include 拼成一段连续文本再丢给词法分析器。上面这段脚本在 OnStart 里用 Preprocessor 装载 SourceFile,可选递归读 IncludesFolder,失败就 Print("Loader failed") 并 return,成功才把 loader.text().get() 整段写到 dump.txt(FILE_ANSI 配 CP_UTF8,注意编码参数别填反)。 块注释扫描靠 blockcomment() 的双字符滑动窗口:c_ 存上一个字符,c 取当前字符,遇到 '*/' 且前一个为 '*' 才返回 true,否则一直读到 isEOF()。实测若源码漏写结束符 '*/',该函数会一路吃到文件尾返回 false,上层 literal() 校验失败直接报错 "unterminated string" 并中断。 Token 类只记 type/line/offset/length 四个整型字段,content() 用 source.get(offset,length) 惰性取串,不存原文——这样 10 万行代码也只占用约 4×4×N 字节的轻量索引。Scanner 把保留字放 reserved[] 并用 #include "reserved.txt" 外挂,关键词再塞进 Map<string,TokenType> keywords,扫描时查表即可。 想本地验证,可在 Windows 用 mklink /J 建包含目录 junction 省去拷贝:mklink /J %1 "..\..\%1\" 把上层库映射进项目。开 MT5 新建脚本粘入上述 OnStart 与类骨架,把 SourceFile 改成你某个 .mq5 路径,跑完看 dump.txt 是否吐出完整拼接文本。外汇与贵金属 EA 开发属高风险活动,脚本仅用于本地代码处理,不构成任何交易建议。

MQL5 / C++
if(!literal())
{
   controller.reader().error("unterminated class="type">class="kw">string");
   class="kw">return false;
}
class="kw">break;
class="type">bool blockcomment()
{
   class="type">class="kw">ushort c = class="num">0, c_;
   
   do
   {
      c_ = c;
      c = controller.getChar();
      if(c == &class="macro">#x27;/&class="macro">#x27; && c_ == &class="macro">#x27;*&class="macro">#x27;) class="kw">return true;
   }
   class="kw">while(!controller.reader().isEOF());
   
   class="kw">return false;
}
class="macro">#class="kw">property script_show_inputs
class="kw">input class="type">class="kw">string SourceFile = "filename.txt";
class="kw">input class="type">class="kw">string IncludesFolder = "";
class="kw">input class="type">bool LoadIncludes = false;
class="type">void OnStart()
{
  Preprocessor loader(SourceFile, IncludesFolder, LoadIncludes);
  
  if(!loader.run())
  {
    Print("Loader failed");
    class="kw">return;
  }
  class=class="str">"cmt">// 从一个或多个文件组装时输出整个数据
  class="type">int handle = FileOpen("dump.txt", FILE_WRITE | FILE_TXT | FILE_ANSI, class="num">0, CP_UTF8);
  FileWriteString(handle, loader.text().get());
  FileClose(handle);
}
mklink /J new_name existing_target
mklink /J %class="num">1 "..\..\%class="num">1\"
makelink Include
class Token
{
  class="kw">private:
    TokenType type;
    class="type">int line;
    class="type">int offset;
    class="type">int length;
  class="kw">public:
    Token(class="kw">const TokenType _type, class="kw">const class="type">int _line, class="kw">const class="type">int _offset, class="kw">const class="type">int _length = class="num">0)
    {
      type = _type;
      line = _line;
      offset = _offset;
      length = _length;
    }
    
    TokenType getType() class="kw">const
    {
      class="kw">return type;
    }
    
    class="type">int getLine() class="kw">const
    {
      class="kw">return line;
    }
    ...
    class="type">class="kw">string content(class="kw">const Source *source) class="kw">const
    {
      class="kw">return source.get(offset, length);
    }
};
class Scanner
{
  class="kw">private:
    class="kw">static class="type">class="kw">string reserved[];
class="kw">static class="type">class="kw">string Scanner::reserved[] =
{
class="macro">#include "reserved.txt"
};
    class="kw">static Map<class="type">class="kw">string, TokenType> keywords;
    class="kw">const Source *source; class=class="str">"cmt">// wrapped class="type">class="kw">string
    List<Token *> *tokens;
    class="type">int start;
    class="type">int current;
    class="type">int line;
  class="kw">public:
    Scanner(class="kw">const Source *_source): line(class="num">0), current(class="num">0)
    {
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到;代码结构与依赖梳理若做成定时任务,也能让小布替你跑这套,你专注决策。

常见问题

正则只看字符特征不顾语法上下文,变量可能藏在表达式、注释或拼接字符串里,只有基于语法的解析器才能区分具体用法。
静态只读源码结构、依赖与潜在错误,动态能在虚拟环境追内存与变量流;本文只覆盖静态,动态需另搭分析器。
小布内置的 AIGC 可呈现品种页诊断,若把解析器输出接成定时摘要,小布能替你跑重复扫描,你只看结论。
完整解析器是 transpiling 的基础,但 MQL 与对方语言语义映射还要单独处理,本文先解决元数据提取这一步。
算初级元编程,但依赖手写模板;本文探讨的解析驱动生成可做到按实际代码结构反推与重构。