CSSセレクタを使用した HTML ページからの構造化データの抽出

Stanislav Korotky | 17 5月, 2019

MetaTrader 開発環境では、アプリケーションと外部データ、特に WebRequest 関数を使用してインターネットから取得したデータを統合することができます。 HTML は、web 上で最も普遍的で頻繁に使用するデータ形式です。 パブリックサービスがリクエストに対してオープンな API を提供していない場合、またはそのプロトコルが MQL で実装するのが難しい場合は、目的の HTML ページを解析できます。 特に, トレーダーは、多くの場合、様々な経済カレンダーを使用します。 現時点ではこのタスクはさほど重要ではありませんが、プラットフォームには組み込みのカレンダー関数があるため、特定のサイトから特定のニュースを必要とするトレーダーもいます。 また、サードパーティから受け取ったトレードの HTML レポートからトレードを分析する必要がある場合もあります。

MQL5 エコシステムは、問題に対してさまざまな解決策を提供しますが、通常は特定の制限があります。 一方、HTML からデータを検索して解析するには、一種の "native " と普遍的な方法があります。 このメソッドは、CSS セレクタを使用して接続されています。 この記事では、このメソッドの MQL5 実装とその実用例を考察します。

HTML を分析するには、内部ページテキストをドキュメントオブジェクトモデルまたは DOM と呼ばれるオブジェクトの階層に変換できるパーサーを作成する必要があります。この階層から、指定されたパラメータを持つオブジェクトを検索することができます。 この方法は、外部ページビューでは使用できないドキュメント構造に関するサービス情報の使用に基づいています。

たとえば、ドキュメント内の特定のテーブルの行を選択し、そこから必要な列を読み取り、値を持つ配列を取得して、csv ファイルに保存したり、チャートに表示したり、Expert Advisor の計算に使用したりすることができます。


HTML/CSS と DOM 技術の概要

HTML は、よく知られている人気のある形式です。 したがって、このハイパーテキストマークアップ言語の構文については詳しく説明しません。

関連する技術情報の主なソースは、IETF (インターネット技術タスクフォース) とその仕様、いわゆる RFC (コメントのリクエスト) です。 これには多くの HTML 仕様があります (ここではを示します)。 標準は、関連する組織、W3C (ワールドワイドウェブコンソーシアムhtml 5.2) のウェブサイト上でも利用可能です。

これらの組織は、CSS (カスケードスタイルシート) 技術を開発し、管理します。 しかし、この技術に興味があるのは、web ページの情報表現スタイルを記述しているからではなく、そこに含まれるCSS セレクタ、すなわち html ページ内の要素の検索を可能にする特別なクエリ言語であるからではありません。

HTML と CSS の両方が常に進化しており、新しいバージョンが作成されています。 たとえば、現在関連するバージョンは、HTML 5.2 および CSS4 です。 しかし、更新と拡張は、常に古いバージョンの関数の継承を伴います。 このウェブは大きく、異質であり、しばしば不活性であり、したがって、古いものに沿って新しいバージョンが存在します。 そのため、web 技術の使用を暗示するアルゴリズムを記述する場合は、次の仕様を慎重に使用する必要があります。一方では、可能な従来の逸脱を考慮に入れる必要があり、他方では、シンプル化を追加する必要がある複数のバリエーションの問題を回避するのに役立ちます。

このプロジェクトでは、簡略化された HTML 構文について考察します。

Html ドキュメントは、文字 ' < ' ' および ' > ' 内のタグで構成されています。 タグの内部には、タグ名と省略可能な属性が指定されています。 オプション属性は、name = "value " のストリング・ペアであり、符号 ' = ' は時々省略することができます。 タグの例を次に示します。

<a href="https://www.w3.org/standards/webdesign/htmlcss" target="_blank">HTML and CSS</a>

— これは' a ' (ハイパーリンクとして web ブラウザによって解釈されます) という名前のタグで、指定されたハイパーリンクの web サイトアドレスの ' href ' と web サイトの開始オプションの ' target ' (この場合は "_blank "、つまりサイトが開く新しいブラウザタブ)。

この最初のタグは開始タグです。 続いて、web ページに実際に表示されるテキスト、 "HTML と CSS "、および一致する終了タグは、開始タグと同じ名前を持ち、追加のスラッシュ '/' は角度ブラケットの後<'にあります ' ')。 言い換えれば、開閉タグはペアで使用され、他のタグを含むことができますが、タグ全体が重複することはありません。 正しいネストの例を次に示します。

<group attribute1="value1">

  <name>text1</name>

  <name>text2</name>

</group>

次の "オーバーラップ " は許されていません。

<group id="id1">

<name>text1

</group>

</name>

ただし、これは文法上は使用できないというい話です。 実際には、間違った場所でタグを誤って開いたり閉じたりすることがよくあります。 このパーサーは、この状況を処理できる必要があります。

タグは空であるかもしれません、すなわち、空行である場合もあります:

<p></p>

タグはまったくコンテンツを持たないかもしれません。 たとえば、イメージを説明するタグは次のとおりです。

<img src="/ico20190101.jpg">

開始タグのように見えますが、一致する終了タグを持っていません。 このようなタグは空と呼ばれます。 タグに属する属性はタグの内容ではないことに注意してください。

タグが空であるかどうか、さらに終了タグが必要かどうかを判断することは必ずしも容易ではありません。 有効な空のタグの名前は仕様で定義されていますが、他のタグの中にはまだ閉じていないものもあります。 また、HTML と XML の形式が近いため (XHTML もあります)、一部の web ページデザイナは次のように空のタグを作成します。

<img src="/ico20190101.jpg" />

角括弧 ' > ' の前にスラッシュ '/' に注意してください。 このスラッシュは、厳密な HTML5 ルールの観点からは過剰と見なされます。 すべての特定のケースは、通常の web ページで満たすことができるので、パーサーはを処理することができる必要があります。

Web ブラウザによって解釈されるタグと属性名は標準ですが、HTML にはカスタマイズされた要素をインクルードすることができます。 このような要素はブラウザによってスキップされますが、開発者が特殊なスクリプト API を使用して DOM に "接続" する場合を除きます。 すべてのタグには有用な情報が含まれている可能性があることに注意してください。

パーサーは有限状態マシンと見なすことができ、文字を進め、コンテキストに従ってその状態を変更します。 上記のタグ構造の記述から明らかなことは、最初にパーサーが任意のタグの外側にあるということです (この状態を "blank " と呼ぶことにします)。 その後、オープンアングルブラケット ' < ' に遭遇した後、開始タグ ( "insideTagOpen " 状態) に入り、終了アングルブラケット ' > ' まで続きます。 ' </' の文字の組み合わせは、終了タグ ( "insideTagClose " 状態) であることを示します。 その他の状態は、パーサーの実装のセクションで考慮されます。

状態の間で切り替えるときは、状態の意味を知っているので、文書内の現在のポジションから構造化された情報を選択することができます。 たとえば、現在のポジションが開始タグの内側にある場合、直近の ' < ' ' と以降の領域 (タグに属性が含まれているかどうかによって異なります) の間の線としてタグ名を選択できます。 このパーサーは、データを抽出し、特定の DomElement クラスのオブジェクトを作成します。 名前、属性、および内容に加えて、オブジェクトの階層はタグのネスト構造に基づいて保持されます。 つまり、各オブジェクトには親 (ドキュメント全体を記述するルート要素を除く) と子オブジェクトのオプションの配列があります。

パーサーは、1つのオブジェクトがソースドキュメント内の1つのタグに対応するオブジェクトの完全なツリーを出力します。

CSS セレクタは、階層内のパラメータとポジションに基づいて、オブジェクトの条件付き選択の標準表記法を記述します。 セレクタの完全なリストは広範です。 CSS1、CSS2 および CSS3 の標準に含まれているサポートを提供します。

メインセレクタコンポーネントのリストを次に示します。

これらは右側に追加される、いわゆる擬似クラスを伴うことができます。

1つのセレクタは、属性に関連する条件によって補足できます。

必要に応じて、異なる属性を持つ括弧のペアをいくつか指定することができます。

Simple selectorは、名前セレクタまたはユニバーサルセレクタであり、任意のオーダーでクラス、識別子、0個以上の属性、または擬似クラスを任意に続けることができます。 セレクタのすべてのコンポーネントが要素のプロパティと一致する場合、シンプルなセレクタは要素を選択します。

CSS selector(またはフルセレクタ) は、組み合わせ文字 (' ' (スペース)、' > '、' + '、' ~ ') によって結合された1つ以上のシンプルなセレクタのチェーンです。

これまでのところ、我々は純粋な理論を研究しています。 上記のアイデアが実際にどのように機能するかを見てみましょう。

最新の web ブラウザでは、現在開いているページの HTML を表示できます。 たとえば、Chrome では、コンテキストメニューから「ページソースの表示」コマンドを実行するか、開発者ウィンドウ (開発者ツール、Ctrl + Shift + I) を開くことができます。 開発者ウィンドウには、CSS セレクタを使用して要素を見つけることができるコンソールタブがあります。 セレクタを適用するには、querySelectorAll 関数をコンソールから呼び出すだけです (すべてのブラウザのソフトウェア API に含まれています)。

たとえば、フォーラムの開始ページhttps://www.mql5.com/ja/forumで、次のコマンド (JavaScript コード) を実行できます。

document.querySelectorAll("div.widgetHeader")

この結果、 "widgetHeader " クラスが指定されている ' div ' 要素 (タグ) のリストが表示されます。 フォーラムのトピックは、このように設計されていることが明らかであることに基づいて、ソースページのコードを見た後、このセレクタを使用することにしました。

セレクタは次のように展開できます。

document.querySelectorAll("div.widgetHeader a:first-child")

フォーラム・トピック・ディスカッション・ヘッダのリストを受け取るには、最初のステージで選択された各「div」ブロックの最初の子要素であるハイパーリンク ' a ' として使用できます。 どのように見えるかは次のとおりです (ブラウザのバージョンによって異なります)。

MQL5 ウェブページと CSS セレクタを使用した HTML 要素の選択の結果

MQL5 ウェブページと CSS セレクタを使用した HTML 要素の選択の結果

また、目的のサイトの HTML コードを分析し、目的の要素を見つけ、適切な CSS セレクタを選択する必要があります。 開発者ウィンドウには、ドキュメント内の任意のタグを選択し (このタグは強調表示されます)、このタグに適切な CSS セレクタを見つけることができる要素 (または類似する) タブがあります。 したがって、徐々にセレクタの使用を練習し、手動でセレクタチェーンを作成することを学びます。 さらに、特定の web ページに対して適切なセレクタを選択する方法についても検討します。


デザイン

今後必要とするかもしれないクラスをグローバルレベルで見てみましょう。 最初の HTML テキスト処理は、HtmlParser クラスによって実行されます。 このクラスは、マークアップ文字 ' < '、'/'、' > ' などのテキストをスキャンし、上記の有限状態マシンルールに従って DomElement クラスオブジェクトを作成します。空のタグごと、または開閉のペアごとに1つのオブジェクトが作成されます。 開始タグには、現在の DomElement オブジェクトで読み取って保存する必要がある属性が含まれている場合があります。 これはAttributesParser クラスによって実行されます。 また、このクラスは有限状態マシンの原理に従って動作します。

このパーサーは、階層を考慮して DomElement オブジェクトを作成しますが、タグのネストオーダーと同じです。 たとえば、テキストに複数の段落が配置されている ' div ' タグが含まれている場合 (' p ' タグが存在することを意味します)、このような段落は ' div ' を記述するオブジェクトの子オブジェクトに変換されます。

最初のルートオブジェクトには、ドキュメント全体が含まれます。 ブラウザ (querySelectorAll メソッドを提供します) と同様に、渡された CSS セレクタに対応するオブジェクトをリクエストする方法を DomElement で提供する必要があります。 セレクタを事前に分析し、文字列表現からオブジェクトに変換する必要があります: 単一セレクタコンポーネントは SubSelector クラスに格納され、シンプルセレクタ全体は SubSelectorArray に格納されます。

パーサー操作の結果として準備ができた DOM ツリーを用意したら、ルート DomElement オブジェクト (またはその他のオブジェクト) がセレクタパラメータに一致するすべての子要素をリクエストすることができるようになります。 選択したすべての要素が iterable DomIterator リストに配置されます。 シンプルにするために、DomElement の子としてリストを実装し、子ノードの配列を使用して見つかった要素を格納します。

特定のサイトまたは HTML ファイルの処理ルールとアルゴリズム実行結果の設定は、マッププロパティを組み合わせた (つまり、適切な属性の名前に基づいて値へのアクセスを提供する) クラスに便利に格納できます。 (すなわち、インデックスによる要素へのアクセス)。 このクラスを IndexMap と呼びましょう。

IndexMap をネストにする可能性を提供しましょう: web ページから表形式データを収集するときに、列のリストを含む行のリストを取得します。 どちらのデータ型でも、ソース要素の名前を保存することができます。 ソースドキュメントに必要な要素の一部が欠落している (頻繁に発生する可能性がある) 場合に特に便利です。 ボーナスとして、CSV 形式を含む複数行のテキストにシリアル化するために、 IndexMap を「トレイン 」します。 この特徴は、HTML ページを表形式データに変換する場合に便利です。 必要に応じて、主な関数を維持しながら、IndexMap クラスを独自のものに置き換えることができます。

次の UML 図は、記述されたクラスを表示します。

MQL で CSS セレクタを実装するクラスの UML 図

MQL で CSS セレクタを実装するクラスの UML 図



実装

HtmlParser

HtmlParser クラスでは、ソーステキストをスキャンしてオブジェクトツリーを生成するために必要な変数について説明するとともに、有限状態マシンアルゴリズムを配置します。

テキスト内の現在ポジションは ' offset ' 変数に格納されます。 結果として得られるツリールートと現在のオブジェクト (このオブジェクトコンテキストでスキャンが実行される) は、' root ' ポインタと ' cursor ' ポインタによって表されます。 DomElement タイプは後で考慮されます。 HTML の仕様に従って空になる可能性のあるタグのリストは、' 空 ' マップにロードされます (コンストラクタで初期化され、以下を参照してください)。 最後に、有限状態機械の状態の記述の「状態」変数を提供します。 この変数は、StateBit 型の列挙体です。

enum StateBit
{
  blank,
  insideTagOpen,
  insideTagClose,
  insideComment,
  insideScript
};

class HtmlParser
{
  private:

    StateBit state;
    
    int offset;
    DomElement *root;
    DomElement *cursor;
    IndexMap empties;
    ...

StateBit 列挙体には、テキスト内の現在のポジションに応じて、次のパーサーの状態を説明する要素があります。

さらに、マークアップの検索に使用する定数文字列についても説明します。

    const string TAG_OPEN_START;
    const string TAG_OPEN_STOP;
    const string TAG_OPENCLOSE_STOP;
    const string TAG_CLOSE_START;
    const string TAG_CLOSE_STOP;
    const string COMMENT_START;
    const string COMMENT_STOP;
    const string SCRIPT_STOP;

パーサーコンストラクタは、すべての変数を初期化します。

  public:
    HtmlParser():
      TAG_OPEN_START("<"),
      TAG_OPEN_STOP(">"),
      TAG_OPENCLOSE_STOP("/>"),
      TAG_CLOSE_START("</"),
      TAG_CLOSE_STOP(">"),
      COMMENT_START("<!--"),
      COMMENT_STOP("-->"),
      SCRIPT_STOP("/script>"),
      state(blank)
    {
      for(int i = 0; i < ArraySize(empty_tags); i++)
      {
        empties.set(empty_tags[i]);
      }
    }

ここでは、empty_tags 文字列の配列を使用します。 この配列は、外部テキストファイルから暫定接続されています。

string empty_tags[] =
{
  #include <empty_strings.h>
};

以下の内容を参照してください (有効な空のタグですが、リストは完全ではありません)。

//ヘッダ
"isindex",
"base",
"meta",
"link",
"nextid",
"range",
// body
"img",
"br",
"hr",
"frame",
"wbr",
"basefont",
"spacer",
"area",
"param",
"keygen",
"col",
"limittext"

DOM ツリーを削除することを忘れないでください:

    ~HtmlParser()
    {
      if(root != NULL)
      {
        delete root;
      }
    }

主な操作は、解析メソッドによって実行されます。

    DomElement *parse(const string &html)
    {
      if(root != NULL)
      {
        delete root;
      }
      root = new DomElement("root");
      cursor = root;
      offset = 0;
      
      while(processText(html));
      
      return root;
    }

Web ページがインプットされ、空のルート DomElement が作成され、カーソルが設定される一方で、テキスト (offset) の現在ポジションは先頭に設定されます。 次に、テキスト全体が正常に読み取られるまで、processText ヘルパメソッドがループで呼び出されます。 有限状態マシンは、このメソッドで実行されます。 コンピュータのデフォルトの状態は空白です。

    bool processText(const string &html)
    {
      int p;
      if(state == blank)
      {
        p = StringFind(html, "<", offset);
        if(p == -1) //以上のタグ
        {
          return(false);
        }
        else if(p > 0)
        {
          if(p > offset)
          {
            string text = StringSubstr(html, offset, p - offset);
            StringTrimLeft(text);
            StringTrimRight(text);
            StringReplace(text, "&nbsp;", "");
            if(StringLen(text) > 0)
            {
              cursor.setText(text);
            }
          }
        }
        
        offset = p;
        
        if(IsString(html, COMMENT_START)) state = insideComment;
        else
        if(IsString(html, TAG_CLOSE_START)) state = insideTagClose;
        else
        if(IsString(html, TAG_OPEN_START)) state = insideTagOpen;
        
        return(true);
      }

このアルゴリズムは、角度ブラケット ' < ' のテキストを詳しく見ることができます。 見つからない場合は、以上のタグがないので、処理を中断する必要があります (false が返されます)。 括弧が見つかって、新しい見つかったタグと前のポジション (offset) の間にテキストの断片がある場合、そのフラグメントは現在のタグの内容とみなされます (オブジェクトは ' カーソル ' ポインタで利用可能です)-したがって、このテキストはオブジェクトに追加されます使っg setText() の呼び出し。

その後、テキスト内のポジションが新しい見つかったタグの先頭に移動され、' < ' (COMMENT_START, TAG_CLOSE_START, TAG_OPEN_START) に続く署名に応じて、パーサーは適切な新しい状態に切り替わります。 IsString 関数は、StringSubstr を使用する小さなヘルパー文字列比較メソッドです。

いずれにせよ、true は processText メソッドから返されるため、メソッドはループ内で再度呼び出されますが、パーサーの状態は現在とは異なります。 現在ポジションが開始タグにある場合は、次のコードが実行されます。

      else
      if(state == insideTagOpen)
      {
        offset++;
        int pspace = StringFind(html, " ", offset);
        int pright = StringFind(html, ">", offset);
        p = MathMin(pspace, pright);
        if(p == -1)
        {
          p = MathMax(pspace, pright);
        }
        
        if(p == -1 || pright == -1) // no tag closing
        {
          return(false);
        }

テキストにスペースも ' > ' もない場合、HTML 構文はブレイクしているので、false が返されます。 さらにステップは、タグ名を選択します。

        if(pspace > pright)
        {
          pspace = -1; // outer space, disregard
        }

        bool selfclose = false;
        if(IsString(html, TAG_OPENCLOSE_STOP, pright - StringLen(TAG_OPENCLOSE_STOP) + 1))
        {
          selfclose = true;
          if(p == pright) p--;
          pright--;
        }
        
        string name = StringSubstr(html, offset, p - offset);
        
        StringToLower(name);
        StringTrimRight(name);
        DomElement *e = new DomElement(cursor, name);

ここでは、見つかった名前で新しいオブジェクトを作成しました。 現在のオブジェクト (カーソル) がオブジェクトの親として使用します。

ここで、属性がある場合は処理する必要があります。

        if(pspace != -1)
        {
          string txt;
          if(pright - pspace > 1)
          {
            txt = StringSubstr(html, pspace + 1, pright - (pspace + 1));
            e.parseAttributes(txt);
          }
        }

ParseAttributes メソッド "生きている " DomElement クラスに直接、後で検討します。

タグが閉じていない場合は、空にできるものでないかどうかを確認する必要があります。 そうであれば、暗黙的に "closed" にする必要があります。

        bool softSelfClose = false;
        if(!selfclose)
        {
          if(empties.isKeyExisting(name))
          {
            selfclose = true;
            softSelfClose = true;
          }
        }

タグが閉じているかどうかによって、オブジェクト階層に沿ってより深く移動し、新しく作成されたオブジェクトを現在のものとして設定するか (e)、または前のオブジェクトのコンテキスト内にとどまります。 いずれの場合も、テキスト (offset) 内のポジションは直近の読み取り文字 (' > ' を超えて) に移動します。

        pright++;
        if(!selfclose)
        {
          cursor = e;
        }
        else
        {
          if(!softSelfClose) pright++;
        }
        
        offset = pright;

特殊なケースはスクリプトです。 もし、<script></script>タグに遭遇したら、このパーサーはinsideScript状態にスイッチします。それ以外の場合はブランクの状態になります。

        if((name == "script") && !selfclose)
        {
          state = insideScript;
        }
        else
        {
          state = blank;
        }
        
        return(true);
        
      }

次のコードは、終了タグの状態で実行されます。

      else
      if(state == insideTagClose)
      {
        offset += StringLen(TAG_CLOSE_START);
        p = StringFind(html, ">", offset);
        if(p == -1)
        {
          return(false);
        }

HTML 構文に従って利用可能でなければならない ' > ' を再度検索してください。 Iаが見つからない場合は、処理を中断する必要があります。 成功した場合は、タグ名が強調表示されます。 終了タグが始値と一致するかどうかをチェックするために行われます。 一致がブレイクしている場合は、何とかこのレイアウトエラーを克服し、解析を続行しようとする必要があります。

        string tag = StringSubstr(html, offset, p - offset);
        StringToLower(tag);
        
        DomElement *rewind = cursor;
        
        while(StringCompare(cursor.getName(), tag) != 0)
        {
          string previous = cursor.getName();
          cursor = cursor.getParent();
          if(cursor == NULL)
          {
            // orphan closing tag
            cursor = rewind;
            state = blank;
            offset = p + 1;
            return(true);
          }
        }

終了タグを処理していますが、現在のオブジェクトのコンテキストが終了しているため、パーサーは親 DomElement に戻ります。

        cursor = cursor.getParent();
        if(cursor == NULL) return(false);
        
        state = blank;
        offset = p + 1;
        
        return(true);
      }

成功した場合、パーサーの状態は再び ' 空白 ' になります。

パーサーがコメントの中にあるとき、明らかにコメントの終わりを探します。

      else
      if(state == insideComment)
      {
        offset += StringLen(COMMENT_START);
        p = StringFind(html, COMMENT_STOP, offset);
        if(p == -1)
        {
          return(false);
        }
        
        offset = p + StringLen(COMMENT_STOP);
        state = blank;
        
        return(true);
      }

パーサーがスクリプト内にある場合は、スクリプトの終了を詳しく見ることができます。

      else
      if(state == insideScript)
      {
        p = StringFind(html, SCRIPT_STOP, offset);
        if(p == -1)
        {
          return(false);
        }
        
        offset = p + StringLen(SCRIPT_STOP);
        state = blank;
        
        cursor = cursor.getParent();
        if(cursor == NULL) return(false);
        
        return(true);
      }
      return(false);
    }

実際には HtmlParser クラス全体でしました。 では DomElement を考えてみましょう。


DomElement. Beginning

DomElement クラスには、名前 (必須)、内容、属性、親および子要素へのリンク (派生クラス DomIterator で使用するため、「プロテクト」として作成されます) を格納するための変数があります。

class DomElement
{
  private:
    string name;
    string content;
    IndexMap attributes;
    DomElement *parent;

  protected:
    DomElement *children[];

コンストラクタのセットには説明は必要ありません。

  public:
    DomElement(): parent(NULL) {}
    DomElement(const string n): parent(NULL)
    {
      name = n;
    }

    DomElement(DomElement *p, const string &n, const string text = "")
    {
      p.addChild(&this);
      parent = p;
      name = n;
      if(text != "") content = text;
    }

もちろん、クラスには "setter " と "ゲッター " フィールドメソッド (記事では省略されています) と、子要素を操作するためのメソッドのセットがあります (この記事ではプロトタイプのみを示しています)。

    void addChild(DomElement *child)
    int getChildrenCount() const;
    DomElement *getChild(const int i) const;
    void addChildren(DomElement *p)
    int getChildIndex(DomElement *e) const;

解析の段階で使用された parseAttributes メソッドは、AttributesParser ヘルパクラスにさらにタスクを行います。

    void parseAttributes(const string &data)
    {
      AttributesParser p;
      p.parseAll(data, attributes);
    }

シンプルな ' data ' 文字列は、メソッドが検出されたプロパティを持つ ' 属性 ' マップを埋めるための出力です。

完全な AttributesParser コードは、以下の添付ファイルで利用可能です。 このクラスは大規模ではなく、HtmlParser と同様に有限状態機械原理によって動作します。 ただし、次の2つの状態しかありません。

enum AttrBit
{
  name,
  value
};

属性のリストは name = "値 " ペアで構成される文字列であるため、AttributesParser は常に名前または値のどちらかになります。 このパーサーは StringSplit 関数を使用して実装できますが、書式のずれ (クオートの有無、クオート内のスペースの使用など) に、マシンアプローチが選択されました。

DomElement クラスについては、その中のほとんどのタスクは、与えられた CSS セレクタに対応する子要素を選択するメソッドによって実行されるべきです。 この関数に進む前に、セレクタクラスを記述する必要があります。

SubSelector と SubSelectorArray

SubSelector クラスは、シンプルなセレクタの1つのコンポーネントについて説明します。 たとえば、シンプルなセレクタ "td [align=left] [width = 325] " には3つのコンポーネントがあります。

シンプルセレクタ "td:first-child" には2つのコンポーネントがあります: シンプルなセレクタ "span.main[id^=calendarTip]"再び3つのコンポーネントがあります:

ここにクラスがあります:

class SubSelector
{
  enum PseudoClassModifier
  {
    none,
    firstChild,
    lastChild,
    nthChild,
    nthLastChild
  };
  
  public:
    ushort type;
    string value;
    PseudoClassModifier modifier;
    string param;
};

' Type ' 変数には、セレクタの最初の文字 ('. '、' # '、' [')、または名前セレクタに対応するデフォルトの0があります。 値変数は、文字に続く部分文字列を格納します。 これが、つまり、実際に検索された要素です。 セレクタ文字列に擬似クラスがある場合、その id は ' 修飾子 ' フィールドに書き込まれます。 セレクタ ": n 番目の子 " と ": n 番目から直近の子 " の説明では、検索された要素のインデックスは角かっこで指定します。 ' param ' フィールドに保存されます (現在の実装では数値しか指定できませんが、特殊な数式も許可されているため、フィールドは文字列として宣言されます)。

SubSelectorArray クラスには多数のコンポーネントが用意されているので、' セレクタ ' 配列を宣言してみましょう。

class SubSelectorArray
{
  private:
    SubSelector *selectors[];

SubSelectorArray は、全体として1つのシンプルなセレクタです。 完全な CSS セレクタでは、各階層レベルの1つのセレクタで順番に処理されるため、クラスは必要ありません。

サポートされている擬似クラスセレクタを ' mod ' マップに追加しましょう。 これより、その文字列の PseudoClassModifier から適切な修飾子をすぐに取得できます。

    IndexMap mod;
    
    static TypeContainer<PseudoClassModifier> first;
    static TypeContainer<PseudoClassModifier> last;
    static TypeContainer<PseudoClassModifier> nth;
    static TypeContainer<PseudoClassModifier> nthLast;
    
    void init()
    {
      mod.add(":first-child", &first);
      mod.add(":last-child", &last);
      mod.add(":nth-child", &nth);
      mod.add(":nth-last-child", &nthLast);
    }

TypeContainer クラスは、IndexMap に追加される値のテンプレートラッパです。

静的メンバ (この場合は map のオブジェクト) は、クラス記述の後で初期化する必要があることに注意してください。

TypeContainer<PseudoClassModifier> SubSelectorArray::first(PseudoClassModifier::firstChild);
TypeContainer<PseudoClassModifier> SubSelectorArray::last(PseudoClassModifier::lastChild);
TypeContainer<PseudoClassModifier> SubSelectorArray::nth(PseudoClassModifier::nthChild);
TypeContainer<PseudoClassModifier> SubSelectorArray::nthLast(PseudoClassModifier::nthLastChild);

SubSelectorArray クラスに戻りましょう。

シンプルなセレクタコンポーネントを配列に追加する必要がある場合は、add 関数が呼び出されます。

    void add(const ushort t, string v)
    {
      int n = ArraySize(selectors);
      ArrayResize(selectors, n + 1);
      
      PseudoClassModifier m = PseudoClassModifier::none;
      string param;
      
      for(int j = 0; j < mod.getSize(); j++)
      {
        int p = StringFind(v, mod.getKey(j));
        if(p > -1)
        {
          if(p + StringLen(mod.getKey(j)) < StringLen(v))
          {
            param = StringSubstr(v, p + StringLen(mod.getKey(j)));
            if(StringGetCharacter(param, 0) == '(' && StringGetCharacter(param, StringLen(param) - 1) == ')')
            {
              param = StringSubstr(param, 1, StringLen(param) - 2);
            }
            else
            {
              param = "";
            }
          }
        
          m = mod[j].get<PseudoClassModifier>();
          v = StringSubstr(v, 0, p);
          
          break;
        }
      }
      
      if(StringLen(param) == 0)
      {
        selectors[n] = new SubSelector(t, v, m);
      }
      else
      {
        selectors[n] = new SubSelector(t, v, m, param);
      }
    }

最初の文字 (type) と次の文字列が渡されます。 この文字列は、検索対象のオブジェクト名、オプションで擬似クラスとパラメータに解析されます。 このすべてが SubSelector コンストラクタに渡され、新しいセレクタコンポーネントが ' セレクタ ' 配列に追加されます。

Add 関数は、シンプルなセレクタコンストラクタから間接的に使用します。

  private:
    void createFromString(const string &selector)
    {
      ushort p = 0; //前の/保留中の型
      int ppos = 0;
      int i, n = StringLen(selector);
      for(i = 0; i < n; i++)
      {
        ushort t = StringGetCharacter(selector, i);
        if(t == '.' || t == '#' || t == '[' || t == ']')
        {
          string v = StringSubstr(selector, ppos, i - ppos);
          if(i == 0) v = "*";
          if(p == '[' && StringLen(v) > 0 && StringGetCharacter(v, StringLen(v) - 1) == ']')
          {
            v = StringSubstr(v, 0, StringLen(v) - 1);
          }
          add(p, v);
          p = t;
          if(p == ']') p = 0;
          ppos = i + 1;
        }
      }
      
      if(ppos < n)
      {
        string v = StringSubstr(selector, ppos, n - ppos);
        if(p == '[' && StringLen(v) > 0 && StringGetCharacter(v, StringLen(v) - 1) == ']')
        {
          v = StringSubstr(v, 0, StringLen(v) - 1);
        }
        add(p, v);
      }
    }

  public:
    SubSelectorArray(const string selector)
    {
      init();
      createFromString(selector);
    }

CreateFromString 関数は、CSS セレクタのテキスト表現を受け取り、ループで表示して、特別な開始文字 '. '、' # ' または ' [' を検索し、次にコンポーネントがどこで終わるかを決定し、選択された情報に対して ' add ' メソッドを呼び出します。 このループは、コンポーネントのチェーンが続く限り継続します。

SubSelectorArray の完全なコードは、以下に添付されています。

では、DomElement クラスに戻って取得しましょう。 これは最も難しい部分です。


DomElement. の続き

QuerySelect メソッドは、(テキスト形式で) 指定されたセレクタに一致する要素を検索するために使用します。 このメソッドでは、完全な CSS セレクタはシンプルセレクタに分割され、SubSelectorArray オブジェクトに変換されます。 一致する要素のリストは、各シンプルなセレクタを検索しました。 次のシンプルなセレクタに一致する他の要素は、見つかった要素を基準に検索されます。 直近のシンプルなセレクタが満たされるか、見つかった要素のリストが空になるまで続きます。

    DomIterator *querySelect(const string q)
    {
      DomIterator *result = new DomIterator();

ここで、戻り値は、DomElement の子である未知の DomIterator クラスです。 DomElement に加えて補助関数を提供します (特に "スクロールする" 子要素) ので、DomIterator を詳細に分析することはありません。 ここにまた別の複雑な部分があります。

セレクタ文字列は文字ごとに分析されます。 このために、ローカル変数が使用します。 現在の文字は c 変数に格納されます。(abbr. of 'character'). 前の文字はp変数 (略称) に格納されます。(abbr. of 'previous'). 文字がコンビネータ文字の1つ (' '、' + '、' > '、' ~ ') である場合、変数 (a) に保存されますが、次のシンプルなセレクタが決定されるまでは使用されません。

コンビネータはシンプルなセレクタの間にありますが、コンビネータによって定義された操作は、右のセレクタ全体を読んだ後にのみ実行することができます。 したがって、直近の読み取りコンビネータ (a) は最初に "待機中 " 状態を通過します: a変数は次のコンビネータが現れるまで、または文字列の終わりに達するまで使用されませんが、どちらの場合もセレクタが完全に形成されたことを意味します。 この時点でのみ、 "old" コンビネータ (b) が適用され、新しいもの (a) に置き換えられます。 このコード自体は、その説明よりも明確です。

      int cursor = 0; //セレクタ文字列が開始された場所
      int i, n = StringLen(q);
      ushort p = 0;   //前の文字
      ushort a = 0;   //次/保留中のオペレータ
      ushort b = '/'; //現在の演算子、先頭からの ' root ' 記法
      string selector = "*"; //デフォルトでは、現在のシンプルなセレクタ
      int index = 0;  //オブジェクトの結果の配列内のポジション

      for(i = 0; i < n; i++)
      {
        ushort c = StringGetCharacter(q, i);
        if(isCombinator(c))
        {
          a = c;
          if(!isCombinator(p))
          {
            selector = StringSubstr(q, cursor, i - cursor);
          }
          else
          {
            //他のコンビネータの周りの空白を抑制
            a = MathMax(c, p);
          }
          cursor = i + 1;
        }
        else
        {
          if(isCombinator(p)) //アクション
          {
            index = result.getChildrenCount();
            
            SubSelectorArray selectors(selector);
            find(b, &selectors, result);
            b = a;
            
            // ' インデックス ' までのポジションに古い結果を削除することができます
            result.removeFirst(index);
          }
        }
        p = c;
      }
      
      if(cursor < i) //アクション
      {
        selector = StringSubstr(q, cursor, i - cursor);
        
        index = result.getChildrenCount();
        
        SubSelectorArray selectors(selector);
        find(b, &selectors, result);
        result.removeFirst(index);
      }
      
      return result;
    }

' Cursor ' 変数は常に、シンプルなセレクタを持つ文字列が始まる最初の文字を指します (すなわち、直前のコンビネータの直後または文字列の先頭にある文字)。 別のコンビネータが見つかったら、「カーソル」の部分文字列を現在の文字 (i) から「セレクタ」変数にコピーします。

時にはコンビネータが連続している: 通常、他の連結文字がスペースを囲むときに起こるかもしれませんが、スペース自体もコンビネータです。 たとえば、 "td > span " と "td > span " は等価ですが、読みやすくするために2番目のケースにはスペースが挿入されています。 このような状況は、次の行で処理されます。

a = MathMax(c, p);

両方がコンビネータである場合、現在と前の文字を比較します。 その後、スペースが最小のコードがあるという事実に基づいて、 "古い " コンビネータを選択します。 コンビネータは次のように明らかに定義されています:

ushort combinators[] =
{
  ' ', '+', '>', '~'
};

この配列に文字が含まれているかどうかのチェックは、シンプルな isCombinator ヘルパー関数によって実行されます。

スペース以外の行に2つのコンビネータがある場合、セレクタは誤っており、動作は仕様書で定義されていません。 ただし、今回のコードはパフォーマンスを低下させず、一貫性のある動作を提案します。

前の文字がコンビネータであったが、現在の文字がコンビネータでない場合、実行は「アクション」コメントでマークされたブランチに落ちます。 この瞬間に選択された DomElements の配列の現在のサイズを暗記します。

index = result.getChildrenCount();

配列は最初は空で、index = 0 です。

現在のシンプルセレクタ、すなわち ' セレクタ ' 文字列に対応するセレクタオブジェクトの配列を作成します。

SubSelectorArray selectors(selector);

その後、さらに検討される ' find ' メソッドを呼び出します。

find(b, &selectors, result);

その中にコンビネータ文字を渡します (上記のコンビネータ、すなわち b 変数から) だけでなく、シンプルなセレクタと結果を出力するための配列でなければなりません。

その後、コンビネータのキューを前方に移動し、最後に見つかったコンビネータ (まだ処理されていない) を a 変数からbにコピーし、結果から「find」の呼び出しの前に使用可能であったすべてのものを使用して削除します。

result.removeFirst(index);

RemoveFirst メソッドは、DomIterator で定義されます。 シンプルなタスクを実行します: 配列から指定された数までの最初の要素をすべて削除します。 各連続したシンプルなセレクタ処理の過程で、要素の選択条件を絞り込み、先に選択したすべてのものがもはや有効ではなく、新たに追加された要素 (狭い条件を満たす) が ' index ' で始まるためです。.

同様の処理 (' action ' コメントでマークされた) もインプット文字列の終わりに達した後に行われます。 この場合、直近の保留中のコンビネータは、行の残りの部分と一緒に (' カーソル ' ポジションから) 処理されるべきです。

ここで、' find ' メソッドを考えてみましょう。

    bool find(const ushort op, const SubSelectorArray *selectors, DomIterator *output)
    {
      bool found = false;
      int i, n;

コンビネータ設定タグネスティング条件 (' ', ' > ') のいずれかがインプットされた場合、すべての子要素に対してチェックを再帰的に呼び出す必要があります。 このブランチでは、呼び出し元のメソッドの検索の開始時に使用する特別なコンビネータ "/" も考慮する必要があります。

      if(op == ' ' || op == '>' || op == '/')
      {
        n = ArraySize(children);
        for(i = 0; i < n; i++)
        {
          if(children[i].match(selectors))
          {
            if(op == '/')
            {
              found = true;
              output.addChild(GetPointer(children[i]));
            }

' Match ' メソッドは後で考慮されます。 オブジェクトが渡されたセレクタに対応するか、さもなければ false を返します。 検索の最初 (コンビネータ op = '/') では、まだ組み合わせがないので、セレクタルールを満たすすべてのタグが結果に追加されます (addChild)。

            else
            if(op == ' ')
            {
              DomElement *p = &this;
              while(p != NULL)
              {
                if(output.getChildIndex(p) != -1)
                {
                  found = true;
                  output.addChild(GetPointer(children[i]));
                  break;
                }
                p = p.parent;
              }
            }

連結子 ' ' の場合、現在の DomElement またはその親が既に ' output ' に存在するかどうかのチェックが行われます。 検索条件を満たす新しい子要素が、既に親にネストされていることを意味します。 これがコンビネータのタスクです。

コンビネータ ' > ' は同様の方法で動作しますが、即時に "relatives(似たもの) " だけを追跡し、現在の DomElement が中間結果で利用可能かどうかをチェックする必要があります。 ある場合、以前にコンビネータの左側にセレクタの条件によって「出力」に選択されており、その i 番目の子要素はちょうどコンビネータの右側にセレクタの条件を満たします。

            else //on = = ' > '
            {
              if(output.getChildIndex(&this) != -1)
              {
                found = true;
                output.addChild(GetPointer(children[i]));
              }
            }
          }

その後、同様のチェックを DOM ツリーの奥深くで実行する必要があるため、子要素に対しては ' find ' を再帰的に呼び出す必要があります。

          children[i].find(op, selectors, output);
        }
      }

結合子 ' + ' と ' ~ ' は、2つの要素が同じ親を参照するかどうかの条件を設定します。

      else
      if(op == '+' || op == '~')
      {
        if(CheckPointer(parent) == POINTER_DYNAMIC)
        {
          if(output.getChildIndex(&this) != -1)
          {

要素の1つは、左側のセレクタによって既に選択されている必要があります。 この条件が満たされている場合は、右側のセレクタの "siblings("兄弟") " をチェックします ( "siblings" は現在のノードの親の子です)。

            int q = parent.getChildIndex(&this);
            if(q != -1)
            {
              n = (op == '+') ? (q + 2) : parent.getChildrenCount();
              if(n > parent.getChildrenCount()) n = parent.getChildrenCount();
              for(i = q + 1; i < n; i++)
              {
                DomElement *m = parent.getChild(i);
                if(m.match(selectors))
                {
                  found = true;
                  output.addChild(m);
                }
              }
            }

' + ' と ' ~ ' の処理の差は次のとおりです。 ' + ' 要素は即時近傍でなければならず、' ~ ' を使用する場合、要素間には他に任意の数の "兄弟 " を使用できます。 したがって、このループは ' + '、つまり子要素の配列の次の要素に対して1回だけ実行されます。 ' Match ' 関数はループ内で再び呼び出されます (詳細については後述します)。

          }
        }
        for(i = 0; i < ArraySize(children); i++)
        {
          found = children[i].find(op, selectors, output) || found;
        }
      }
      return found;
    }

すべてのチェックが完了したら、次の DOM 要素ツリー階層レベルに移動して、子の ' find ' を呼び出します。

これがすべて ' find ' メソッドについてです。 「マッチ」関数を見てみましょう。 セレクタ実装の説明の直近の点です。

この関数は、インプットパラメータを通過したシンプルなセレクタのコンポーネントのチェーン全体を現在のオブジェクト内でチェックします。 ループ内の少なくとも1つのコンポーネントが要素プロパティと一致しない場合、チェックは失敗します。

    bool match(const SubSelectorArray *u)
    {
      bool matched = true;
      int i, n = u.size();
      for(i = 0; i < n && matched; i++)
      {
        if(u[i].type == 0) //タグ名と擬似クラス
        {
          if(u[i].value == "*")
          {
            //任意のタグ
          }

0型セレクタはタグ名または擬似クラスです。 任意のタグは、アスタリスクを含むセレクタに適します。 それ以外の場合は、セレクタ文字列をタグ名と比較する必要があります。

          else
          if(StringCompare(name, u[i].value) != 0)
          {
            matched = false;
          }

現在実装されている擬似クラスは、親の子要素の配列内の現在の要素の数に制限を設定するので、インデックスを分析します。

          else
          if(u[i].modifier == PseudoClassModifier::firstChild)
          {
            if(parent != NULL && parent.getChildIndex(&this) != 0)
            {
              matched = false;
            }
          }
          else
          if(u[i].modifier == PseudoClassModifier::lastChild)
          {
            if(parent != NULL && parent.getChildIndex(&this) != parent.getChildrenCount() - 1)
            {
              matched = false;
            }
          }
          else
          if(u[i].modifier == PseudoClassModifier::nthChild)
          {
            int x = (int)StringToInteger(u[i].param);
            if(parent != NULL && parent.getChildIndex(&this) != x - 1) //子は1から始まるカウントされます。
            {
              matched = false;
            }
          }
          else
          if(u[i].modifier == PseudoClassModifier::nthLastChild)
          {
            int x = (int)StringToInteger(u[i].param);
            if(parent != NULL && parent.getChildrenCount() - parent.getChildIndex(&this) - 1 != x - 1)
            {
              matched = false;
            }
          }
        }

セレクタ '. ' は、 "class " 属性に制限を課しています:

        else
        if(u[i].type == '.')
        {
          if(attributes.isKeyExisting("class"))
          {
            Container *c = attributes["class"];
            if(c == NULL || StringFind(" " + c.get<string>() + " ", " " + u[i].value + " ") == -1)
            {
              matched = false;
            }
          }
          else
          {
            matched = false;
          }
        }

セレクタ ' # ' は、 "id " 属性に制限を課しています:

        else
        if(u[i].type == '#')
        {
          if(attributes.isKeyExisting("id"))
          {
            Container *c = attributes["id"];
            if(c == NULL || StringCompare(c.get<string>(), u[i].value) != 0)
            {
              matched = false;
            }
          }
          else
          {
            matched = false;
          }
        }

セレクタ ' [' は、必要な属性の任意のセットの指定を可能にします。 また、値の厳密な比較に加えて、部分文字列 (サフィックス ' * ')、先頭 (' ^ ') および end (' $ ') の発生を確認することができます。

        else
        if(u[i].type == '[')
        {
          AttributesParser p;
          IndexMap hm;
          p.parseAll(u[i].value, hm);
          //属性は1つずつ選択されます: 要素 [attr1 = 値] [attr2 = 値]
          //マップには一度に1つの有効なペアのみをインクルードする必要があります。
          if(hm.getSize() > 0)
          {
            string key = hm.getKey(0);
            ushort suffix = StringGetCharacter(key, StringLen(key) - 1);
            
            if(suffix == '*' || suffix == '^' || suffix == '$') // contains, starts with, or ends with
            {
              key = StringSubstr(key, 0, StringLen(key) - 1);
            }
            else
            {
              suffix = 0;
            }
            
            if(hasAttribute(key) && attributes[key] != NULL)
            {
              string v = hm[0] != NULL ? hm[0].get<string>() : "";
              if(StringLen(v) > 0)
              {
                if(suffix == 0)
                {
                  if(key == "class")
                  {
                    matched &= (StringFind(" " + attributes[key].get<string>() + " ", " " + v + " ") > -1);
                  }
                  else
                  {
                    matched &= (StringCompare(v, attributes[key].get<string>()) == 0);
                  }
                }
                else
                if(suffix == '*')
                {
                  matched &= (StringFind(attributes[key].get<string>(), v) != -1);
                }
                else
                if(suffix == '^')
                {
                  matched &= (StringFind(attributes[key].get<string>(), v) == 0);
                }
                else
                if(suffix == '$')
                {
                  string x = attributes[key].get<string>();
                  if(StringLen(x) > StringLen(v))
                  {
                    matched &= (StringFind(x, v, StringLen(x) - StringLen(v)) == StringLen(v));
                  }
                }
              }
            }
            else
            {
              matched = false;
            }
          }
        }
      }
      
      return matched;

    }

"Class " 属性もサポートされ、ここで処理されることに注意してください。 また、'. ' と同様に、厳密な一致はチェックされませんが、他のクラスのセットの中でクラスが利用可能であることを確認します。 多くの場合、HTML では複数のクラスが1つの要素に割り当てられます。 この場合、クラスは、スペースで区切られた「クラス」で指定します。

中間結果をまとめましょう。 DomElement クラスに実装した querySelect メソッドは、完全な CSS セレクタを持つ文字列をパラメータとして受け取り、DomIterator オブジェクト、つまり見つかった一致する要素の配列を返します。 QuerySelect の内部では、CSS セレクタ文字列は、間のシンプルなセレクタとコンビネータ文字のシーケンスに分割されています。 各シンプルセレクタでは、指定されたコンビネータを持つ ' find ' メソッドが呼び出されます。 このメソッドは、結果の一覧を更新し、子要素を再帰的に呼び出します。 シンプルなセレクタコンポーネントと特定の要素のプロパティの比較は、' match ' メソッドで実行されます。

たとえば、querySelect メソッドを使用して、1つの CSS セレクタを使用してテーブルから行を選択し、特定のセルを分離するために、別の CSS セレクタを使用して各行の querySelect を呼び出すことができます。 テーブルの操作は頻繁に必要であるため、上記の方法を実装する DomElement クラスで tableSelect メソッドを作成してみましょう。 そのコードは簡略化された形式で提供します。

    IndexMap *tableSelect(const string rowSelector, const string &columSelectors[], const string &dataSelectors[])
    {

行セレクタは rowSelector パラメータで指定しますが、セルセレクタは columSelectors 配列で指定します。

すべての要素を選択したら、テキストや属性値などの情報を取得する必要があります。 DataSelectors を使用して、要素内の必要な情報のポジションを決定し、個々のデータ抽出方法をテーブル列ごとに使用することができます。

DataSelectors [i] が空の行である場合は、タグのテキストの内容を読みます (たとえば、開始と終了の部分の間で "100% "タグ "から<p>100%</p>"). DataSelectors [i] が行である場合は、属性名と見なしてこの値を使用します。

完全な実装を詳細に見てみましょう:

      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();

したがって、次の行 (e) を取得し、 (行) のコンテナマップを作成し、セルを追加し、次の列をループで実行します。

        for(int i = 0; i < ArraySize(columSelectors); i++)
        {
          DomIterator *d = e.querySelect(columSelectors[i]);

各行のオブジェクトで、適切なセレクタを使用して、セルオブジェクト (d) のリストを選択します。 見つかった各セルからデータを選択し、「行」マップに保存します。

          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;
    }

したがって、この出力では、マップのマップ、すなわち、第1の次元に沿った行番号と2番目の列番号を持つテーブルが得られます。 必要に応じて、tableSelect 関数を他のデータコンテナに合わせて調整することができます。

上記のすべてのクラスを適用するために、非トレードユーティリティEAが作成されました。

WebDataExtractor ユーティリティEA

このEAは、ウェブページからのデータを表形式の構造に変換し、結果を CSV ファイルに保存するために使用します。

このEAは、ソースデータ (WebRequest を使用してダウンロード可能なローカルファイルまたは web ページ)、行と列のセレクタ、および CSV ファイル名へのリンクを受け取った次のインプットパラメータを受け取りました。 主なインプットパラメータを以下に示します。

input string URL = "";
input string SaveName = "";
input string RowSelector = "";
input string ColumnSettingsFile = "";
input string TestQuery = "";
input string TestSubQuery = "";

URL上、ウェブページのアドレスを指定するか(http:// か https://で始まる)、ローカルディレクトリのHMTMファイル名を指定します。

SaveName では、結果を含む CSV ファイルの名前が通常モードで指定されます。 しかし、また、他の目的に使用することができます: セレクタの後のデバッグにダウンロードしたページを保存します。 このモードでは、次のパラメータを空のままにする必要があります: RowSelector、CSS 行セレクタは通常指定されています。

列セレクタがあるので、別々の CSV セットファイルに設定され、ColumnSettingsFile パラメータで指定されます。 このファイル形式は次のとおりです。

最初の行はヘッダで、トレーリングの各行には別のフィールド (テーブル行のデータ列) が記述されています。

このファイルには、名前、CSS セレクタ、データロケータの3つの列が必要です。

TestQuery および TestSubQuery パラメータを使用すると、行と列に対してセレクタをテストできますが、結果はログに出力されますが、CSV には保存せず、すべての列の設定ファイルは使用しません。

ここでは、簡単な形でのExpert Advisorの主な動作関数です。

int process()
{
  string xml;
  
  if(StringFind(URL, "http://") == 0 || StringFind(URL, "https://") == 0)
  {
    xml = ReadWebPageWR(URL);
  }
  else
  {
    Print("Reading html-file ", URL);
    int h = FileOpen(URL, FILE_READ|FILE_TXT|FILE_SHARE_WRITE|FILE_SHARE_READ|FILE_ANSI, 0, CP_UTF8);
    if(h == INVALID_HANDLE)
    {
      Print("Error reading file '", URL, "': ", GetLastError());
      return -1;
    }
    StringInit(xml, (int)FileSize(h));
    while(!FileIsEnding(h))
    {
      xml += FileReadString(h) + "\n";
    }
    //xml = FileReadString (h, (int) ファイルサイズ (h));-バイナリファイルで4095バイトの制限があります!
    FileClose(h);
  }
  ...

したがって、ファイルから HTML ページを読んだり、インターネットからダウンロードしました。 ここで、ドキュメントを DOM オブジェクトの階層に変換するために、HtmlParser オブジェクトを作成して解析を開始します。

  HtmlParser p;
  DomElement *document = p.parse(xml);

テストセレクタが指定されている場合は、querySelect 呼び出しによって処理します。

  if(TestQuery != "")
  {
    Print("Testing query, subquery: '", TestQuery, "', '", TestSubQuery, "'");
    DomIterator *r = document.querySelect(TestQuery);
    r.printAll();
    
    if(TestSubQuery != "")
    {
      r.rewind();
      while(r.hasNext())
      {
        DomElement *e = r.next();
        DomIterator *d = e.querySelect(TestSubQuery);
        d.printAll();
        delete d;
      }
    }
    
    delete r;
    return(0);
  }

通常の操作モードでは、列の設定ファイルを読み、tableSelect 関数を呼び出します。

  string columnSelectors[];
  string dataSelectors[];
  string headers[];
  
  if(!loadColumnConfig(columnSelectors, dataSelectors, headers)) return(-1);
  
  IndexMap *data = document.tableSelect(RowSelector, columnSelectors, dataSelectors);

結果を保存するための CSV ファイルが指定されている場合は、' data ' マップにこのタスクを実行させます。

  if(StringLen(SaveName) > 0)
  {
    Print("Saving data as CSV to ", SaveName);
    int h = FileOpen(SaveName, FILE_WRITE|FILE_CSV|FILE_ANSI, '\t', CP_UTF8);
    if(h == INVALID_HANDLE)
    {
      Print("Error writing ", data.getSize() ," rows to file '", SaveName, "': ", GetLastError());
    }
    else
    {
      FileWriteString(h, StringImplodeExt(headers, ",") + "\n");
      
      FileWriteString(h, data.asCSVString());
      FileClose(h);
      Print((string)data.getSize() + " rows written");
    }
  }
  else
  {
    Print("\n" + data.asCSVString());
  }
  
  delete data;
  
  return(0);
}

さて、EAの実用化に進みましょう。


実用化

トレーダーは、多くの場合、テストレポートや MetaTrader によって生成されたトレードレポートなどの標準的な HTML ファイルを扱います。 時々、他のトレーダーからこのようなファイルを受信したり、インターネットからダウンロードして、さらに分析のチャート上のデータを可視化したいです。 この目的に HTML からのデータは、(シンプルなケースでは CSV 形式に) 表形式のビューに変換する必要があります。

ユーティリティで CSS セレクタは、このプロセスを自動化することができます。

HTML ファイルの内部を見てみましょう。 以下は MetaTrader5 トレーディングレポートの HTML コードの外観と一部です (ReportHistory ファイルは以下に添付されています)。

トレードレポートの外観と HTML コードの一部

トレードレポートの外観と HTML コードの一部

そして今ここに MetaTrader5 テストレポートの HTML コードの外観と一部があります (テスター. HTML ファイルは下に添付されています)。

テスターレポートの外観と HTML コードの一部

テスターレポートの外観と HTML コードの一部

上記の図の外観によると、トレードレポートには2つのテーブルがあります: オーダーとディール。 ただし、内部レイアウトからは、単一のテーブルであることがわかります。 すべての可視ヘッダと分割線は、表のセルのスタイルによって形成されます。 オーダーとトレードを区別し、各サブテーブルを別々の CSV ファイルに保存する方法を学ぶ必要があります。

最初のパーツと2番目の部分の差は、列数: オーダーの場合は11列、トレードの場合は13列です。 残念ながら、CSS 標準では、子要素の数または内容 (この例では、テーブルセル、' td ' タグ) に基づいて、親要素 (ここではテーブル行、' tr ' タグ) を選択する条件を設定することはできません。 したがって、場合によっては、標準の方法で必須要素を選択することはできません。 しかし、セレクタの独自の実装を開発しているので、子要素の数の特別な非標準セレクタを追加することができます。 これは新しい擬似クラスになります。 ": N 番目の子 (n) " と類推して、 "のように設定してみましょう。

次のセレクタは、オーダー行を選択するために使用できます。

tr:has-n-children(11)

ただし、このセレクタはデータ行に加えてテーブルヘッダを選択するため、問題の解決策全体ではありません。 これを削除してみましょう。 データ行の色分けに注意してください-bgcolor 属性がために設定されており、色の値は偶数と奇数の行 (#FFFFFF と #F7F7F7) を交互にします。 Bgcolor 属性はヘッダにも使用しますが、その値は #E5F0FC です。 したがって、データ行の明るい色は、 "#F " で始まる bgcolor で始まります。 この条件をセレクタに追加してみましょう:

tr:has-n-children(11)[bgcolor^="#F"]

このセレクタは、オーダーを含むすべての行を正しく決定します。

各オーダーのパラメータは、行のセルから読み取ることができます。 これを行うには、設定ファイル ReportHistoryOrders 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),

このファイル内のすべてのフィールドは、シーケンス番号によって識別されます。 それ以外の場合は、属性とクラスでよりスマートなセレクタが必要になることがあります。

取引のテーブルを取得するには、row セレクタで子要素の数を13に置き換えるだけです。

tr:has-n-children(13)[bgcolor^="#F"]

設定ファイル ReportHistoryDeals は以下に添付されています。

ここで、次のインプットパラメータを使用して WebDataExtractor を起動します (webdataex-report1 ファイルが添付されています)。

URL=ReportHistory.html
SaveName=ReportOrders.csv
RowSelector=tr:has-n-children(11)[bgcolor^="#F"]
ColumnSettingsFile=ReportHistoryOrders.cfg.csv

ソースの HTML レポートに対応する結果の ReportOrders ファイルを受け取ります。

CSS セレクタをトレードレポートに適用した結果の CSV ファイル

CSS セレクタをトレードレポートに適用した結果の CSV ファイル

取引のテーブルを取得するには、webdataex-report2.set から添付された設定を使用します。

作成したセレクタはテスターレポートにも適します。 添付の webdataex-tester1.set と webdataex-tester2.set を使用すると、サンプルの HTML repost Tester. html を CSV ファイルに変換できます。

注意! MetaTrader で生成された HTML ファイルだけでなく、多くのウェブページのレイアウトは随時変更される可能性があります。 このため、外部プレゼンテーションがほぼ同じであっても、一部のセレクタは適用できなくなります。 この場合、HTML コードを再分析し、それに応じて CSS セレクタを変更する必要があります。

次に、MetaTrader4 テスターレポートのコンバージョンを見てみましょう。CSS セレクタを選択する際の興味深いテクニックを実証できます。 確認には、添付の StrategyTester-ecn-1.htm を使用します。

これらのファイルは、2つのテーブルがありますがあります: テスト結果とトレードトレード操作と他の1つ。 2番目のテーブルを選択するには、セレクタ "table ~ table" を使用します。 操作テーブルにはヘッダが含まれているため、最初の行は省略します。 これは、セレクタ "tr + tr " を使用して行うことができます。

組み合わせて、タスク行を選択するためのセレクタを取得します。

table ~ table tr + tr

これは実際に次のことを意味します。テーブルの後にテーブルを選択します (つまり、2番目のテーブルの内側で、前の行を持つ各行を選択します。

セルから取引パラメータを抽出するための設定は、ファイル test-report-MetaTrader4.csv で使用できます。 日付フィールドは、クラスセレクタによって処理されます。

DateTime,td.msdate,

つまり、class = "msdate " 属性を使用して td タグを詳しく見ることができます。

ユーティリティの完全な設定ファイルは webdataex-tester-MetaTrader4.set です。

追加の CSS セレクタの使用とセットアップの例は、 WebDataExtractor の考察のページで提供されています。

このユーティリティは、より多くのことを行うことができます。

特定の web ページの CSS セレクタの設定についてサポートが必要な場合は、WebDataExtractor (MetaTrader4MetaTrader5) を購入し、プロダクトサポートの一部として推奨事項を受け取ることができます。 ただし、ソースコードの可用性により、関数全体を使用し、必要に応じて拡張することができます。 これに関しては無料です。


結論

今回は、WEBドキュメントの解釈における主要な基準の1つである CSS セレクタの技術を検討しました。 MQL で最も一般的に使用する CSS セレクタの実装により、標準の MetaTrader ドキュメントを含むあらゆる HTML ページを、サードパーティのソフトウェアを使用せずに構造化データに柔軟に設定および変換することができます。

また、web ドキュメントを処理するための汎用性の高いツールについてまだ考慮していません。 メタトレーダーは HTML だけでなく XML 形式も処理できるため、このようなツールは便利です。 トレーダーは、特に XPath と XSLT に興味を持つことができます。 これらの形式は、web 標準に基づいてトレーディングシステムを自動化するというアイデアを実装するためのさらなるステップとして役立ちます。 MQL での CSS セレクタのサポートは、この目標への第一歩に過ぎません。