更新时间 2018-03-17
问题:
问题是,正如你所发现的那样,String.Contains
没有执行单词边界检查,因此Contains("float")
会对"foo float bar"(正确)和"unfloating"(不正确)都返回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
对象的数组,并确保它们都匹配,或者使用lookarounds将它们合并成一个单一的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", "") )
)
);
}
方法二:使用非正则表达式字符串匹配:
使用自定义的 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;
}
方法三:使用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 );
}
Contains()
方法不能用于该属性,因此需要将d.Attributes["class"].Contains("float")
替换为d.Attributes["class"].Value.Split(' ').Any(b => b.Equals("float"))
。新代码可以检查属性值中是否包含"class"属性中的"float"值。 - maxpContains
对我来说可行,因为我编写了自己的扩展方法。 - DaiValue.Contains("float")
也会匹配该类。 - ticCheapClassListContains
可能比正则表达式更便宜,并且实现了相同的逻辑 - 但是,是的,那也是一个选项。 - Dai