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 findclasses = _doc.DocumentNode.Descendants("div").Where(d => d.Attributes.Contains("class"));

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

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

¿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, ciertos signos de 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 ser lentos de ejecutar si no usa la opción .Compiled , y pueden ser lentos para compilar. 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:

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 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:

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

Enfoque 2: uso de coincidencia de cadenas no regex:

La ventaja de usar un método de C # personalizado para hacer la coincidencia de cadenas en lugar de una expresión regular es hipotéticamente un rendimiento más rápido y un menor uso de memoria (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 :

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

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 extienden HtmlAgilityPack con él: Fizzler y CssSelectors . Tanto Fizzler como CssSelectors implementan QuerySelectorAll , por lo que puedes usarlo así:

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

Con clases definidas en tiempo de ejecución:

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

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:

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



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é