(更新2018-03-17)
问题:
正如您所发现的,问题是String.Contains
不执行单词边界检查,因此对于“ foo float bar”(正确)和“ unfloating”(不正确)都Contains("float")
将返回true
。
解决方案是确保在两端的单词边界旁边显示“ 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
使用环顾四周将它们组合成一个单一的对象,但这会导致极其复杂的表达式-因此使用一个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
)
);
}
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选择器库:
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 );
}