Html Agility Pack ottiene tutti gli elementi per classe

c# html html-agility-pack

Domanda

Sto prendendo una pugnalata al pacchetto di agilità html e ho difficoltà a trovare la strada giusta da fare per questo.

Per esempio:

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

Tuttavia, ovviamente puoi aggiungere classi molto di più delle div, quindi ho provato questo ..

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

Ma questo non gestisce i casi in cui si aggiungono più classi e "float" è solo uno di loro come questo ..

class="className float anotherclassName"

C'è un modo per gestire tutto questo? Fondamentalmente voglio selezionare tutti i nodi che hanno una classe = e contiene float.

** La risposta è stata documentata sul mio blog con una spiegazione completa su: Html Agility Pack Ottieni tutti gli elementi per classe

Risposta accettata

(Aggiornato 2018-03-17)

Il problema:

Il problema, come hai notato, è che String.Contains non esegue un controllo dei limiti di parola, quindi Contains("float") restituirà true sia per "foo float bar" (corretto) che "unfloating" (che è errato).

La soluzione è assicurarsi che "float" (o qualunque sia il nome di classe desiderato) appaia accanto a un limite di parole ad entrambe le estremità. Un limite di parole è l'inizio (o la fine) di una stringa (o linea), uno spazio bianco, una certa punteggiatura, ecc. Nella maggior parte delle espressioni regolari questo è \b . Quindi la regex che vuoi è semplicemente: \bfloat\b .

Uno svantaggio nell'usare un'istanza Regex è che possono essere lenti da eseguire se non si utilizza l'opzione .Compiled e possono essere lenti a compilare. Quindi dovresti memorizzare nella cache l'istanza regex. Questo è più difficile se il nome della classe che stai cercando cambia in fase di esecuzione.

In alternativa puoi cercare una stringa per parole in base a limiti di parole senza usare String.Split implementando la regex come una funzione di elaborazione di stringhe C #, facendo attenzione a non causare alcuna nuova stringa o altra allocazione di oggetto (ad esempio non usando String.Split ).

Approccio 1: utilizzo di un'espressione regolare:

Supponiamo di voler cercare elementi con un solo nome di classe specifico per la fase di progettazione:

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

Se è necessario scegliere un singolo nome di classe in fase di esecuzione, è possibile creare un'espressione regolare:

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

Se hai più nomi di classe e vuoi abbinarli tutti, puoi creare una serie di oggetti Regex e assicurarti che siano tutti corrispondenti, o combinarli in un unico Regex usando i lookaround, ma questo si traduce in espressioni orribilmente complicate - quindi usare un Regex[] è probabilmente meglio:

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

Approccio 2: utilizzo della corrispondenza delle stringhe non regolari:

Il vantaggio dell'utilizzo di un metodo C # personalizzato per eseguire la corrispondenza delle stringhe al posto di un'espressione regolare è una prestazione ipoteticamente più rapida e un utilizzo ridotto della memoria (sebbene Regex potrebbe essere più veloce in alcune circostanze - profila sempre prima il codice, figli!)

Questo metodo di seguito: CheapClassListContains fornisce una rapida funzione di corrispondenza delle stringhe di controllo delle parole che può essere utilizzata allo stesso modo di 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;
}

Approccio 3: utilizzo di una libreria di selettori CSS:

HtmlAgilityPack è un po 'stagnante non supporta .querySelector e .querySelectorAll , ma ci sono librerie di terze parti che estendono HtmlAgilityPack con esso: vale a dire Fizzler e CssSelectors . Sia Fizzler che CssSelector implementano QuerySelectorAll , quindi puoi utilizzarlo in questo modo:

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

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

Con le classi definite in runtime:

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

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

    return doc.QuerySelectorAll( selector  );
}

Risposta popolare

Puoi risolvere il tuo problema usando la funzione 'contiene' all'interno della tua query Xpath, come di seguito:

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

Per riutilizzare ciò in una funzione, fare qualcosa di simile al seguente:

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



Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché