Html Agility Pack erhält alle Elemente nach Klasse

74

Ich mache einen Stich in das HTML-Agility-Pack und habe Probleme, den richtigen Weg zu finden, um dies zu erreichen.

Zum Beispiel:

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 es versucht.

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

Dies ist jedoch nicht der Fall, wenn Sie mehrere Klassen hinzufügen und "float" nur eine davon ist.

class="className float anotherclassName"

Gibt es eine Möglichkeit, mit all dem umzugehen? Grundsätzlich möchte ich alle Knoten auswählen, die eine Klasse = haben und float enthalten.

** Die Antwort wurde in meinem Blog mit einer vollständigen Erklärung unter folgender Adresse dokumentiert: HTML Agility Pack Alle Elemente nach Klasse abrufen

Adam
quelle

Antworten:

94

(Aktualisiert am 17.03.2018)

Das Problem:

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

Die Lösung besteht darin, sicherzustellen, dass "float" (oder was auch immer Ihr gewünschter Klassenname ist) an beiden Enden neben einer Wortgrenze erscheint . Eine Wortgrenze ist entweder der Anfang (oder das Ende) einer Zeichenfolge (oder Zeile), ein Leerzeichen, eine bestimmte Interpunktion usw. In den meisten regulären Ausdrücken ist dies der Fall \b. Der gewünschte Regex lautet also einfach : \bfloat\b.

Ein Nachteil bei der Verwendung einer RegexInstanz ist, dass die Ausführung langsam sein kann, wenn Sie die .CompiledOption nicht verwenden - und dass die Kompilierung langsam sein kann. Sie sollten also die Regex-Instanz zwischenspeichern. Dies ist schwieriger, wenn sich der gesuchte Klassenname zur Laufzeit ändert.

Alternativ können Sie eine Zeichenfolge nach Wörtern durch Wortgrenzen durchsuchen, ohne einen regulären Ausdruck zu verwenden, indem Sie den regulären Ausdruck als C # -String-Verarbeitungsfunktion implementieren, wobei Sie darauf achten, keine neue Zeichenfolge oder andere Objektzuweisung zu verursachen (z String.Split. B. nicht zu verwenden ).

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 Sie wollen alle von ihnen entsprechen, könnten Sie eine Reihe von erstellen RegexObjekte und sicherzustellen , dass sie alle passenden sind, oder sie zu einem einzigen kombinieren Regexmit lookarounds, aber diese Ergebnisse in horrend kompliziert Ausdrücke - so mit a Regex[]ist 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 von Nicht-Regex-Zeichenfolgenabgleich:

Der Vorteil der Verwendung einer benutzerdefinierten C # -Methode für den String-Abgleich anstelle eines regulären Ausdrucks ist die hypothetisch schnellere Leistung und die geringere Speichernutzung ( Regexkann jedoch unter bestimmten Umständen schneller sein - profilieren Sie Ihren Code immer zuerst, Kinder!).

Die folgende Methode: CheapClassListContainsBietet eine schnelle Zeichenfolgenübereinstimmungsfunktion zur Überprüfung der Wortgrenzen, die auf die gleiche Weise verwendet werden kann 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-Auswahlbibliothek:

HtmlAgilityPack ist etwas stagniert nicht unterstützt .querySelectorund .querySelectorAll, aber es gibt Bibliotheken von Drittanbietern , die HtmlAgilityPack mit ihm verlängern: nämlich Fizzler und CssSelectors . Sowohl Fizzler als auch CssSelectors implementieren QuerySelectorAll, sodass Sie es folgendermaßen verwenden können:

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

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

Mit zur Laufzeit definierten Klassen:

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

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

    return doc.QuerySelectorAll( selector  );
}
Dai
quelle
1
Entfernen Sie dann das Prädikat "div".
Dai
3
Rufen Sie einfach .Descendants ()
Lloyd Powell
14
Contains()existiert nicht auf dem Attribut, also ersetzen Sie d.Attributes["class"].Contains("float")durchd.Attributes["class"].Value.Split(' ').Any(b => b.Equals("float"))
maxp
2
Gäbe es eine Klasse mit dem Namen floatingdann Value.Contains("float")würde auch die Match
tic
1
@RobertOschler CheapClassListContainsist möglicherweise billiger als ein regulärer Ausdruck und implementiert dieselbe Logik - aber ja, das ist auch eine Option.
Dai
92

Sie können Ihr Problem mithilfe der Funktion "Enthält" in Ihrer Xpath-Abfrage wie folgt lösen:

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));
Ryan McCarty
quelle
Was wäre der Objekttyp allElementsWithClassFloat?
Adromil Balais
allElementsWithClassFloatist eine HtmlNodeCollection
feztheforeigner
Anstelle von string.Format können Sie auch$"//*[contains(@class,'{classToFind}')]"
feztheforeigner
5
Was passiert, wenn Sie eine Klasse mit dem Namen float-xs haben?
Sameera Kumarasingha
@SameeraKumarasingha Die Klassen 'float-xs' und 'unfloating' werden beide in die allElementsWithClassFloatListe aufgenommen. Bitte werfen Sie stattdessen einen Blick auf @ Dais
webStuff
4

Ich habe diese Erweiterungsmethode in meinem Projekt häufig verwendet. Hoffe, es wird einem von euch helfen.

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));
    }
Hung Cao
quelle
3
Verwenden ToLower()Sie diese Option nicht, wenn Sie den IgnoreCase-Vergleich wirklich möchten. Das Bestehen StringComparison.CultureIgnoreCaseist sauberer und zeigt eine explizitere Absicht.
Pauli Østerø
0
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;
    }      
hadi.sh
quelle
-7

Sie können das folgende Skript verwenden:

var findclasses = _doc.DocumentNode.Descendants("div").Where(d => 
    d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("float")
);
nguyen.huu.duy
quelle