使用CSS选择器从HTML页面提取结构化数据·综合运用
🌐

使用CSS选择器从HTML页面提取结构化数据·综合运用

(3/3)·从DOM解析到WebDataExtractor,把网页经济日历与交易报告变成EA可读数组

实战向 第 3/3 篇
很多交易者卡在第三方网页没有开放API,只能手动抄经济日历。其实用CSS选择器在MQL5里直接抽表行,比想象中轻。本篇把前两层铺垫的解析器用法一次性串成可跑的实战链路。

◍ DOM 节点的选择器匹配逻辑

这段 MQL5 片段实现了一个类 DOM 树节点的递归查找与子选择器匹配。find 方法会遍历当前节点的 children 数组,只要任意一个子节点命中就置 found 为 true,递归深度由调用栈自然控制。 match 函数按 SubSelectorArray 逐个校验:type==0 时先比标签名,通配符 "*" 直接放行,否则用 StringCompare 做严格字符串比对,不等就判失败。 伪类分支里,firstChild 要求 parent.getChildIndex 返回 0,lastChild 要求等于 getChildrenCount()-1,nthChild 把 param 转整型后要求索引等于 x-1(外部从 1 计数)。这些边界若写错,外汇 EA 里做界面元素定位时可能漏选或误选控件。 开 MT5 把这段代码塞进你自己的 CTreeNode 类,改 u[i].value 打几个日志,能直接验证匹配分支是否按预期走。

MQL5 / C++
      }
      }
    }
    for(i = class="num">0; i < ArraySize(children); i++)
    {
      found = children[i].find(op, selectors, output) || found;
    }
    }
    class="kw">return found;
  }
  class="type">bool match(class="kw">const SubSelectorArray *u)
  {
    class="type">bool matched = true;
    class="type">int i, n = u.size();
    for(i = class="num">0; i < n && matched; i++)
    {
      if(u[i].type == class="num">0) class=class="str">"cmt">// 标签名称和伪类
      {
        if(u[i].value == "*")
        {
          class=class="str">"cmt">// 任意标签
        }
        else
        if(StringCompare(name, u[i].value) != class="num">0)
        {
          matched = class="kw">false;
        }
        else
        if(u[i].modifier == PseudoClassModifier::firstChild)
        {
          if(parent != NULL && parent.getChildIndex(&this) != class="num">0)
          {
            matched = class="kw">false;
          }
        }
        else
        if(u[i].modifier == PseudoClassModifier::lastChild)
        {
          if(parent != NULL && parent.getChildIndex(&this) != parent.getChildrenCount() - class="num">1)
          {
            matched = class="kw">false;
          }
        }
        else
        if(u[i].modifier == PseudoClassModifier::nthChild)
        {
          class="type">int x = (class="type">int)StringToInteger(u[i].param);
          if(parent != NULL && parent.getChildIndex(&this) != x - class="num">1) class=class="str">"cmt">// children are counted starting from class="num">1
          {
            matched = class="kw">false;
          }
        }
        else
        if(u[i].modifier == PseudoClassModifier::nthLastChild)
        {
          class="type">int x = (class="type">int)StringToInteger(u[i].param);

「选择器匹配里的兄弟序号与属性判定」

这段逻辑处理的是 DOM 风格选择器在 MQL5 结构体树里的命中判断。当父节点存在时,先用 getChildrenCount() - getChildIndex() - 1 算出当前节点在兄弟中的倒序位置,若不等于目标序号 x-1,直接判 matched=false,也就是只认倒数第 x 个兄弟。 对于点选择器 .class,代码会检查属性表里有没有 class 键;有则取出容器值,用 StringFind(" "+值+" ", " "+目标+" ") 做带空格的包含判断,返回 -1 就失配。没有 class 键同样失配,这能避免子串误命中。 井号 #id 走精确比对:StringCompare 不等就失败。方括号 [attr=value] 则交给 AttributesParser 解析后取第一个键,支持 * ^ $ 三种后缀分别表示包含、前缀、后缀匹配,后缀会被截掉再拿去查属性。 在 MT5 里跑这套解析时,注意 StringFind 前后补空格的写法,若你的 class 值本身带首尾空格会漏配;外汇与贵金属行情数据喂进这类选择器前,建议先 StringTrim 再比对,否则高波动时段解析错误可能触发误信号。

MQL5 / C++
if(parent != NULL && parent.getChildrenCount() - parent.getChildIndex(&this) - class="num">1 != x - class="num">1)
      {
        matched = class="kw">false;
      }
      }
      }
      else
      if(u[i].type == &class="macro">#x27;.&class="macro">#x27;)
      {
        if(attributes.isKeyExisting("class"))
        {
          Container *c = attributes["class"];
          if(c == NULL || StringFind(" " + c.get<class="type">class="kw">string>() + " ", " " + u[i].value + " ") == -class="num">1)
          {
            matched = class="kw">false;
          }
        }
        else
        {
          matched = class="kw">false;
        }
      }
      else
      if(u[i].type == &class="macro">#x27;#&class="macro">#x27;)
      {
        if(attributes.isKeyExisting("id"))
        {
          Container *c = attributes["id"];
          if(c == NULL || StringCompare(c.get<class="type">class="kw">string>(), u[i].value) != class="num">0)
          {
            matched = class="kw">false;
          }
        }
        else
        {
          matched = class="kw">false;
        }
      }
      else
      if(u[i].type == &class="macro">#x27;[&class="macro">#x27;)
      {
        AttributesParser p;
        IndexMap hm;
        p.parseAll(u[i].value, hm);
        class=class="str">"cmt">// 逐个选择属性:element[attr1=value][attr2=value]
        class=class="str">"cmt">// 映射一次只能包含一个有效对
        if(hm.getSize() > class="num">0)
        {
          class="type">class="kw">string key = hm.getKey(class="num">0);
          class="type">class="kw">ushort suffix = StringGetCharacter(key, StringLen(key) - class="num">1);
          
          if(suffix == &class="macro">#x27;*&class="macro">#x27; || suffix == &class="macro">#x27;^&class="macro">#x27; || suffix == &class="macro">#x27;$&class="macro">#x27;) class=class="str">"cmt">// contains, starts with, or ends with
          {
            key = StringSubstr(key, class="num">0, StringLen(key) - class="num">1);

属性匹配的四种后缀规则

在 MT5 自定义类的选择器解析里,属性匹配不是简单地全等比较,而是靠 suffix 变量决定匹配模式。suffix 为 0 是精确匹配,为 '*' 是包含匹配,为 '^' 是前缀匹配,为 '$' 是后缀匹配。 当 suffix 等于 0 且 key 为 "class" 时,代码用 " " + 属性值 + " " 包住两端再查找,避免 "btn" 误中 "btnpair" 这类部分重合。其余 key 则直接 StringCompare 全等。 后缀 '$' 的处理稍微绕:先取属性串 x,若 x 长度大于待查串 v,才从 x 尾部倒数 StringLen(v) 的位置起找 v,命中位置必须等于 StringLen(v) 才认。这意味着 "panel" 能中 "signal" 的尾,但 "p" 中 "x" 因长度不够直接落空。 开 MT5 把下面片段塞进你的选择器函数,改 suffix 赋值就能验证四种命中差异;外汇与贵金属图表对象繁杂,误匹配可能让脚本改错控件,实盘前务必在策略测试器跑一遍。

MQL5 / C++
else
{
   suffix = class="num">0;
}

if(hasAttribute(key) && attributes[key] != NULL)
{
   class="type">class="kw">string v = hm[class="num">0] != NULL ? hm[class="num">0].get<class="type">class="kw">string>() : "";
   if(StringLen(v) > class="num">0)
   {
      if(suffix == class="num">0)
      {
         if(key == "class")
         {
            matched &= (StringFind(" " + attributes[key].get<class="type">class="kw">string>() + " ", " " + v + " ") > -class="num">1);
         }
         else
         {
            matched &= (StringCompare(v, attributes[key].get<class="type">class="kw">string>()) == class="num">0);
         }
      }
      else
      if(suffix == &class="macro">#x27;*&class="macro">#x27;)
      {
         matched &= (StringFind(attributes[key].get<class="type">class="kw">string>(), v) != -class="num">1);
      }
      else
      if(suffix == &class="macro">#x27;^&class="macro">#x27;)
      {
         matched &= (StringFind(attributes[key].get<class="type">class="kw">string>(), v) == class="num">0);
      }
      else
      if(suffix == &class="macro">#x27;$&class="macro">#x27;)
      {
         class="type">class="kw">string x = attributes[key].get<class="type">class="kw">string>();
         if(StringLen(x) > StringLen(v))
         {
            matched &= (StringFind(x, v, StringLen(x) - StringLen(v)) == StringLen(v));

◍ 从 DOM 节点抽取表格数据的映射逻辑

这段 MQL5 片段实现了把网页 DOM 查询结果转成二维索引表的过程。核心入口是 tableSelect,它先按行选择器拿到一组 DomElement,再对每一行按列选择器逐个抓取值。 若某列的选择器命中了子节点,且 dataSelectors 传空串,就取该节点文本;否则取指定属性。抓到的值会做左右去空格再写进 row,没命中则只占位不写值。 只有 row.getSize()>0 的行才会以自增 counter 转成字符串作为键存入 data,空行直接释放。对做贵金属跨平台报价聚合来说,这种跳过空行的机制能避免后续计算时踩到空指针——外汇与贵金属波动剧烈,解析层少一次崩溃就少一次漏单风险。 下面把关键函数逐行拆一下: IndexMap *tableSelect(const string rowSelector, const string &columSelectors[], const string &dataSelectors[]) // 入参:行选择器、列选择器数组、取属性名数组(空串表示取文本) { DomIterator *r = querySelect(rowSelector); // 按行选择器查出所有行迭代器 IndexMap *data = new IndexMap('\n'); // 新建以换行符分隔的索引表容器 int counter = 0; // 行号计数器 r.rewind(); // 迭代器复位到头部 while(r.hasNext()) // 遍历每一行 { DomElement *e = r.next(); // 取出当前行元素 string id = IntegerToString(counter); // 用计数器生成行键 IndexMap *row = new IndexMap(); // 新建单行映射 for(int i = 0; i < ArraySize(columSelectors); i++) // 遍历每一列选择器 { DomIterator *d = e.querySelect(columSelectors[i]); // 在当前行内查列 string value; // 声明值变量 if(d.getChildrenCount() > 0) // 若列节点存在 { if(dataSelectors[i] == "") // 未指定属性则取文本 { value = d[0].getText(); } else // 指定了属性名 { value = d[0].getAttribute(dataSelectors[i]); } StringTrimLeft(value); StringTrimRight(value); // 去左右空格 row.setValue(IntegerToString(i), value); // 写入列索引与值 } else // 没有找到栏位 { row.set(IntegerToString(i)); // 仅占位 } delete d; // 释放列迭代器 } if(row.getSize() > 0) // 非空行才保留 { data.set(id, row); counter++; } else { delete row; // 空行直接释放 } } delete r; // 释放行迭代器 return data; // 返回二维表 }

MQL5 / C++
IndexMap *tableSelect(class="kw">const class="type">class="kw">string rowSelector, class="kw">const class="type">class="kw">string &columSelectors[], class="kw">const class="type">class="kw">string &dataSelectors[])
{
   DomIterator *r = querySelect(rowSelector);
   IndexMap *data = new IndexMap(&class="macro">#x27;\n&class="macro">#x27;);
   class="type">int counter = class="num">0;
   r.rewind();
   class="kw">while(r.hasNext())
   {
      DomElement *e = r.next();
      class="type">class="kw">string id = IntegerToString(counter);
      IndexMap *row = new IndexMap();
      for(class="type">int i = class="num">0; i < ArraySize(columSelectors); i++)
      {
         DomIterator *d = e.querySelect(columSelectors[i]);
         class="type">class="kw">string value;
         if(d.getChildrenCount() > class="num">0)
         {
            if(dataSelectors[i] == "")
            {
               value = d[class="num">0].getText();
            }
            else
            {
               value = d[class="num">0].getAttribute(dataSelectors[i]);
            }
            StringTrimLeft(value);
            StringTrimRight(value);
            row.setValue(IntegerToString(i), value);
         }
         else class=class="str">"cmt">// 没有找到栏位
         {
            row.set(IntegerToString(i));
         }
         class="kw">delete d;
      }
      if(row.getSize() > class="num">0)
      {
         data.set(id, row);
         counter++;
      }
      else
      {
         class="kw">delete row;
      }
   }
   class="kw">delete r;
   class="kw">return data;

「空小节无内容可析」

当前传入的小节原文仅包含一个未闭合的代码结束标记,没有任何技术描述、参数说明或价格现象可供提炼。 若需补全本技术块,应提供该小节完整的 MQL5 代码片段与对应文字说明,否则无法生成可验证的中文硬核内容。

用 EA 把网页表格扒成 CSV

这个工具 EA 的核心任务,是把网页或本地 HTML 里的表格数据抽出来,按列结构写进 CSV。它不吃人工复制粘贴那套,输入参数配好就能在 MT5 里自动跑。 输入参数里,URL 填 http(s) 开头网址或本地 HTML 文件名;SaveName 是结果 CSV 名,若留空 RowSelector 则进入“存原页”模式,方便你后面调选择器。列配置不写在 EA 参数里,而是丢进一个独立的 CSV set 文件,由 ColumnSettingsFile 指定。 那个 set 文件第一行是表头,之后每行描述一列,固定三列:name(输出第 i 列)、CSS selector(行内取数用,填“.”表示直接取行元素)、data locator(取属性名或留空拿标签文本)。TestQuery / TestSubQuery 两个参数只把选择器命中结果打到日志,不写文件,适合先验证再正式抓。 代码里能看清实际流程:先按 URL 前缀决定走 WebRequest 还是读本地文件(本地读注意 FileReadString 循环拼装,注释里提醒二进制一次读有 4095 字节坑);接着 HtmlParser 把字符串变 DOM,TestQuery 非空就 querySelect 打印验证;正常模式才 loadColumnConfig 再 tableSelect 落盘。 实操建议:第一次跑任意陌生页面,先填 TestQuery 看日志里 DOM 节点对不对,再补 RowSelector 和列 set,能省掉大半调试时间。外汇与贵金属行情页抓取同样受平台网络权限限制,实盘环境开 EA 前需在“允许 WebRequest”列表加白域名,属高风险自动化操作,验证不充分可能抓到空表。

MQL5 / C++
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> URL = <span class="class="type">class="kw">string">""</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> SaveName = <span class="class="type">class="kw">string">""</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> RowSelector = <span class="class="type">class="kw">string">""</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> ColumnSettingsFile = <span class="class="type">class="kw">string">""</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> TestQuery = <span class="class="type">class="kw">string">""</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> TestSubQuery = <span class="class="type">class="kw">string">""</span>;
<span class="keyword">class="type">int</span> process()
{
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> xml;
&nbsp;&nbsp;
&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">StringFind</span>(URL, <span class="class="type">class="kw">string">"http:class=class="str">"cmt">//"</span>) == <span class="number">class="num">0</span> || <span class="functions">StringFind</span>(URL, <span class="class="type">class="kw">string">"https://"</span>) == <span class="number">class="num">0</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;xml = ReadWebPageWR(URL);
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"读取 html 文件 "</span>, URL);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> h = <span class="functions">FileOpen</span>(URL, <span class="macro">FILE_READ</span>|<span class="macro">FILE_TXT</span>|<span class="macro">FILE_SHARE_WRITE</span>|<span class="macro">FILE_SHARE_READ</span>|<span class="macro">FILE_ANSI</span>, <span class="number">class="num">0</span>, <span class="macro">CP_UTF8</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(h == <span class="macro">INVALID_HANDLE</span>)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"读取文件错误 &class="macro">#x27;"</span>, URL, <span class="class="type">class="kw">string">"&class="macro">#x27;: "</span>, <span class="functions">GetLastError</span>());
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> -<span class="number">class="num">1</span>;
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">StringInit</span>(xml, (<span class="keyword">class="type">int</span>)<span class="functions">FileSize</span>(h));
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">while</span>(!<span class="functions">FileIsEnding</span>(h))
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;xml += <span class="functions">FileReadString</span>(h) + <span class="class="type">class="kw">string">"\n"</span>;
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// xml = FileReadString(h, (class="type">int)FileSize(h)); - 二进制文件中有4095个字节的限制!</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">FileClose</span>(h);
&nbsp;&nbsp;}
&nbsp;&nbsp;...
&nbsp;&nbsp;HtmlParser p;
&nbsp;&nbsp;DomElement *document = p.parse(xml);
&nbsp;&nbsp;<span class="keyword">if</span>(TestQuery != <span class="class="type">class="kw">string">""</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"测试 query, subquery: &class="macro">#x27;"</span>, TestQuery, <span class="class="type">class="kw">string">"&class="macro">#x27;, &class="macro">#x27;"</span>, TestSubQuery, <span class="class="type">class="kw">string">"&class="macro">#x27;"</span>);
&nbsp;&nbsp;&nbsp;&nbsp;DomIterator *r = document.querySelect(TestQuery);
&nbsp;&nbsp;&nbsp;&nbsp;r.printAll();
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(TestSubQuery != <span class="class="type">class="kw">string">""</span>)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;r.rewind();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">while</span>(r.hasNext())
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DomElement *e = r.next();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DomIterator *d = e.querySelect(TestSubQuery);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;d.printAll();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">delete</span> d;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">delete</span> r;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(<span class="number">class="num">0</span>);
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> columnSelectors[];
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> dataSelectors[];
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">string</span> headers[];
&nbsp;&nbsp;
&nbsp;&nbsp;<span class="keyword">if</span>(!loadColumnConfig(columnSelectors, dataSelectors, headers)) <span class="keyword">class="kw">return</span>(-<span class="number">class="num">1</span>);
&nbsp;&nbsp;
&nbsp;&nbsp;IndexMap *data = document.tableSelect(RowSelector, columnSelectors, dataSelectors);
&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">StringLen</span>(SaveName) &gt; <span class="number">class="num">0</span>)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="class="type">class="kw">string">"把数据保存为 CSV 到 "</span>, SaveName);

◍ 把指标矩阵落盘成 CSV 的收尾写法

这段逻辑出现在导出函数的末尾,负责把已经组装好的二维数据(data)真正写出去,或者仅在日志里打印。先尝试用 FileOpen 以 CSV+ANSI 模式、制表符作分隔打开目标文件,若返回 INVALID_HANDLE 就打印错误行数与 GetLastError(),便于在 MT5 终端里直接定位权限或路径问题。 成功拿到句柄后,先用 FileWriteString 写表头:StringImplodeExt(headers, ",") 把列名数组合并成逗号分隔串并补换行;随后 data.asCSVString() 一次性写出全部数据行,再 FileClose 收尾,并打印“行已写入”的确认信息,行数来自 data.getSize()。 如果前置判断走的是非文件分支(else),则直接 Print(data.asCSVString()) 把内容吐到专家日志,适合临时核对而不落盘。最后 delete data 释放内存并返回 0,整个导出调用结束。 在 MT5 里验证时,可故意把 SaveName 设成只读目录,观察是否触发“写入错误”分支;外汇与贵金属行情数据导出涉及真实报价,复盘用途虽广但实盘决策仍属高风险,任何历史切片都不能直接推导未来。

MQL5 / C++
  class="type">int h = FileOpen(SaveName, FILE_WRITE|FILE_CSV|FILE_ANSI, &class="macro">#x27;\t&class="macro">#x27;, CP_UTF8);
  if(h == INVALID_HANDLE)
  {
    Print("写入错误 ", data.getSize() ," rows to file &class="macro">#x27;", SaveName, "&class="macro">#x27;: ", GetLastError());
  }
  else
  {
    FileWriteString(h, StringImplodeExt(headers, ",") + "\n");
    
    FileWriteString(h, data.asCSVString());
    FileClose(h);
    Print((class="type">class="kw">string)data.getSize() + " 行已写入");
  }
}
else
{
  Print("\n" + data.asCSVString());
}

class="kw">delete data;

class="kw">return(class="num">0);
}

「把 MT5 报告 HTML 拆成可分析的 CSV」

MT5 跑完回测或导出账户历史,会得到 ReportHistory.html、tester.html 这类标准网页文件。想直接在图表上叠加订单/交易分布,第一步是把嵌套表格捞出来转成 CSV,否则只能瞪着浏览器看。 交易报告表面分「订单」和「交易」两块,但 HTML 里其实是同一张表,靠单元格样式画出了分隔线。订单行 11 列、交易行 13 列——CSS 标准没法按子元素数量反选父行,所以得自己造个非标准伪类 :has-n-children(n),类比 :nth-child(n)。 光用 tr:has-n-children(11) 会把表头也选进来。看 bgcolor:数据行奇偶交替 #FFFFFF / #F7F7F7,表头是 #E5F0FC。因此把条件收成 tr:has-n-children(11)[bgcolor^="#F"],就能精准命中订单数据行。 每行参数用配置文件映射列序即可,例如订单配置 ReportHistoryOrders.cfg.csv: Name,Selector,Data Time,td:nth-child(1), Order,td:nth-child(2), Symbol,td:nth-child(3), Type,td:nth-child(4), Volume,td:nth-child(5), Price,td:nth-child(6), S/L,td:nth-child(7), T/P,td:nth-child(8), Time,td:nth-child(9), State,td:nth-child(10), Comment,td:nth-child(11), 启动 WebDataExtractor 时填 URL=ReportHistory.html、RowSelector=tr:has-n-children(11)[bgcolor^="#F"]、ColumnSettingsFile=ReportHistoryOrders.cfg.csv,就得到 ReportOrders.csv。交易表只需把 11 换成 13。 MT4 的 htm 报告结构不同:两个表并列,用 table~table 抓第二个,再用 tr+tr 跳过首行标题,合并选择器 table~table tr+tr。日期列靠 class="msdate" 提,配置写 td.msdate。 MT5 生成的 HTML 布局可能随版本变,外部看着一样、内部标签一改旧选择器就失效。遇到空文件先重新读源码调选择器,别怀疑程序坏了。外汇与贵金属杠杆高,任何历史报告都只是过去样本,转成 CSV 后仍需自判信号有效性。

MQL5 / C++
Name,Selector,Data
Time,td:nth-child(class="num">1),
Order,td:nth-child(class="num">2),
Symbol,td:nth-child(class="num">3),
Type,td:nth-child(class="num">4),
Volume,td:nth-child(class="num">5),
Price,td:nth-child(class="num">6),
S/L,td:nth-child(class="num">7),
T/P,td:nth-child(class="num">8),
Time,td:nth-child(class="num">9),
State,td:nth-child(class="num">10),
Comment,td:nth-child(class="num">11),

最后一句大实话

MQL 里实现 CSS 选择器已经能不依赖第三方软件,把标准 MetaTrader 文档这类 HTML 直接转成结构化数据,但这一步只是起点。 MetaTrader 还大量使用 XML,XPath 和 XSLT 才是下一步该碰的工具,能补上 HTML 之外的通用解析能力。 外汇和贵金属市场波动剧烈、杠杆风险高,用 Web 文档自动化提取数据做策略输入时,先在小账户或模拟盘验证选择器稳定性,别直接挂实盘。

让小布替你验网页结构
这些诊断网页DOM与选择器命中率的活儿,小布盯盘的AIGC已内置,打开对应品种页即可看到外部数据接入的可用度提示,你只管把解析结果接进策略。

常见问题

HTML嵌套与属性顺序混乱,硬切容易漏行;转成DOM后用选择器按层级取节点才稳,容错也高。
不一定,项目用的是简化语法,传统id和class命中优先,复杂伪类可能要在EA里二次过滤。
小布内置的AIGC可预检目标站点的结构变动并标出选择器失效风险,你再把WebDataExtractor的输出接进EA即可。
先确认表格列顺序固定,再用行选择器批量读列,写文件时带时间戳避免覆盖历史。
特定站点有细分口径或区域事件,平台日历未覆盖,自选源可做交叉验证提升信息概率。