web-dev-qa-db-ja.com

HTML Agility Packはクラスごとにすべての要素を取得します

私はhtmlアジリティパックに突き刺されて、これについて正しい方法を見つけるのに苦労しています。

例えば:

var findclasses = _doc.DocumentNode.Descendants("div").Where(d => d.Attributes.Contains("class"));

ただし、明らかにdivよりも多くのクラスを追加できるので、これを試しました。

var allLinksWithDivAndClass = _doc.DocumentNode.SelectNodes("//*[@class=\"float\"]");

ただし、複数のクラスを追加し、「float」がこのようなクラスの1つである場合は処理できません。

class="className float anotherclassName"

これをすべて処理する方法はありますか?基本的に、class =を持ち、floatを含むすべてのノードを選択します。

** Answerは私のブログで完全な説明とともに文書化されています: Html Agility Packクラスごとにすべての要素を取得

71
Adam

(2018-03-17に更新)

問題:

問題は、お気付きのとおり、String.ContainsはWord境界チェックを実行しないため、Contains("float")は "foo float bar"(正しい)と "unfloating"(正しくない)の両方に対してtrueを返します。

解決策は、両端に「フロート」(または希望するクラス名)が表示されるようにすることですWord境界と一緒に。単語境界は、文字列(または行)の開始(または終了)、空白、特定の句読点などのいずれかです。ほとんどの正規表現では、これは\bです。したがって、必要な正規表現は、単に\bfloat\bです。

Regexインスタンスを使用する場合のマイナス面は、.Compiledオプションを使用しない場合、実行が遅くなる可能性があり、コンパイルが遅くなる可能性があることです。したがって、正規表現インスタンスをキャッシュする必要があります。探しているクラス名が実行時に変更される場合、これはより困難です。

または、C#文字列処理関数として正規表現を実装することにより、正規表現を使用せずに単語境界で単語の文字列を検索できます。新しい文字列または他のオブジェクトの割り当てが発生しないように注意してください(String.Splitを使用しないなど)。

アプローチ1:正規表現を使用する:

設計時に指定された単一のクラス名を持つ要素のみを検索するとします。

class Program {

    private static readonly Regex _classNameRegex = new Regex( @"\bfloat\b", RegexOptions.Compiled );

    private static IEnumerable<HtmlNode> GetFloatElements(HtmlDocument doc) {
        return doc
            .Descendants()
            .Where( n => n.NodeType == NodeType.Element )
            .Where( e => e.Name == "div" && _classNameRegex.IsMatch( e.GetAttributeValue("class", "") ) );
    }
}

実行時に単一のクラス名を選択する必要がある場合、正規表現を作成できます。

private static IEnumerable<HtmlNode> GetElementsWithClass(HtmlDocument doc, String className) {

    Regex regex = new Regex( "\\b" + Regex.Escape( className ) + "\\b", RegexOptions.Compiled );

    return doc
        .Descendants()
        .Where( n => n.NodeType == NodeType.Element )
        .Where( e => e.Name == "div" && regex.IsMatch( e.GetAttributeValue("class", "") ) );
}

複数のクラス名があり、それらすべてを一致させたい場合は、Regexオブジェクトの配列を作成し、それらがすべて一致することを確認するか、ルックアラウンドを使用して単一のRegexに結合できますが、この結果は in恐ろしく複雑な式 -したがって、Regex[]を使用する方がおそらく良いでしょう:

using System.Linq;

private static IEnumerable<HtmlNode> GetElementsWithClass(HtmlDocument doc, String[] classNames) {

    Regex[] exprs = new Regex[ classNames.Length ];
    for( Int32 i = 0; i < exprs.Length; i++ ) {
        exprs[i] = new Regex( "\\b" + Regex.Escape( classNames[i] ) + "\\b", RegexOptions.Compiled );
    }

    return doc
        .Descendants()
        .Where( n => n.NodeType == NodeType.Element )
        .Where( e =>
            e.Name == "div" &&
            exprs.All( r =>
                r.IsMatch( e.GetAttributeValue("class", "") )
            )
        );
}

アプローチ2:非正規表現文字列照合を使用する:

正規表現の代わりにカスタムC#メソッドを使用して文字列マッチングを行う利点は、仮想的にパフォーマンスが向上し、メモリ使用量が減少することです(ただし、状況によってはRegexの方が速い場合があります-必ず最初にコードをプロファイリングしてください!)

以下のメソッド:CheapClassListContainsは、regex.IsMatchと同じ方法で使用できる高速なワード境界チェック文字列照合関数を提供します。

private static IEnumerable<HtmlNode> GetElementsWithClass(HtmlDocument doc, String className) {

    return doc
        .Descendants()
        .Where( n => n.NodeType == NodeType.Element )
        .Where( e =>
            e.Name == "div" &&
            CheapClassListContains(
                e.GetAttributeValue("class", ""),
                className,
                StringComparison.Ordinal
            )
        );
}

/// <summary>Performs optionally-whitespace-padded string search without new string allocations.</summary>
/// <remarks>A regex might also work, but constructing a new regex every time this method is called would be expensive.</remarks>
private static Boolean CheapClassListContains(String haystack, String needle, StringComparison comparison)
{
    if( String.Equals( haystack, needle, comparison ) ) return true;
    Int32 idx = 0;
    while( idx + needle.Length <= haystack.Length )
    {
        idx = haystack.IndexOf( needle, idx, comparison );
        if( idx == -1 ) return false;

        Int32 end = idx + needle.Length;

        // Needle must be enclosed in whitespace or be at the start/end of string
        Boolean validStart = idx == 0               || Char.IsWhiteSpace( haystack[idx - 1] );
        Boolean validEnd   = end == haystack.Length || Char.IsWhiteSpace( haystack[end] );
        if( validStart && validEnd ) return true;

        idx++;
    }
    return false;
}

アプローチ3:CSSセレクターライブラリの使用:

HtmlAgilityPackは多少停滞しており、.querySelectorおよび.querySelectorAllをサポートしていませんが、HtmlAgilityPackを拡張するサードパーティライブラリがあります。つまり、 Fizzler および CssSelectors です。 FizzlerとCssSelectorsは両方ともQuerySelectorAllを実装しているため、次のように使用できます。

private static IEnumerable<HtmlNode> GetDivElementsWithFloatClass(HtmlDocument doc) {

    return doc.QuerySelectorAll( "div.float" );
}

ランタイム定義のクラスの場合:

private static IEnumerable<HtmlNode> GetDivElementsWithClasses(HtmlDocument doc, IEnumerable<String> classNames) {

    String selector = "div." + String.Join( ".", classNames );

    return doc.QuerySelectorAll( selector  );
}
92
Dai

以下のように、Xpathクエリ内で「含む」関数を使用して問題を解決できます。

var allElementsWithClassFloat = 
   _doc.DocumentNode.SelectNodes("//*[contains(@class,'float')]")

これを関数で再利用するには、次のようなことを行います。

string classToFind = "float";    
var allElementsWithClassFloat = 
   _doc.DocumentNode.SelectNodes(string.Format("//*[contains(@class,'{0}')]", classToFind));
87
Ryan McCarty

私は自分のプロジェクトでこの拡張方法をよく使いました。それが皆さんの助けになることを願っています。

public static bool HasClass(this HtmlNode node, params string[] classValueArray)
    {
        var classValue = node.GetAttributeValue("class", "");
        var classValues = classValue.Split(' ');
        return classValueArray.All(c => classValues.Contains(c));
    }
3
Hung Cao
public static List<HtmlNode> GetTagsWithClass(string html,List<string> @class)
    {
        // LoadHtml(html);           
        var result = htmlDocument.DocumentNode.Descendants()
            .Where(x =>x.Attributes.Contains("class") && @class.Contains(x.Attributes["class"].Value)).ToList();          
        return result;
    }      
0
hadi.sh