利用MQL进行MQL解析·进阶篇
(2/3)·正则搞不定的语义搜索与结构提取,靠自写解析器在MQL里拿下元数据
◍ 用类封装 MT5 文件逐字符读取
| 在 MT5 里做自定义文本解析,直接调 FileReadString 拿整行再拆字符,比一次读一个字节更稳。下面这段把文件名、句柄、行缓存、游标都收进 FileReader 类,构造时以 FILE_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_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,调试时很难一眼看出。
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_READ | FILE_TXT | FILE_ANSI | FILE_SHARE_READ | FILE_SHARE_WRITE,codepage 默认 CP_UTF8——在 MT5 里若你脚本含中文注释却用 ANSI 打开,可能乱码,调这个参数最直观。 |
|---|
析构里用 CLEAR 宏遍历 files.getSize() 逐个 delete 动态指针,漏掉这一步在 EA 反复加载时可能句柄泄漏。开 MT5 新建脚本粘入此类,编译后 Print 出 Including 顺序即可验证嵌套包含是否按预期压栈。
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_READ | FILE_TXT | FILE_ANSI 换成 FILE_UNICODE 就能跑 UTF-16 的 mq5 源文件。 |
|---|
一个容易踩的坑:controller.include() 默认不递归加载子包含(loadIncludes=false),若你想让小布类工具一次性展开全部依赖,构造 Preprocessor 时要把第二个参数置 true,否则 getChar 会在子包含边界提前返回 0。
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 单步时盯一下返回值类型。
{
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 可能让策略在实盘加载时静默失效。
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 开发属高风险活动,脚本仅用于本地代码处理,不构成任何交易建议。
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) {