c# - parser - Html Agility Pack obtiene todos los elementos por clase
htmlagilitypack nuget (5)
Apredaré el paquete de agilidad html y tendré problemas para encontrar la forma 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 probé esto.
var allLinksWithDivAndClass = _doc.DocumentNode.SelectNodes("//*[@class=/"float/"]");
Pero eso no soluciona los casos en los que se agregan varias clases y "flotar" es solo uno de ellos como este ...
class="className float anotherclassName"
¿Hay alguna manera de manejar todo esto? Básicamente, quiero seleccionar todos los nodos que tienen una clase = y contiene flotante.
** La respuesta ha sido documentada en mi blog con una explicación completa en: Html Agility Pack Obtener todos los elementos por clase
(Actualizado 2018-03-17)
El problema:
El problema, como has visto, es que String.Contains
no realiza una verificación de límite de palabras, por lo que Contains("float")
devolverá true
para "foo float bar" (correcto) y "unfloating" (que es incorrecto).
La solución es garantizar que "flotante" (o el nombre de clase que desee) aparezca junto a un límite de palabras en ambos extremos. Un límite de palabras 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
. Entonces, la expresión regular que desea es simplemente: /bfloat/b
.
Una desventaja de usar una instancia de Regex
es que pueden ejecutarse lentamente si no usas la opción .Compiled
, y pueden tardar en compilarse. Por lo tanto, debe almacenar en caché la instancia de expresiones regulares. Esto es más difícil si el nombre de clase que está buscando cambia en tiempo de ejecución.
Alternativamente, puede buscar una cadena de palabras por límite de palabras sin usar una expresión regular implementando la expresión regular como una función de procesamiento de cadenas C #, teniendo cuidado de no causar ninguna nueva cadena u otra asignación de objetos (por ejemplo, no usar String.Split
).
Enfoque 1: Usar una expresión regular:
Supongamos que solo quiere buscar elementos con un nombre de clase único y de tiempo de diseño específico:
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, entonces puede compilar 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 hacer coincidir todos ellos, puede crear una matriz de objetos Regex
y asegurarse de que todos coinciden, o combinarlos en una sola Regex
usando lookarounds, pero esto da como resultado 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: Usar la coincidencia de cadenas no regex:
La ventaja de utilizar un método C # personalizado para hacer coincidencias de cadenas en lugar de expresiones regulares es un rendimiento hipotéticamente más rápido y un uso reducido de la memoria (aunque Regex
puede ser más rápido en algunas circunstancias: ¡siempre primero debe crear un perfil de su perfil, niños!)
Este método a continuación: CheapClassListContains
proporciona una función de coincidencia de cadenas de comprobación de límite de palabras rápida 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: Uso de una biblioteca de CSS Selector:
HtmlAgilityPack está un tanto estancado, no admite .querySelector
y .querySelectorAll
, pero hay bibliotecas de terceros que amplían HtmlAgilityPack con él: concretamente, Fizzler y CssSelectors . Tanto Fizzler como CssSelectors implementan QuerySelectorAll
, por lo que puede usarlo de la siguiente manera:
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 );
}
Puede resolver su problema utilizando la función ''contiene'' en 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));
Puede usar la siguiente secuencia de comandos:
var findclasses = _doc.DocumentNode.Descendants("div").Where(d =>
d.Attributes.Contains("class") && d.Attributes["class"].Value.Contains("float")
);
Utilicé este método de extensión mucho en mi proyecto. Espero que ayude a uno de ustedes, chicos.
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));
}
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;
}