Html Agility Pack получить все элементы по классам
Я беру удар на html agility pack и возникли проблемы с поиском правильного пути, чтобы пойти об этом.
например:
var findclasses = _doc.DocumentNode.Descendants("div").Where(d => d.Attributes.Contains("class"));
однако, очевидно, вы можете добавить классы намного больше, чем divs, поэтому я попробовал это..
var allLinksWithDivAndClass = _doc.DocumentNode.SelectNodes("//*[@class="float"]");
но это не обрабатывает случаи, когда вы добавляете несколько классов и "float" является только одним из них, как это..
class="className float anotherclassName"
есть ли способ справиться со всем этим? Я в основном хочу выбрать все узлы, которые имеют класс = и содержит float.
**ответ был задокументирован в моем блоге с полным объяснением по адресу:Html Agility Pack получить все элементы по классам
5 ответов:
(Обновлено 2018-03-17)
проблема:
проблема, как вы заметили, заключается в том, что
String.Contains
не выполняет проверку границы слова, поэтомуContains("float")
вернутсяtrue
как для " Foo float bar "(правильно), так и для" unfloating " (что неверно).решение состоит в том, чтобы гарантировать, что" float " (или независимо от вашего желаемого имени класса) появляется рядом со словом-граница на обоих концах. Граница слова - это либо начало (либо конец) слова строку (или строки), пробел, некоторые знаки препинания и т. д. В большинстве регулярных выражений это
\b
. Таким образом, регулярное выражение, которое вы хотите, просто:\bfloat\b
.недостаток использования
Regex
пример заключается в том, что они могут быть медленными для запуска, если вы не используете.Compiled
опция-и они могут быть медленными для компиляции. Поэтому вы должны кэшировать экземпляр регулярного выражения. Это сложнее, если имя класса, которое вы ищете, изменяется во время выполнения.в качестве альтернативы вы можете искать строку для слов по границам слов без использования регулярного выражения, реализуя регулярное выражение как функцию обработки строк C#, соблюдая осторожность, чтобы не вызвать какую-либо новую строку или другое выделение объекта (например, не используя
String.Split
).подход 1: Использование регулярного выражения:
предположим, что вы просто хотите искать элементы с одним, указанным во время разработки именем класса:
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", "") ) ); } }
Если вам нужно выбрать одно имя-класса во время выполнения, то вы можете построить регулярное выражение:
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", "") ) ); }
если у вас есть несколько имен классов и вы хотите, чтобы соответствовать всем из них, вы можете создать массив
Regex
объекты и убедитесь, что они все совпадают, или объединить их в одинRegex
использовать lookarounds, но это приводит в ужасно сложных выражениях - так что с помощьюRegex[]
лучше: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", "") ) ) ); }
подход 2: Использование нерегулярного сопоставления строк:
преимущество использования пользовательского метода C# для сопоставление строк вместо регулярного выражения гипотетически повышает производительность и уменьшает использование памяти (хотя
Regex
может быть быстрее в некоторых обстоятельствах-всегда профиль вашего кода в первую очередь, дети!)этот метод ниже:
CheapClassListContains
обеспечивает быструю функцию сопоставления строк проверки границ слов, которая может использоваться так же, как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; }
подход 3: использование библиотеки селекторов CSS:
HtmlAgilityPack несколько застопорился не поддерживает
.querySelector
и.querySelectorAll
, но есть сторонние библиотеки, которые расширяют HtmlAgilityPack с ним, а именно:Fizzler и CssSelectors. И Fizzler, и CssSelectors реализуютQuerySelectorAll
, так что вы можете использовать его вот так:private static IEnumerable<HtmlNode> GetDivElementsWithFloatClass(HtmlDocument doc) { return doc.QuerySelectorAll( "div.float" ); }
с определенными во время выполнения классами:
private static IEnumerable<HtmlNode> GetDivElementsWithClasses(HtmlDocument doc, IEnumerable<String> classNames) { String selector = "div." + String.Join( ".", classNames ); return doc.QuerySelectorAll( selector ); }
вы можете решить свою проблему с помощью функции "содержит" в вашем запросе Xpath, как показано ниже:
var allElementsWithClassFloat = _doc.DocumentNode.SelectNodes("//*[contains(@class,'float')]")
чтобы повторно использовать это в функции, сделайте что-то похожее на следующее:
string classToFind = "float"; var allElementsWithClassFloat = _doc.DocumentNode.SelectNodes(string.Format("//*[contains(@class,'{0}')]", classToFind));
я использовал этот метод расширения много в моем проекте. Надеюсь, это поможет одному из вас, ребята.
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)); }