Html Agility Pack erhält alle Elemente nach Klassen

c# html html-agility-pack

Frage

Ich begehe einen Html Agility Pack und habe Probleme den richtigen Weg zu finden.

Beispielsweise:

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

Natürlich können Sie Klassen zu viel mehr als divs hinzufügen, also habe ich das versucht.

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

Aber das behandelt nicht die Fälle, in denen Sie mehrere Klassen hinzufügen und "float" ist nur eine davon.

class="className float anotherclassName"

Gibt es einen Weg, all dies zu bewältigen? Ich möchte im Grunde alle Knoten auswählen, die eine Klasse = und Float enthalten.

** Antwort wurde in meinem Blog mit einer vollständigen Erklärung dokumentiert unter: Html Agility Pack Holen Sie sich alle Elemente nach Klasse

Akzeptierte Antwort

(Aktualisiert am 17.03.2017)

Das Problem:

Das Problem, wie Sie entdeckt haben, ist , dass String.Contains kein Wort-Grenze Prüfung durchführen, so Contains("float") wird wieder true für beide „foo float bar“ (richtig) und „unfloating“ (das ist falsch).

Die Lösung besteht darin, sicherzustellen, dass "Float" (oder wie auch immer der gewünschte Klassenname lautet) an beiden Enden neben einer Wortgrenze angezeigt wird. Eine Wortgrenze ist entweder der Anfang (oder das Ende) einer Zeichenfolge (oder Zeile), Leerzeichen, bestimmte Interpunktion usw. In den meisten regulären Ausdrücken ist dies \b . Der Regex, den Sie möchten, lautet also einfach: \bfloat\b .

Ein Nachteil der Verwendung einer Regex Instanz besteht darin, dass sie langsam ausgeführt werden kann, wenn Sie die .Compiled Option nicht verwenden - und sie können langsam kompilieren. Sie sollten also die Regex-Instanz zwischenspeichern. Dies ist schwieriger, wenn der Klassenname, den Sie zur Laufzeit suchen, geändert wird.

Alternativ können Sie einen String nach Wörtern nach Wortgrenzen durchsuchen, ohne einen String.Split indem Sie den String.Split als C # -Stringverarbeitungsfunktion implementieren. Dabei ist darauf zu achten, dass keine neue String.Split oder andere Objektzuordnung erfolgt (z. B. ohne Verwendung von String.Split ).

Ansatz 1: Verwenden eines regulären Ausdrucks:

Angenommen, Sie möchten nur nach Elementen mit einem einzelnen, zur Entwurfszeit angegebenen Klassennamen suchen:

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", "") ) );
    }
}

Wenn Sie zur Laufzeit einen einzelnen Klassennamen auswählen müssen, können Sie einen regulären Ausdruck erstellen:

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", "") ) );
}

Wenn Sie mehrere Klassennamen haben und alle übereinstimmen möchten, können Sie ein Array von Regex Objekten erstellen und sicherstellen, dass sie alle übereinstimmen, oder sie mithilfe von Lookarounds zu einem einzigen Regex kombinieren. Dies führt jedoch zu äußerst komplizierten Ausdrücken. mit einem Regex[] ist es wahrscheinlich besser:

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", "") )
            )
        );
}

Ansatz 2: Verwenden des String-Abgleichs ohne Regex:

Der Vorteil der Verwendung einer benutzerdefinierten C # -Methode zum Ausführen eines Zeichenfolgenabgleichs anstelle eines regulären Ausdrucks ist hypothetisch eine schnellere Leistung und ein geringerer Speicherbedarf (obwohl Regex unter Umständen schneller ist - profilieren Sie Ihren Code immer zuerst, Kinder!)

Diese Methode unten: CheapClassListContains bietet eine Funktion zum schnellen CheapClassListContains der Wortgrenzen, die auf die Übereinstimmung von Strings angewendet werden kann. Diese Funktion kann auf dieselbe Weise wie 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;
}

Ansatz 3: Verwenden einer CSS Selector-Bibliothek:

HtmlAgilityPack ist etwas stagniert und unterstützt .querySelector und .querySelectorAll , aber es gibt Bibliotheken von Drittanbietern, die HtmlAgilityPack damit erweitern: nämlich Fizzler und CssSelectors . Sowohl Fizzler als auch CssSelectors implementieren QuerySelectorAll , sodass Sie es wie QuerySelectorAll können:

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

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

Bei Laufzeitklassen:

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

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

    return doc.QuerySelectorAll( selector  );
}

Beliebte Antwort

Sie können Ihr Problem lösen, indem Sie die Funktion 'contains' in Ihrer Xpath-Abfrage verwenden:

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

Um dies in einer Funktion wiederzuverwenden, gehen Sie wie folgt vor:

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


Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum