web-dev-qa-db-fra.com

Pack d'agilité HTML obtenir tous les éléments par classe

Je me lance à l'assaut du pack d'agilité html et j'ai du mal à trouver la bonne façon de procéder.

Par exemple:

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

Cependant, vous pouvez évidemment ajouter des classes à bien plus que des divs, alors j’ai essayé cela ..

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

Mais cela ne gère pas les cas où vous ajoutez plusieurs classes et où "float" n’est que l’une de celles-ci.

class="className float anotherclassName"

Y at-il un moyen de gérer tout cela? Je veux fondamentalement sélectionner tous les nœuds qui ont une classe = et qui contiennent float.

** La réponse a été documentée sur mon blog avec une explication complète à l'adresse: Pack d'agilité HTML Obtenir tous les éléments par classe

67
Adam

(Mise à jour 2018-03-17)

Le problème:

Le problème, comme vous l'avez remarqué, est que String.Contains n'effectue pas de vérification des limites de Word. Contains("float") renverra donc true pour "foo float bar" (correct) et "unfloating" (incorrect).

La solution consiste à faire en sorte que "float" (ou le nom de votre classe souhaitée) apparaisse à côté d'une limite de mot aux deux extrémités. Une limite de mot est soit le début (ou la fin) d'une chaîne (ou d'une ligne), des espaces, une certaine ponctuation, etc. Dans la plupart des expressions régulières, il s'agit de \b. Donc, la regex que vous voulez est simplement: \bfloat\b.

L’inconvénient d’utiliser une instance Regex est qu’ils peuvent être lents à s’exécuter si vous n’utilisez pas l’option .Compiled - et qu’ils peuvent être lents à compiler. Donc, vous devriez mettre en cache l'instance regex. Ceci est plus difficile si le nom de classe que vous recherchez pour les modifications au moment de l'exécution.

Vous pouvez également rechercher des mots dans une chaîne par mots-frontières sans utiliser une expression rationnelle en l'implémentant en tant que fonction de traitement de chaîne C #, en veillant à ne pas créer de nouvelle chaîne ou autre allocation d'objet (par exemple, en n'utilisant pas String.Split).

Approche 1: Utiliser une expression régulière:

Supposons que vous souhaitiez simplement rechercher des éléments avec un seul nom de classe spécifié lors de la conception:

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

Si vous devez choisir un seul nom de classe au moment de l'exécution, vous pouvez créer un regex:

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

Si vous avez plusieurs noms de classe et que vous voulez les faire correspondre, vous pouvez créer un tableau d'objets Regex et vous assurer qu'ils correspondent tous, ou les combiner en une seule Regex à l'aide de lookarounds, mais cela entraîne horriblement expressions compliquées - utiliser un Regex[] est probablement préférable:

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

Méthode 2: Utilisation de la correspondance de chaîne non regex:

L'avantage d'utiliser une méthode personnalisée C # pour faire de la correspondance de chaîne au lieu d'un regex est une performance supposément plus rapide et une utilisation réduite de la mémoire (bien que Regex puisse être plus rapide dans certaines circonstances - profilez toujours votre code, gamins!)

La méthode ci-dessous: CheapClassListContains fournit une fonction rapide de correspondance de chaîne de vérification des limites de Word pouvant être utilisée de la même manière que 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;
}

Approche 3: Utiliser une bibliothèque de sélecteur CSS:

HtmlAgilityPack est quelque peu stagné. Ne supporte pas .querySelector et .querySelectorAll, mais il existe des bibliothèques tierces qui étendent HtmlAgilityPack avec lui: à savoir Fizzler _ et CssSelectors . Fizzler et CssSelectors implémentent QuerySelectorAll, vous pouvez donc l'utiliser comme suit:

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

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

Avec les classes définies à l'exécution:

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

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

    return doc.QuerySelectorAll( selector  );
}
88
Dai

Vous pouvez résoudre votre problème en utilisant la fonction 'contient' dans votre requête Xpath, comme ci-dessous:

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

Pour réutiliser ceci dans une fonction, procédez comme suit: 

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

J'ai beaucoup utilisé cette méthode d'extension dans mon projet. J'espère que ça va aider l'un d'entre vous.

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