1. ホーム
  2. c#

Html Agility Pack クラスごとに全要素を取得する

2023-10-14 03:10:06

質問

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

例えば

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

しかし、div以外にもクラスを追加できることは明らかなので、次のようにしてみました。

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

しかし、これでは、このように複数のクラスを追加して、"float"がその一つに過ぎない場合、対応できません...。

class="className float anotherclassName"

このすべてを処理する方法はありますか? 私は基本的にクラス=を持ち、floatを含むすべてのノードを選択したいです。

**回答は、私のブログで完全な説明とともに文書化されています。 Html Agility Pack クラスによってすべての要素を取得する

どのように解決するのですか?

(2018-03-17更新)

問題の内容

問題は、お気づきのように String.Contains は単語境界のチェックを行わないので Contains("float") が返されます。 true を返します。これは "foo float bar" (正しい) と "unfloating" (正しくない) の両方です。

解決策は、"float" (または、希望するクラス名が何であれ) を確実に表示することです。 が単語境界と一緒に表示されるようにすることです。 を両端に表示することです。単語境界とは、文字列(または行)の開始(または終了)、空白、特定の句読点などのことです。ほとんどの正規表現では、これは \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 にまとめるか、ルックアラウンドを使用しますが、この結果 という恐ろしいほど複雑な表現になります。 - になってしまうので 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  );
}