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

c# html html-agility-pack

Question

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 qu'un exemple comme celui-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 suivante : Pack d'agilité HTML Obtenir tous les éléments par classe

Réponse acceptée

(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 mots. Par conséquent, Contains("float") renvoie true pour "foo float bar" (correct) et "unfloating" (qui est Incorrect).

La solution consiste à faire en sorte que "float" (ou quel que soit 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 de l’utilisation d’une instance Regex est qu’elles peuvent être lentes si vous n’utilisez pas l’option .Compiled - et leur compilation peut être lente. 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 une chaîne de mots par mot-frontières sans utiliser d'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, sans utiliser 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é au moment 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 faire correspondre tous, vous pouvez créer un tableau de Regex objets et d' assurer qu'ils sont tous matching, ou les combiner en un seul Regex en utilisant lookarounds, mais cela se traduit dans les expressions effroyablement compliquées - donc utiliser un Regex[] est probablement mieux:

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 la correspondance des chaînes au lieu d'une expression régulière 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 en premier, les enfants!)

Cette méthode ci-dessous: CheapClassListContains fournit une fonction de correspondance de chaîne de contrôle de limite de mot rapide qui peut ê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é. Il ne prend pas en charge .querySelector et .querySelectorAll , mais il existe des bibliothèques tierces qui étendent HtmlAgilityPack avec: à savoir Fizzler et CssSelectors . Fizzler et CssSelectors implémentent tous deux QuerySelectorAll , vous pouvez donc l'utiliser comme QuerySelectorAll :

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

Réponse populaire

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));



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi