Ответ 1
(Обновлено 2018-03-17)
Проблема:
Проблема, как вы заметили, заключается в том, что String.Contains
не выполняет проверку границ слов, поэтому Contains("float")
вернет true
для "foo float bar" (правильный) и "разворачивания" (который неправильно).
Решение состоит в том, чтобы гарантировать, что "float" (или любое другое ваше желаемое имя класса) появляется рядом с границей слов на обоих концах. Слово-граница - это начало (или конец) строки (или строки), пробелов, определенных знаков препинания и т.д. В большинстве регулярных выражений это \b
. Итак, вы хотите просто регулярное выражение: \bfloat\b
.
Недостатком использования экземпляра Regex
является то, что они могут работать медленно, если вы не используете параметр .Compiled
- и они могут быть медленными для компиляции. Поэтому вы должны кэшировать экземпляр regex. Это сложнее, если имя класса, которое вы ищете для изменений во время выполнения.
Кроме того, вы можете искать строку для слов по границам слов без использования регулярного выражения, реализуя регулярное выражение как функцию строковой обработки С#, стараясь не вызывать новую строку или другое распределение объектов (например, не используя 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: Использование сопоставления строк без регулярных выражений:
Преимущество использования пользовательского метода С# для выполнения сопоставления строк вместо регулярного выражения является гипотетически более быстрой производительностью и уменьшенным использованием памяти (хотя в некоторых случаях Regex
может быть быстрее), всегда сначала просматривайте свой код, дети!)
Этот метод ниже: CheapClassListContains
обеспечивает быструю функцию сопоставления строк с проверкой regex.IsMatch
которая может использоваться так же, как 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 Selector:
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 );
}