(Оновлено 17.03.2018)
Проблема:
Проблема, як ви помітили, полягає в тому, 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
використовуючи підстановки, але це призводить до жахливо складних виразів - тому використання aRegex[]
, мабуть, краще:
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
)
);
}
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;
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 );
}