Html Agility Pack consigue todos los elementos por clase

c# html html-agility-pack

Pregunta

Estoy probando el paquete de agilidad de html y tengo problemas para encontrar la manera correcta de hacerlo.

Por ejemplo:

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

Sin embargo, obviamente puedes agregar clases a mucho más que divs, así que intenté esto ...

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

Pero eso no controla los casos en los que agregas varias clases y "flotar" es solo uno de ellos como este ...

class="className float anotherclassName"

¿Hay una manera de manejar todo esto? Básicamente quiero seleccionar todos los nodos que tienen una clase = y contiene flotante.

** La respuesta se ha documentado en mi blog con una explicación completa en: Html Agility Pack Obtenga todos los elementos por clase

Respuesta aceptada

(Actualizado el 2018-03-17)

El problema:

El problema, como has visto, es que String.Contains no realiza una comprobación de límite de palabra, por lo que Contains("float") devolverá true para "foo float bar" (correcto) y "unfloating" (que es incorrecto).

La solución es garantizar que aparezca "flotante" (o cualquiera que sea su nombre de clase deseado) junto a un límite de palabra en ambos extremos. Un límite de palabra es el inicio (o final) de una cadena (o línea), espacios en blanco, cierta puntuación, etc. En la mayoría de las expresiones regulares esto es \b . Así que la expresión regular que desea es simplemente: \bfloat\b .

Una desventaja de usar una instancia de Regex es que pueden Regex en ejecutarse si no usa la opción .Compiled , y pueden .Compiled en compilarse. Así que deberías cachear la instancia de expresiones regulares. Esto es más difícil si el nombre de la clase que está buscando cambia en tiempo de ejecución.

Alternativamente, puede buscar palabras en una cadena por límites de palabras sin usar una expresión regular implementando la expresión regular como una función de procesamiento de cadenas en C #, teniendo cuidado de no causar ninguna cadena nueva u otra asignación de objetos (por ejemplo, no utilizando String.Split ).

Enfoque 1: Usando una expresión regular:

Supongamos que solo desea buscar elementos con un único nombre de clase especificado en tiempo de diseño:

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 necesita elegir un solo nombre de clase en tiempo de ejecución, puede crear una expresión regular:

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 tiene varios nombres de clase y desea unirlos a todos, puede crear una matriz de objetos Regex y asegurarse de que todos coincidan, o combinarlos en un solo Regex con lookarounds, pero esto da lugar a expresiones terriblemente complicadas . así que usar un Regex[] es probablemente mejor:

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

Enfoque 2: uso de coincidencia de cadenas no regex:

La ventaja de usar un método de C # personalizado para hacer una coincidencia de cadenas en lugar de una expresión regular es hipotéticamente un rendimiento más rápido y un uso de memoria reducido (aunque Regex puede ser más rápido en algunas circunstancias, ¡siempre Regex su código primero, niños!)

Este método a continuación: CheapClassListContains proporciona una función rápida de coincidencia de cadenas de comprobación de límites de palabras que se puede utilizar de la misma manera 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;
}

Enfoque 3: utilizando una biblioteca de selector de CSS:

HtmlAgilityPack está algo estancado, no es compatible con .querySelector y .querySelectorAll , pero hay bibliotecas de terceros que amplían HtmlAgilityPack con él: Fizzler y CssSelectors . Tanto Fizzler como CssSelectors implementan QuerySelectorAll , por lo que puedes usarlo así:

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

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

Con clases definidas en tiempo de ejecución:

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

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

    return doc.QuerySelectorAll( selector  );
}

Respuesta popular

Puede resolver su problema usando la función 'contiene' dentro de su consulta Xpath, como se muestra a continuación:

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

Para reutilizar esto en una función, haga algo similar a lo siguiente:

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


Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué